アセンブリ解読 小休止

これまで、4回に渡ってコンパイラ(gcc v5.4.0)の生成するアセンブリコードを解読してきた。これまでの内容は、以下のような感じになる。

  1. その1: レジスタの使い方、関数の定石、初歩的な最適化
  2. その2: 条件分岐、除算の最適化
  3. その3: 除算の最適化と定数伝播、リンク時最適化
  4. その4: ループと自動ベクトル化

これまでは、私が読んでいく過程で何を思ったかをだだ漏れに垂れ流していたので、推敲も何もあったものではなく、読みやすくわかりやすい記事というには程遠かった。 ここで一度小休止してここまでのちょっとしたまとめをしてみよう。ただし、既にそれぞれの記事を読んでいる人にはこれは無意味な記録ということになる。 あと、私にはアセンブリの解説ができるほどの技量はまだないので、そういう方向の期待はあまりしないでほしい。

レジスタの役割や命令のまとめをしようと思ったが気力がなかったので箇条書きになった。回復したらやる。

その1

  • Linuxでは、第一から第六までの引数がそれぞれrdirsircxrdxr8r9に乗せられて渡される。それ以上はスタックに載せる。
  • Windowsでは、第一から第四までの引数がrcxrdxr8r9に乗せられて渡される(らしい)。それ以上はスタックに載せる。
  • 呼びだされた関数は、基本的にはprologueとepilogueを行う。これは主に以下のようなことを行う。
    • スタックベースポインタなどの値を退避し、あとで戻す
    • 関数内で使うスタックフレームをわかりやすくする
    • 必要ならスタックアロケーション
  • 最適化すると、必要ないスタックアロケーションやスタックメモリアクセスがなくなる。
  • 最適化すると、整数演算はアドレス演算で置き換えられることがある。

その2

  • testcmpでフラグを立てた後、je/jneなどのジャンプやcmovなどの条件命令によって条件分岐が行われる。
  • 最適化レベル0でも割り算は避けられ、マジックナンバーを使った乗算と桁あふれによって割り算が計算される。

その3

  • 最適化レベルが0でも、定数除算はマジックナンバーやシフト演算によって行われる。
  • ゼロクリアは即値0のmovよりも自分自身とのxorが好まれる(何故?)。
  • 渡す値が決まっている場合、できるところまで計算が進められる。場合によっては関数が即値に置き換えられる。
  • 翻訳単位を分けると、リンクしても関数は定数伝播・定数畳み込みの恩恵を受けられない。
  • ただし、リンク時最適化を行えば、別々にコンパイルしてリンクしても定数伝播・定数畳み込みの恩恵を受けられる。その代わり、コンパイル時間が爆発する。
  • リンク時最適化を行うためには、リンク時でなく、コンパイル時にリンク時最適化オプションをつける必要がある。

その4

  • ループは、比較とジャンプによってなされる。
  • -O2まではベクトル化などはなされず、比較とジャンプによる素直なコードが生成される。
  • -O3にすると、要素の余りの他にメモリアライメントまで考慮したベクトル化が行われる(少なくともgcc5.4では)。
  • -march=nativeにすると、使えるベクトル命令のうちで、データ量が十分多かった時に最も効率的と思われるものが使われるようになる。
  • なので、非常に小さい配列を渡すと、ベクトル化のためのオーバーヘッドが(その場合はそもそもの計算量が少ないので無視できる程度だが)出かねない。
    • 配列のサイズと関数の実装がコンパイラから同時に参照できる状態なら、定数伝播と関数コピー(-fipa-cp-cloneまたは-O3)によってサイズに応じた最適化が生じる可能性があるが、未検証。