C / C ++コンパイラが不良コードを生成する方法

これは、記事「 C / C ++コンパイラをtrickしてひどいコードを生成する方法 」の翻訳です。 」、原作者はAater Sulemanです。



コンピュータアーキテクチャのコースで、プロセッサはマシンのように見えると教えてくれました。 ハンドルとペダルはISA、エンジンはマイクロアーキテクチャー、プログラムはドライバーです。 この類推を続けると、コンピューターを使用することは、リモートコントロールを介してマシンを制御することに似ていると言えます。 コンソールはクールなものですが、同時にその仕組みを理解することも重要です。 プロのソフトウェアであっても、最も賢いコンパイラでさえ混乱させる可能性のある多くのコード例を見てきました。 この記事では、コンパイラーを難読化するための基本的な方法について説明します。







コンパイラーの説明に完全に入る前に、小さな免責事項を追加します:既存のコンパイラーには改善が必要だと思いますが、コンパイラーは常にプログラマーが少しでも手助けする必要があるボトルネックがあると思います。



コンパイラー



コンパイラーは、コードを高水準言語で取得し、マシンコードに変換します。 コンパイラの作業スキームは次のようになります。



  1. コードの解析を実行し、その中間表現を構築します。
  2. 中間プレゼンテーションを最適化します。
  3. マシンコードを生成します。
  4. リンク(レイアウト)。




コードを最適化するには、コンパイラがかなり大量の分析を実行する必要があります。 簡単な例は、デッドコード(DCE)の削除です。 DCEを実行すると、コンパイラは、実行がプログラムの結果に影響を与えないコードをプログラムから除外します。 たとえば、ループの結果はどこでも使用されないため、3行目と4行目のループはコンパイラーによって除外されます。



1: void foo(){ 2: int x; 3: for(int i = 0; i < N; i++) 4: x += i; 5: }
      
      







DCEを実行すると、コンパイラーはデータストリームの不使用分析を実行して、使用されていない変数を判断します。 次に、これらの未使用変数に書き込む行を削除します。 コンパイラーがDCEおよびその他の同様の最適化を実行できるようにするには、変数がアクセスされるかどうかを100%の確実性で検出できる必要があります。 これは、コンパイラを難読化する可能性が開かれる場所です。 よく知られているいくつかのボトルネックを使用できます。



関数呼び出し



コンパイラーによって実行される分析は、非常に複雑な計算タスクです。 参考までに、コンパイラ自体のコードはおそらく世界で最も複雑なコードです。 さらに、最適化の問題は非常に迅速に現れます。 コンパイル時間を妥当な制限内に保つために、コンパイラーは多くの場合、分析の範囲を関数に制限し、限られたプロシージャー間分析(IPA)を実行するか、まったく実行しません。 さらに、関数を独立したエンティティとして扱うコンパイラーは、複数のファイルを一度にコンパイルできます。



理想的には異なるファイルに散在する関数呼び出しの複数の層を追加することは、多くの場合、コンパイラを混乱させる良い方法です。



この例を見てみましょう:



 void foo(){ int x; for(int i = 0; i < N; i++) x += bar(i); }
      
      







そして別のファイルで:



 int bar(int i){ return i; }
      
      







プログラマーは、ここでの追加リソースはbar()



呼び出しにのみ費やされると考えるかもしれません。 ただし、 foo()



コンパイル中foo()



コンパイラーはbar()



グローバル変数やI / Oへの書き込みなどの副作用がないことを確認できません。 したがって、DCEが無効になり、役に立たないループも実行されます。 その結果、パフォーマンスが大幅に低下する可能性があります。



ポインタ



ポインターを不必要に使用する多くのプログラマーを見てきました。 理論的には、ポインターの場合、リソースの浪費の主な原因は、参照解除(存在する場合)です。 実際には、コンパイラを混乱させるため、はるかに多くのリソースを消費します。



理論的には、ポインターはメモリ内の任意の場所を指すことができます。 コンパイラは、コード内でポインターを検出すると、どの変数にアクセスするかを確認できなくなります。 したがって、彼はスコープ内のいくつかの変数にポインターを介してアクセスできることを提案します。 コンパイラの目には、これにより多くの誤った依存関係が作成されることがよくあります。 私の典型的な例はこのループです(画像処理プログラムで見つけました):



 for(int i = 0; i < N; i++) *a++ = *b++ + *c++;
      
      







入念に勉強した後、コードは実際には次のことを行っていることを理解できます。



 for(int i = 0; i < N; i++) a[i] = b[i] + c[i]; a += N; b += N; c += N;
      
      







最初のコードはコンパイラを混乱させます。 ループの反復が独立していても、コンパイラーはループの反復をそのように見なしません。 このため、特定のコードをベクトル化することはできません。 しかし、2番目-多分。



参考:ベクトル化されたコードの実行速度は5倍です。 マルチスレッド処理を使用すると、プログラマーの手間をほとんどかけずに、4コアで4倍の増加を実現できます。 自動的にベクトル化された同じコードは、努力なしで5倍の速度向上をもたらします。 Intel C Compiler(ICC)9.1およびGCC 4.1を試しました。 結果はICCのものです。



これ以上アイデアがない場合は、グローバル変数を使用します



グローバル変数は多くの理由で悪と見なされます。 もう一つ持ってきます。 グローバル変数を使用するため、グローバル変数を使用するコードはコンパイラーによって完全に最適化できません。 たとえば、変数N



グローバルではなくローカルの場合、次のコードの実行速度が30%速くなることに気付きました。



 for(int i = 0; i < N; i++) a[i] = b[i] + c[i];
      
      







変数N



グローバルに宣言すると、コンパイラは変数をメモリに残し、ループにロードする命令を追加します。 N



ローカルとして宣言されている場合、コンパイラはそれをレジスタにロードします。 コンパイラのこの動作を非難することもできます(Nはvolatile



はないため)が、その動作を確認する必要があります。



おわりに



C / C ++でコードを記述するときは、IPAとポインターを覚えておくことが重要です。 そのため、コンパイラーがより高速に実行されるコードを生成するのに役立ちます。



ところで: GCCとICCの両方がリンクの順序に敏感であることに気付きました。 生成されるコードは、リンカのコマンドラインで入力ファイルがリストされる順序によって異なります。 これは、キャッシュと分岐予測により、パフォーマンスに大きな影響(最大10%)を及ぼす可能性があります。 パフォーマンスを本当に重視する場合は、リンクの順序をいじってみてください。



All Articles