インライン展開がどう展開されるのかを調べた

C++コンパイラは、関数呼び出し部分にその関数の内容を展開し、
関数呼び出しのオーバーヘッドを削減する、インライン展開をします。

インライン展開はコンパイル時にされるため、
実際に行われたのか、どう行われているかは出力されません。

そのため、コンパイルたコードがどうなってるかを調べ、
インライン展開がどう展開しているのかを調べました。

なお、アセンブラに関してはほとんど説明しません。
「callq シンボル名(文字列)」で関数呼び出しを実行する事だけ理解していれば大丈夫です。

ソースコード

以下のソースコードを使います

test.cpp

#include "stdio.h"
#include "func.h"

int main(){
  TestA test;
  int a = test.getNumInCpp();
  int b = test.getNumInH();
  int c = test.getNumInline();
  int d = test.getNumCallCpp();
  printf("%d %d %d %d\n", a, b, c, d);

  return 0;
}

func.h

class TestA{
  private:
  int privateFunc();

  public:
  int getNumInCpp();

  int getNumInH() {
    return 42;
  }

  int getNumCallCpp(){
    return privateFunc() + getNumInH();
  }

  int getNumInline();

  //int getNumNormal();
};

inline int TestA::getNumInline(){return 321;}

// そもそも定義できない
// int TestA::getNumNormal(){return 111;}

func.cpp

#include "func.h"

int TestA::privateFunc(){
  return 123;
}

int TestA::getNumInCpp() {
  return 73 + getNumInH();
}

内容としては、

  • getNumInCpp()
    • cpp内に関数の中身が書かれている
    • cpp内のものはインライン展開されないはず
  • getNumInH()
    • ヘッダファイル内に関数の中身が置かれている
    • インライン展開される
  • getNumInline()
    • インライン不可能なprivateメソッドと、cpp内の関数を呼ぶ
  • getNumCallCpp()
    • 明示的にインライン展開指定をしたもの
    • インライン展開される

になります。

最適化しない場合

まずは最適化オプションをつけずにコンパイルしました。
g++ -S test.cpp func.cpp

そのため、インライン展開はされません。

subq	$32, %rsp
leaq	-8(%rbp), %rdi
movl	$0, -4(%rbp)
callq	__ZN5TestA11getNumInCppEv # (変数aの計算)
leaq	-8(%rbp), %rdi
movl	%eax, -12(%rbp)
callq	__ZN5TestA9getNumInHEv # (変数bの計算)
leaq	-8(%rbp), %rdi
movl	%eax, -16(%rbp)
callq	__ZN5TestA12getNumInlineEv # (変数cの計算)
leaq	-8(%rbp), %rdi
movl	%eax, -20(%rbp)
callq	__ZN5TestA13getNumCallCppEv # (変数dの計算)
leaq	L_.str(%rip), %rdi
movl	%eax, -24(%rbp)
movl	-12(%rbp), %esi
movl	-16(%rbp), %edx
movl	-20(%rbp), %ecx
movl	-24(%rbp), %r8d
movb	$0, %al
callq	_printf

callqで4種類の関数を全て呼び出しているのがわかります。
インライン展開はされていないため、func.hやfunc.cppで直接書いている数値はどこにも出てきません。

最適化した場合

O3オプションをつけてインライン展開されるようにしました。
g++ -S -O3 test.cpp func.cpp

なお、#で注釈を入れています

leaq	-24(%rbp), %rbx
movq	%rbx, %rdi

# getNumInCppの呼び出し(変数aの計算)
callq	__ZN5TestA11getNumInCppEv
movl	%eax, %r14d
movq	%rbx, %rdi
# privateFuncの呼び出し(変数dの計算)
callq	__ZN5TestA11privateFuncEv
# getNumInHの結果が直接書かれている(変数dの計算)
leal	42(%rax), %r8d
leaq	L_.str(%rip), %rdi
# getNumInHの結果が直接書かれている(変数bの計算)
movl	$42, %edx
# getNumInlineの結果が直接書かれている(変数cの計算)
movl	$321, %ecx
xorl	%eax, %eax
movl	%r14d, %esi

# printfの呼び出し
callq	_printf

最適化した場合、関数呼び出しの量も内容もかなり変化しています。

一番初めのgetNumInCpp関数はcppに書かれており、インライン展開が出来ないため、
最適化しない場合と同じく関数呼び出しをしています。

次にprivateFunc関数の呼び出しを行っていますが、
これはgetNumCallCpp関数が展開され、それ以上展開できないprivateFunc関数と、
42を返すだけのgetNumInH関数がさらにインライン展開されたものと思われます。
privateFunc関数はprivateメソッドですが、アセンブラではアクセス指定子は無視されます。

また、321を返すgetNumInline関数もインライン展開されて直接数値が書かれているのがわかります。

なお、ソースコード上で変数bやcに代入している部分は、
直接値を書いてある状態と同じになるようにインライン展開されるため、
コンパイラの最適化によって処理順番を入れ替えられ、printfへの呼び出し直前に移動させられています。

インライン指定しない場合のエラー

コメントアウトしてあるgetNumNormal関数は、ヘッダファイル内でインライン指定をせずに定義しています。
このコメントアウトを戻すと、以下のエラーにより失敗します。

duplicate symbol __ZN5TestA12getNumNormalEv in:
/var/folders/md/b8zf203j65b0qt_t4439fdvm0000gp/T/test-837e1d.o
/var/folders/md/b8zf203j65b0qt_t4439fdvm0000gp/T/func-c8b605.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

sファイルの作成には成功するため調べてみたところ、 getNumNormal関数がfunc.sとtest.s両方に定義されていました。

この関数はインライン展開されないため、func.hを読み込むtest.cppとfunc.cpp両方で定義されてしまい、
duplicate symbolになっています。

インライン展開されるgetNumInline関数は、関数自体はどこにも定義されないため二重定義にはならず、
問題なく動いているようです。

まとめ

  • 最適化しないとインライン展開されない
  • hファイルの中に実装を書くとインライン展開される
    • そもそもインライン展開しないとduplicateになる
      • そのため、ヘッダに書いた関数は全てインライン展開されるはず?
  • cppファイルに実装を書くとインライン展開されない
    • include対象に入ってないのだからあたりまえ
    • 複数のcppファイルに書かれた内容を繋げるのはリンク時なのでコンパイル後