アセンブリプログラムを書いていたら、遭遇したのでメモです。
さすがにプログラム全てをアセンブリ言語で書くのは大変なので、性能上重要ではないところはコンパイラのコード生成に任せて、性能上重要なところだけアセンブリプログラムで書くということはあると思います。 また、一からアセンブリ言語で書いたり全部イントリンシックで書くのも大変なので、コンパイラが出したコードをベースに改造することもあると思います。 この時、コンパイラが出すコードは呼び出し規約に沿っているので、性能上重要な関数を呼び出し規約に沿う範囲で自由に書き換えられると素朴に思っていたのですが、どうもそうではないということがわかりました。
問題を引き起こすコード
__attribute__((noinline)) char* id( char* x ) { return x; } int main( int argc, char* argv[] ) { for( int i = 0; i < argc; ++i ) { argv[i] = id(argv[i]); } }
gcc -O2
でコンパイルした場合、以下のコードになります(Compiler Explorerで確認したところ、gcc5~gcc13でどれもほぼ同じコードになりました)。
id: mov rax, rdi ret main: test edi, edi jle .L4 movsx rdi, edi lea rdx, [rsi+rdi*8] .L5: mov rdi, QWORD PTR [rsi] add rsi, 8 call id mov QWORD PTR [rsi-8], rax cmp rsi, rdx jne .L5 .L4: xor eax, eax ret
どこが問題か
このコードは、id
関数を呼び出してもrsi
やrdx
が変わらないことを前提としています。
rsi
やrdx
は呼び出し側保存レジスタであり、id
関数の中で書き換えても良いレジスタです。
つまり、このコードは呼び出し規約に沿っていないことになります。
おそらく、gccはid
関数でrsi
やrdx
が変わらないことを見抜くことで、このような積極的な最適化*1を行っているのだと思います。
こういうことをされたので、アセンブリファイルを勝手に書き換えたときにおかしくなったという話でした。