メインコンテンツへスキップ
  1. Posts/

org-mode 内でも google/benchmark を使いたい

·
目次
  • org-mode & google/benchmark
  • main関数を書かずに短く記載

きっかけ
#

競プロや普段の業務でコードを書く場合のスピード比較をする際、 C++ では std::chrono::high_resolution_clock::now() 1 で計測したい関数を囲い、処理時間を出力するなどしていた。

時間の計測はできるが複数処理を別々に計測したい場合は、

  • std::chrono::high_resolution_clock::now() まみれになったり
  • 平均を取るために何回もFor文で囲んだり、、、

本来の目的以外のコードが増えてしまうのが悩みだった。

そこで最近、 google/benchmark: A microbenchmark support library というものを知った 2。 Google謹製だし、使ってみるかと少し確認するコードを書いてみると、これが使いやすい。

自分は小さいコードの単位で試したい場合や試行メモを残して置きたい場合は、 Emacsorg-modeorg-babel というコード実行機能を使うことが多い3, 4

簡単にベンチマークが実行できるようになるので、 セットアップのメモとテンプレートを残す。

org-babel で google/benchmark をリンクする
#

インストール
#

brew install google-benchmark

サンプルコードを実行
#

benchmark/README.md からサンプルコードをコピペ。

#include <benchmark/benchmark.h> するために、 #+HEADER::flags を用いてライブラリをリンク。

org-babel のオプションは こちら によくまとまっています。

#+HEADER: :includes <benchmark/benchmark.h>
#+begin_src C++ :flags "-std=c++14 $(pkg-config --cflags --libs benchmark) -pthread"
コード本体
#+end_src

時間がかかると有名なアッカーマン関数5 を用いて、ベンチマークを実施。

次のSRCブロックを評価(C-c C-c)すると,エラーが出る。

#+HEADER: :includes <benchmark/benchmark.h>
#+begin_src C++ :flags "-std=c++14 $(pkg-config --cflags --libs benchmark) -pthread"
#include <benchmark/benchmark.h>

int ackernamm(int m, int n) {
  if (m == 0) {
    return n + 1;
  } else if (n == 0) {
    return ackernamm(m - 1, 1);
  } else {
    return ackernamm(m - 1, ackernamm(m, n - 1));
  }
}

static void BM_SomeFunction(benchmark::State& state) {
  // Perform setup here
  for (auto _ : state) {
    ackernamm(0, 0);
  }
}
// Register the function as a benchmark
BENCHMARK(BM_SomeFunction);
// Run the benchmark
BENCHMARK_MAIN();
#+end_src

1つ目のエラー
#

Macの場合は以下のようなエラーが出る。 Clangを使用するように強制してあげることで解決した。

Undefined symbols for architecture arm64:
  "benchmark::internal::Benchmark::Benchmark(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&)", referenced from:
      benchmark::internal::FunctionBenchmark::FunctionBenchmark(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char>> const&, void (*)(benchmark::State&)) in ccE6nhmb.o
  "std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::find(char, unsigned long) const", referenced from:
      benchmark::StrSplit(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, char) in libbenchmark.a[18](string_util.cc.o)
      benchmark::StrSplit(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&, char) in libbenchmark.a[18](string_util.cc.o)
  "std::__1::basic_stringbuf<char, std::__1::char_traits<char>, std::__1::allocator<char>>::str() const", referenced

NOTE: a missing vtable usually means the first non-inline virtual member function has no definition.
ld: symbol(s) not found for architecture arm64
collect2: error: ld returned 1 exit status
[ Babel evaluation exited with code 1 ]

(when (string= system-type "darwin")
  (setq org-babel-C++-compiler "clang++")
  )

2つ目のエラー
#

