今日、/ r / C_Programmingで 、最適化に対するCのconst
の効果について質問しました。 過去20年にわたり、この問題に関するオプションを何度も聞いてきました。 個人的に、私はすべてのために命名const
を非難します。
次のプログラムを検討してください。
void foo(const int *); int bar(void) { int x = 0; int y = 0; for (int i = 0; i < 10; i++) { foo(&x); y += x; // this load not optimized out } return y; }
foo
関数はconstへのポインターを受け入れます。これは、 x
の値が変更されないことを著者foo
に代わって約束します。 コンパイラーは、 x
常にゼロ、つまりy
も同じであると想定するように思われるかもしれません。
ただし、いくつかの異なるコンパイラによって生成されたアセンブラコードを見ると、ループの各反復でx
ロードされていることがわかります。 これは、gcc 4.9.2が-O3を使用して生成したもので、私のコメントは次のとおりです。
bar: push rbp push rbx xor ebp, ebp ; y = 0 mov ebx, 0xa ; i sub rsp, 0x18 ; allocate x mov dword [rsp+0xc], 0 ; x = 0 .L0: lea rdi, [rsp+0xc] ; &x call foo add ebp, dword [rsp+0xc] ; y += x ( ?) sub ebx, 1 jne .L0 add rsp, 0x18 ; deallocate x mov eax, ebp ; y pop rbx pop rbp ret
clang 3.5(-fno-unroll-loopsを使用)はほぼ同じことを行い、ebpとebxのみが入れ替わり、 r14
で&x
の計算がループから抜け出しました。
両方のコンパイラーがこの有用な情報を利用できませんか? foo
x
変更しますか、未定義の動作ではないでしょうか? 奇妙なことに、答えはノーです。 この状況では 、これはfoo
完全に正しい定義になります。
void foo(const int *readonly_x) { int *x = (int *)readonly_x; // cast away const (*x)++; }
const
は定数を意味しないことを覚えておくことが重要です。 これは間違った名前であることに注意してください。 これは最適化ツールではありません。 コンパイラーではなくプログラマーに、コンパイル中に特定のクラスのエラーをキャッチするためのツールとして通知する必要があります。 APIで使用する場合、関数が引数をどのように使用するか、または呼び出し側が返されたポインターをどのように処理するかを指示するため、気に入っています。 通常、コンパイラの動作を変更するほど厳密ではありません。
私が言ったことにもかかわらず、コンパイラは const
を使用して最適化できる場合があります。 仕様C99、§6.7.3¶5では、これについて1つの提案があります。
const lvalue const, .
元のx
はconst修飾子がなかったため、この規則は適用されませんでした。 また、それ自体がconst
ではないオブジェクトを変更するために、非const
型にキャストすることに対するルールはありません。 つまり、上記のfoo
動作は、この呼び出しの未定義の動作ではありません。 foo
不確実性は、その原因に依存することに注意してください。
bar
1回変更するだけで、このルールを適用可能にし、オプティマイザーを機能させることができます。
const int x = 0;
コンパイラーは、 foo
x
変更は未定義の動作であると想定できるため、 決して発生しません 。 基本的にこれは、Cオプティマイザーがプログラムについて話す方法です。 コンパイラは、 x
が変更されないことを想定し、各反復での読み込みとy
両方を最適化できるようにします。
bar: push rbx mov ebx, 0xa ; i sub rsp, 0x10 ; allocate x mov dword [rsp+0xc], 0 ; x = 0 .L0: lea rdi, [rsp+0xc] ; &x call foo sub ebx, 1 jne .L0 add rsp, 0x10 ; deallocate x xor eax, eax ; 0 pop rbx ret
負荷が消え、 y
消え、関数は常にゼロを返します。
奇妙なことに、この仕様により、コンパイラはさらに先へ進むことができます。 読み取り専用メモリであっても、スタックのどこかにx
を配置できます。 たとえば、彼はそのような変換を行うことができます。
static int __x = 0; int bar(void) { for (int i = 0; i < 10; i++) foo(&__x); return 0; }
または、x86-64( -fPIC、スモールメモリモデル )では、さらにいくつかの命令を取り除くことができます:
section .rodata x: dd 0 section .text bar: push rbx mov ebx, 0xa ; i .L0: lea rdi, [rel x] ; &x call foo sub ebx, 1 jne .L0 xor eax, eax ; 0 pop rbx ret
clangもgccも、ここまでは行きません。これは、コードの記述が不十分だと危険だからです。
const
ルールに関する特別なルールがあっても、 const
を自分と仲間のプログラマーに使用してください。 オプティマイザーに、何が一定で何が一定でないかを自分で決めさせます。