Cの定数と最適化

今日、/ 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



を自分と仲間のプログラマーに使用してください。 オプティマイザーに、何が一定で何が一定でないかを自分で決めさせます。








All Articles