***.cpp:11:54: error: function definition is not allowed here
static void BM_SomeFunction(benchmark::State& state) {
                                                     ^
***.cpp:19:11: error: use of undeclared identifier 'BM_SomeFunction'
BENCHMARK(BM_SomeFunction);
          ^
***.cpp:21:1: error: function definition is not allowed here
BENCHMARK_MAIN();
^
/opt/homebrew/Cellar/google-benchmark/1.8.4/include/benchmark/benchmark.h:1694:35:
note: expanded from macro 'BENCHMARK_MAIN'
  int main(int argc, char** argv) {                                     \
                                  ^
***.cpp:21:1: error: conflicting types for 'main'
/opt/homebrew/Cellar/google-benchmark/1.8.4/include/benchmark/benchmark.h:1707:7:
note: expanded from macro 'BENCHMARK_MAIN'
  int main(int, char**)
      ^
***.cpp:8:5: note: previous definition is here
int main() {
    ^
4 errors generated.
[ Babel evaluation exited with code 1 ]

***.cpp の部分はランダム生成されたファイル名が入っている。 このファイルを見てみると以下のようになっている。

#include <benchmark/benchmark.h>

int main() {
#include <benchmark/benchmark.h>

int ackernamm(int m, int n) {
  if (m == 0) {
    return n + 1;
  } else if (n == 0) {
    return ackernamm(m - 1, 1);
  } else {
    return ackernamm(m - 1, ackernamm(m, n - 1));
  }
}

static void BM_SomeFunction(benchmark::State& state) {
  // Perform setup here
  for (auto _ : state) {
    ackernamm(3, 3);
  }
}
// Register the function as a benchmark
BENCHMARK(BM_SomeFunction);
// Run the benchmark
BENCHMARK_MAIN();
return 0;
}

org-babelは実行時に main 関数でWrapされた状態のファイルを作成している。 BENCHMARK_MAIN(); は マクロで、実行時に main関数に置換されるため、2つのmain関数が混在してエラーになる。

そこで、最終的には :main no タグを付けることでmain関数でのWrapを制限し、エラーが回避できる。

解決策
#

#+HEADER: :includes <benchmark/benchmark.h>
#+begin_src C++ :main no :flags "-std=c++14 $(pkg-config --cflags --libs benchmark) -pthread"
#include <benchmark/benchmark.h>

int ackernamm(int m, int n) {
  if (m == 0) {
    return n + 1;
  } else if (n == 0) {
    return ackernamm(m - 1, 1);
  } else {
    return ackernamm(m - 1, ackernamm(m, n - 1));
  }
}

static void BM_SomeFunction(benchmark::State& state) {
  // Perform setup here
  for (auto _ : state) {
    ackernamm(3, 3);
  }
}
// Register the function as a benchmark
BENCHMARK(BM_SomeFunction);
// Run the benchmark
BENCHMARK_MAIN();
#+end_src

main関数を書きたい場合
#

展開後の、main関数の中身を直接書くことで、 BENCHMARK_MAIN(); マクロを使用せずに実行することができる。

#+HEADER: :includes <benchmark/benchmark.h>
#+begin_src C++ :results scalar :exports both :flags "-std=c++14 $(pkg-config --cflags --libs benchmark) -pthread"
#include <benchmark/benchmark.h>

int ackernamm(int m, int n) {
  if (m == 0) {
    return n + 1;
  } else if (n == 0) {
    return ackernamm(m - 1, 1);
  } else {
    return ackernamm(m - 1, ackernamm(m, n - 1));
  }
}

static void BM_SomeFunction(benchmark::State& state) {
  // Perform setup here
  for (auto _ : state) {
    ackernamm(3, 3);
  }
}
// Register the function as a benchmark
BENCHMARK(BM_SomeFunction);
int main(int argc, char *argv[])
{
  ::benchmark::Initialize(&argc, argv);
  if (::benchmark::ReportUnrecognizedArguments(argc, argv)) return 1;
  ::benchmark::RunSpecifiedBenchmarks();
  ::benchmark::Shutdown();
}
#+end_src

さいごに
#

google/benchmarkの記事ではないので、メソッドについては深く触れないが、 Rnages() メソッドを用いることで、複数パラメータを振って評価することができる。

個人的には、 :main no タグと BENCHMARK_MAIN(); を用いて短く記載できることに利点を感じる。 以下のコードをスニペットに登録しておいた。

#+HEADER: :includes <benchmark/benchmark.h>
#+BEGIN_SRC C++ :results scalar :exports both :main no :flags "-stdlib=libc++ -std=c++14 $(pkg-config --cflags --libs benchmark) -pthread"
int ackernamm(int m, int n) {
  if (m == 0) {
    return n + 1;
  } else if (n == 0) {
    return ackernamm(m - 1, 1);
  } else {
    return ackernamm(m - 1, ackernamm(m, n - 1));
  }
}

static void BM_ackernamm(benchmark::State& state) {
  for (auto _ : state)
    ackernamm(state.range(0), state.range(1));
}

BENCHMARK(BM_ackernamm)->Ranges({{0, 3}, {0, 3}});
BENCHMARK_MAIN();
#+END_SRC

#+RESULTS:
#+begin_example
-----------------------------------------------------------
Benchmark                 Time             CPU   Iterations
-----------------------------------------------------------
BM_ackernamm/0/0       7.88 ns         7.85 ns     89593119
BM_ackernamm/1/0       8.91 ns         8.88 ns     78907025
BM_ackernamm/3/0       34.7 ns         34.6 ns     20173725
BM_ackernamm/0/1       7.85 ns         7.83 ns     89050593
BM_ackernamm/1/1       12.9 ns         12.8 ns     54442085
BM_ackernamm/3/1        197 ns          197 ns      3517800
BM_ackernamm/0/3       8.00 ns         7.90 ns     88878731
BM_ackernamm/1/3       21.1 ns         21.0 ns     33271543
BM_ackernamm/3/3       7033 ns         7031 ns       100921
#+end_example
ゆーとす/ut0s
著者
ゆーとす/ut0s
アラサー独身サラリーマン。個人開発者として活動中。
リリースしたプロダクトのみでの生活を夢見てます。