昨年、SDKを設計し、イベントのパッキング処理を担当しました。外部に対してはクラスインターフェースを提供し、サービス初期化時に呼び出し元が対応するクラスを実装し、オブジェクトポインタをモジュールに渡します。
C11にも触れており、好奇心で猫が死ぬように、これらのインターフェースをlambda関数オブジェクトのコールバックとして実現するとどうなるのか、純粋仮想関数インターフェース定義方法と比較して、より柔軟になるのか試してみようと考えました。
疑問が生じました。2つの異なる構文、性能面からどちらが速いのか不明です。コンパイラ原理は理解していないので、コードを書いて試してみます。
はじめに
オンラインのURLで、異なるコンパイラを選択したり、コンパイルパラメータを設定したり、linux
プラットフォーム上でコードを実行したり、対応するアセンブリコードを確認したりできます。
- https://wandbox.org/:時々技術検証のために、ウェブ上で小さなコードスニペットを実行するのは非常に便利です。
- https://godbolt.org/:異なる色でアセンブリコードと対応するコードを区別できるため、ローカルのデバッガよりもさらに便利です。
本文
標準委員会が定める文法規則について、コンパイルレベルにおいてどのように実現するかは、各社のコンパイラに依存します。この点については、特にマイクロソフトのコンパイラは非常に優れていると言わざるを得ません。文法糖衣は万能ではなく、コールバックインターフェースが少ないこと、ラムダ式を使用することでより便利であり、空のコールバック関数インターフェースを定義する必要がないことが挙げられます。コールバックインターフェースの種類が多い場合には、従来の仮想関数の方がビジネスインターフェースの統一に有利です。
- Windowsプラットフォームでは、両者の性能はほぼ同等で、大きな違いはありません。
- Linuxプラットフォームでは、仮想関数とラムダ式を比較すると、単回りは1.35ns増加します。
通常のビジネスシステム開発においては、この程度の性能損失は無視できる範囲内であり、ラムダ式を使用することで、設計面での利便性が向上します。特に多重信号処理を行う場合には顕著であり、底层にはイベントトリガーがあり、ログ出力が必要な場合、ログオブジェクトへの処理関数を呼び出します。より多くのビジネス処理インターフェースが必要な場合には、底层で
vector
にラムダオブジェクトを保存し、イベントトリガー時に順次呼び出しを行います。これはQTのシグナルとスロットに類似しており、ログ、監視、ビジネス1、ビジネス2といったものが完全に疎結合です。
コード
カウンター:1000000
時間:3966us
カウンター:1000000
時間:5316us
#include <iostream>
#include <chrono>
#include <memory>
#include <functional>
#include <atomic>
#include <string>
std::atomic_int64_t カウンター = 0;
// 回呼インターフェースを定義
class UserInterface
{
public:
virtual void name() = 0;
virtual void full_name() = 0;
};
class User : public UserInterface
{
public:
void name() {}
void full_name() { カウンター++; }
};
void to_string(UserInterface* user)
{
user->name();
user->full_name();
}
using name_handler = std::function<void()>;
using full_name_handler = std::function<void()>;
class Test
{
name_handler name_;
full_name_handler full_name_;
public:
void set_name_handler(name_handler name)
{
name_ = name;
}
void set_full_name_handler(full_name_handler full_name)
{
full_name_ = full_name;
}
void to_string()
{
name_();
full_name_();
}
};
int main()
{
User user;
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; i++)
{
to_string(&user);
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "カウンター: " << カウンター << std::endl;
std::cout << "時間: " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "us" << std::endl;
counter = 0;
auto name = []() {};
auto full_name = []() { カウンター++; };
Test test;
test.set_name_handler(name);
test.set_full_name_handler(full_name);
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000000; i++)
{
test.to_string();
}
end = std::chrono::high_resolution_clock::now();
std::cout << "カウンター: " << カウンター << std::endl;
std::cout << "時間: " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() << "us" << std::endl;
return 0;
}
- 付録 gist.githubusercontent.com/benloong/8050171/raw/fa577ec923b460862078b8b40233a42a1c619eeb/functionperformance.cpp のようなコードスニペットを参考にしました。
#include <iostream>
#include <chrono>
#include <memory>
#include <functional>
using namespace std;
using namespace std::chrono;
class Base
{
public:
Base(){}
virtual ~Base(){}
virtual int func(int i) = 0;
};
class Derived : public Base
{
public:
Derived(int base = 10) : base{base}
{
}
~Derived(){}
virtual int func(int i)
{
return i*base;
}
private:
int base;
};
struct Func
{
int base;
int operator()(int i)
{
return i*base;
}
Func(int base) : base {base}
{
}
};
const int base = 10;
int calculate(int i)
{
return base*i;
}
int main()
{
const int num = 10000;
Base *p = new Derived{10};
int total = 0;
auto start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += p->func(i);
}
auto end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\nvirtual call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
total = 0;
start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += calculate(i);
}
end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\ndirect function call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
Func functor{10};
total = 0;
start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += functor(i);
}
end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\nfunctor call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
int base = 10;
function<int(int)> lambda = [base](int i)
{
return i*base;
};
total = 0;
start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += lambda(i);
}
end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\nlambda call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
return 0;
}
/*
test on mac mini i7 2.7GHz
clang++ -std=c++11 chronotest.cpp -O0
output:
result: 499950000
virtual call elapsed: 43171 nanoseconds.
result: 499950000
direct function call elapsed: 31379 nanoseconds.
result: 499950000
functor call elapsed: 41497 nanoseconds.
result: 499950000
lambda call elapsed: 207416 nanoseconds.
===================================================
clang++ -std=c++11 chronotest.cpp -O1
output:
result: 499950000
virtual call elapsed: 261 ```
/*
*/
ここに、通常の関数と汎関数(ラムダ式)があり、コールバックインターフェースによる比較と直接呼び出しのパフォーマンスの違いは桁違いです。汎関数は関数に近いため、場合によっては汎関数のパフォーマンスが優れています。コンパイラの仕組みについては知識が不足しており、変数へのアクセスアドレスや関数が隣接していることがCPU処理を有利にするという推測です。
wandboxの結果を添付します。
``` - 付録
コードスニペット [functionperformance.cpp](https://gist.githubusercontent.com/benloong/8050171/raw/fa577ec923b460862078b8b40233a42a1c619eeb/functionperformance.cpp) を見つけました。
```c++
#include <iostream>
#include <chrono>
#include <memory>
#include <functional>
using namespace std;
using namespace std::chrono;
class Base
{
public:
Base(){}
~Base(){}
virtual int func(int i) = 0;
};
class Derived : public Base
{
public:
Derived(int base = 10) : base{base}
{
}
~Derived(){}
virtual int func(int i)
{
return i*base;
}
private:
int base;
};
struct Func
{
int base;
int operator()(int i)
{
return i*base;
}
Func(int base) : base {base}
{
}
};
const int base = 10;
int calculate(int i)
{
return base*i;
}
int main()
{
const int num = 10000;
Base *p = new Derived{10};
int total = 0;
auto start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += p->func(i);
}
auto end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\nvirtual call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
total = 0;
start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += calculate(i);
}
end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\ndirect function call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
Func functor{10};
total = 0;
start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += functor(i);
}
end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\nfunctor call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
int base = 10;
function<int(int)> lambda = [base](int i)
{
return i*base;
};
total = 0;
start = high_resolution_clock::now();
for (int i = 0; i < num; ++i)
{
total += lambda(i);
}
end = high_resolution_clock::now();
std::cout<<"result: "<<total<<"\nlambda call elapsed: \t"<<duration_cast<nanoseconds>(end-start).count()<<" nanoseconds.\n"<<std::endl;
return 0;
}
/*
test on mac mini i7 2.7GHz
clang++ -std=c++11 chronotest.cpp -O0
output:
result: 499950000
virtual call elapsed: 43171 nanoseconds.
result: 499950000
direct function call elapsed: 31379 nanoseconds.
result: 499950000
functor call elapsed: 41497 nanoseconds.
result: 499950000
lambda call elapsed: 207416 nanoseconds.
===================================================
clang++ -std=c++11 chronotest.cpp -O1
output:
result: 499950000
virtual call
## 付録
```shell
結果: 499950000
仮想呼び出し時間: 6143 ナノ秒。
結果: 499950000
直接関数呼び出し時間: 30 ナノ秒。
結果: 499950000
ファンクタ呼び出し時間: 31 ナノ秒。
結果: 499950000
ラムダ呼び出し時間: 15134 ナノ秒。