もしかすると一般的な話なのかもしれませんが,おもしろかったのでメモ書き程度に残しておきます *1.ソースコードはすべてアセンブリに直されているものだとばかり思っていましたが,そうではないんですね.
使ったツールは,Compiler Explorer というサイトです.
ちなみに,Rust のゼロコスト抽象化 (zero cost abstraction / zero overhead principle) について,アセンブラではどのような処理がなされているのかを調査していた最中に見つけました (ですが,今回はゼロコスト抽象化は関係のない話です.これはまた別途記事にしようと思います.).
C++
C++ で,次のようなコードをコンパイルさせて,アセンブリがどのように生成されるのかを見ていました.初めて見たんですが.
class A { private: int value; public: A(int a) : value(a) {} int get() { return value; } void set(int a) { value = a; } }; int main() { A a_stack_cpp(5); }
すると,つぎのようなアセンブリが生成されるはずです (以降,すべて x86-64, gcc 8.1 です).
A::A(int): pushq %rbp movq %rsp, %rbp movq %rdi, -8(%rbp) movl %esi, -12(%rbp) movq -8(%rbp), %rax movl -12(%rbp), %edx movl %edx, (%rax) nop popq %rbp ret main: pushq %rbp movq %rsp, %rbp subq $16, %rsp leaq -4(%rbp), %rax movl $5, %esi movq %rax, %rdi call A::A(int) movl $0, %eax leave ret
クラス用のコンストラクタと,main 関数に関するアセンブリが生成されているようですね.ここで,「set
や get
のアセンブリはじゃあどうなるんだろう?」という点が気になりました.C++ のアセンブラの出力内容を見るのは初めてだったので,あまり想像がつかなかったのでやってみました.
ということで, get
を使う記述を追加します.
class A { private: int value; public: A(int a) : value(a) {} int get() { return value; } void set(int a) { value = a; } }; int main() { A a_stack_cpp(5); a_stack_cpp.get(); // 追加した }
すると,アセンブリはつぎのように出力されました.
A::A(int): pushq %rbp movq %rsp, %rbp movq %rdi, -8(%rbp) movl %esi, -12(%rbp) movq -8(%rbp), %rax movl -12(%rbp), %edx movl %edx, (%rax) nop popq %rbp ret A::get(): pushq %rbp movq %rsp, %rbp movq %rdi, -8(%rbp) movq -8(%rbp), %rax movl (%rax), %eax popq %rbp ret main: pushq %rbp movq %rsp, %rbp subq $16, %rsp leaq -4(%rbp), %rax movl $5, %esi movq %rax, %rdi call A::A(int) leaq -4(%rbp), %rax movq %rax, %rdi call A::get() movl $0, %eax leave ret
これは興味深いですね.get
メソッドを呼び出す記述を追加すると,同時にアセンブラにも get
メソッドに関するアセンブリが追加されました.要するに,メソッドを使用するタイミングになってはじめて,必要な分だけアセンブリを生成するようになっているという感じでしょうか.逆に,set
に関するアセンブリは一切生成されていません.無駄がなくていいですね.
Rust
ここで普段使っている Rust がどうなっているのかも知りたくなってきますね.ということで,似たようなコードを書いてどういう動きをするのかを見てみましょう.(以下,rustc 1.27.1 です)
struct A { value: i32, } impl A { fn new(value: i32) -> A { A { value } } fn get(self) -> i32 { self.value } // set はちょっとめんどうだったので省略しました… } pub fn main() { let a = A::new(5); }
アセンブリに直してみましょう.
example::A::new: pushq %rbp movq %rsp, %rbp subq $4, %rsp movl %edi, -4(%rbp) movl -4(%rbp), %eax addq $4, %rsp popq %rbp retq example::main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $5, %edi callq example::A::new movl %eax, -4(%rbp) addq $16, %rsp popq %rbp retq
C++ と似たような匂いがしてきました.main 関数の中では A
の生成しか呼び出していないので,A
の生成部分しかアセンブリが生成されていません.こちらも無駄がなさそうです.
代わりに get
を一度だけ呼び出してみます.普段はこういう書き方しないけど.
struct A { value: i32, } impl A { fn new(value: i32) -> A { A { value } } fn get(self) -> i32 { self.value } } pub fn main() { let a = A::new(5); a.get(); }
すると
example::A::new: pushq %rbp movq %rsp, %rbp subq $4, %rsp movl %edi, -4(%rbp) movl -4(%rbp), %eax addq $4, %rsp popq %rbp retq example::A::get: pushq %rbp movq %rsp, %rbp movl %edi, %eax popq %rbp retq example::main: pushq %rbp movq %rsp, %rbp subq $16, %rsp movl $5, %edi callq example::A::new movl %eax, -4(%rbp) movl -4(%rbp), %edi callq example::A::get movl %eax, -8(%rbp) addq $16, %rsp popq %rbp retq
無事,get
関数に関するアセンブリが追加されたことがわかりました *2.
まとめ
- C++ や Rust においては,メソッド (あるいは関数) に関するアセンブリは,1回以上呼び出されたもののみ生成される.
- 言語にもよりますが通常,関数やメソッドは意味解析の段階でコンパイラ側(あるいはインタプリタ側の)の仮想のスタックに積まれて,そのスタック内で順繰りに評価が走って実行されます.積まれた順に各言語のVM用の命令に直されます.そもそも関数呼び出しが起きなければスタックに積まれずそこに対する処理が走らないため,結果的にアセンブリが生成されなかったという話なんですかね.gcc も rustc もまったく詳しくないのでそうなってるかはわかりませんが.
- ちなみに C++ 側で,変数を volatile にしてみても結果は変わらなかったので,最適化とは関係なさそうというところまではわかっています.その先がよくわからないので,上の解釈が正しいかは微妙なところですね….