ダイナミックライブラリの呼び出しは遅い？

ちょっとたくさん計算するプログラムをC++で書いていて、パラメータをいろいろ渡すのにpythonから呼べたら便利だと思って pybind11 でいろいろ試していたのですが、pythonから呼び出すとやけに計算が遅いことに気が付きました。

何が悪いんだろうと思っていろいろ試した結果、pythonのモジュールにするためにダイナミックライブラリにすると遅いらしいと言うところにたどり着きました。

実際に手元で試せるように、[github](https://github.com/false-git/dynamic_library)にソースを上げてあります。

# テストプログラムの構成

テストプログラムは以下の構成になっています。
* テストドライバ(実行ファイルになる)
* 静的にリンクされる関数(実行ファイルに静的にリンクされる)
* 静的にリンクされる関数を呼び出す関数(実行ファイルに静的にリンクされる)
* 動的にリンクされる関数(実行ファイルに動的にリンクされるライブラリ)
* 動的にリンクされる関数を呼び出す関数(実行ファイルに動的にリンクされるライブラリ)

## テストドライバ

4パターンに分けて関数を呼び出し、かかった時間をミリ秒単位で出力します。

```dynamic_test.cc
#include <chrono>
#include <iostream>

using func = int (*)(int);

int static_func(int);
int static_func_func(int);
int static_func_pfunc(int);
int dynamic_func(int);
int dynamic_func_func(int);
int dynamic_func_pfunc(int);

int run(const std::string &name, int pc, int cc, func f) {
  int t{0};
  std::chrono::system_clock::time_point start, end;
  start = std::chrono::system_clock::now();
  for (int i = 0; i < pc; i++) {
    t += f(cc + i);
  }
  end = std::chrono::system_clock::now();
  auto elapsed =
      std::chrono::duration_cast<std::chrono::milliseconds>(end - start)
          .count();
  std::cout << name << ": " << elapsed << std::endl;
  return t;
}

int main() {
  run("static_func", 100000000, 1, static_func);
  run("dynamic_func", 100000000, 1, dynamic_func);
  run("static_func_func", 1, 100000000, static_func_func);
  run("dynamic_func_func", 1, 100000000, dynamic_func_func);
  run("static_func_pfunc", 1, 100000000, static_func_pfunc);
  run("dynamic_func_pfunc", 1, 100000000, dynamic_func_pfunc);
  return 0;
}
```

## 実行ファイルに静的にリンクされる関数

この関数を、テストドライバから直接1億回呼び出します。
テストドライバと別のソースファイル(別のコンパイル単位)にしないと、コンパイラの最適化で消えてしまいますので注意が必要です。

```static_func.cc
static int t;

using func = int (*)(int);

int static_func(int a) {
  t += a;
  return t;
}

func sf = static_func;
```

## 静的リンクされる関数を呼び出す関数

```static_func_func.cc
int static_func(int);

using func = int (*)(int);
extern func sf;

static int t;

int static_func_func(int a) {
  for (int i = 0; i < a; i++) {
    t += static_func(i);
  }
  return t;
}

int static_func_pfunc(int a) {
  for (int i = 0; i < a; i++) {
    t += sf(i);
  }
  return t;
}
```

## 動的にリンクされる関数

この関数を、テストドライバからダイナミックリンクして1億回呼び出します。

```dynamic_func.cc
static int t;

using func = int (*)(int);

int dynamic_func(int a) {
  t += a;
  return t;
}

func df = dynamic_func;
```

## 動的にリンクされる関数を呼び出す関数

```dynamic_func_func.cc
int dynamic_func(int);

using func = int (*)(int);
extern func df;

static int t;

int dynamic_func_func(int a) {
  for (int i = 0; i < a; i++) {
    t += dynamic_func(t + i);
  }
  return t;
}

int dynamic_func_pfunc(int a) {
  for (int i = 0; i < a; i++) {
    t += df(t + i);
  }
  return t;
}
```

# 結果

Ubuntu 18.04 clang++ 6.0.0 で実行した結果です。

```sh
static_func: 349
dynamic_func: 975
static_func_func: 333
dynamic_func_func: 989
static_func_pfunc: 956
dynamic_func_pfunc: 949
```

この結果からは、ダイナミックライブラリにある関数の呼び出しは、静的にリンクした関数の呼び出しに対して2～3倍程度のオーバーヘッドがかかると言えそうです。
Windows(VisualStudio2019)でも似たようなことをやりましたが、50倍程度の時間差が出ました。

試しにと思い、clang++ではなくg++ 7.4.0でやってみたところ、以下の結果になりました。
(最適化オプションはいずれも-Ofastです)

```sh
static_func: 784
dynamic_func: 791
static_func_func: 306
dynamic_func_func: 1001
static_func_pfunc: 923
dynamic_func_pfunc: 944
```

・・・良くわからなくなってしまいました。
なんとなく言えることは、関数ポインタを通すと遅い、ダイナミックリンクするともっと遅い。

ただ、実際のプログラムでは関数呼び出しがプログラム全体に占める割合と言うのは微々たるもので、実際にはIO待ちなどが大勢を占めます。今回のサンプルのような大きな差がでることはあまり考えなくて良いと言うことです。
(とは言え、ひたすらメモリの中だけでごにょごにょ計算するようなプログラムは、ダイナミックリンクされた関数呼び出しのオーバーヘッドが無視できない場合もありますよ、と言うことで)

# 修正 2019.9.2 20:45

コメントで、10億回のループと関数呼び出しにしては早すぎるのでは、と指摘を受けたので確認してみたところ、ソースには1億回と書いてありました。
なので、上の本文の10億回は1億回に修正しました。
また、MacBook Proとclang++ 10.0.1で試したところ、だいぶ違う結果になりました。

```sh
static_func: 188
dynamic_func: 213
static_func_func: 233
dynamic_func_func: 215
static_func_pfunc: 221
dynamic_func_pfunc: 223
```

これが、OSの違いによるものなのか、llvmのバージョンの違いによるものなのかわかりませんが、ダイナミックリンクしてもパフォーマンスが変わらない環境はあるようです。
(一応、ループ回数を1億回から2億回、10億回に変えて試したところ、それぞれ2倍、10倍近い時間がかかったので、最適化でループが消えていると言うことはないと思います)

# 修正 2019.9.11

コメントで、-Ofastはおかしいのではと指摘をもらって、確かに同じモジュールと共有ライブラリでの関数呼び出しのコストの比較をするのに最適化は邪魔だと考えたので、-O0で測りなおしてみました。
さらに、いくつか関数を書き直していますが、面倒なのでこの記事には書きません。(githubにはpushしてあります)

```sh:clang++での結果
static_func_loop: 1527
static_func_func: 3978
static_func_pfunc: 10411
static_func_dfunc: 10025
dynamic_func_loop: 1577
dynamic_func_func: 10121
dynamic_func_pfunc: 10542
```

```sh:g++での結果
static_func_loop: 1504
static_func_func: 3365
static_func_pfunc: 9631
static_func_dfunc: 9516
dynamic_func_loop: 1621
dynamic_func_func: 9853
dynamic_func_pfunc: 10774
```