話の途中でちゃぶ台返しがあるので注意
if 文で処理を分岐してしまうのと、分岐処理を仮想関数の呼び出しによって実現するのと、どちらが速いのかを調べてみた。
#include <iostream>
#include <random>
#include <limits>
#include <array>
#include <chrono>
class c {
bool m_cond;
int m_val;
public:
c( bool const c, int const v )
: m_cond( c )
, m_val( v )
{}
int get() const {
if( m_cond ) return m_val * 3;
else return m_val * 5;
}
};
class b {
protected:
int m_val;
public:
b( int const v )
: m_val( v )
{}
virtual int get() const {
return m_val * 3;
}
};
class d : public b
{
public:
d( int const v )
: b( v )
{}
virtual int get() const {
return m_val * 5;
}
};
constexpr auto const size = static_cast< std::size_t >( 10000 );
constexpr auto const min = std::numeric_limits< int >::min() / static_cast< int >( size ) / 7;
constexpr auto const max = std::numeric_limits< int >::max() / static_cast< int >( size ) / 7;
template< typename T > void f( T const& a ) {
auto v = 0;
auto begin = std::chrono::high_resolution_clock::now();
for( auto const& e : a )
{
v += e->get();
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "total : " << v << std::endl;
std::cout << "time : " << std::chrono::duration_cast< std::chrono::nanoseconds >( end - begin ).count() << std::endl;
}
void f1() {
std::random_device rd;
std::default_random_engine mt( rd() );
std::uniform_int_distribution<> ucd( 0, 1 );
std::uniform_int_distribution<> uvd( min, max );
std::array< c*, size > a;
for( auto& e : a )
{
e = new c( static_cast< bool >( ucd( mt ) ), uvd( mt ) );
}
f( a );
}
void f2() {
std::random_device rd;
std::default_random_engine mt( rd() );
std::uniform_int_distribution<> ucd( 0, 1 );
std::uniform_int_distribution<> uvd( min, max );
std::array< b*, size > a;
for( auto& e : a )
{
if( ucd( mt ) ) e = new b( uvd( mt ) );
else e = new d( uvd( mt ) );
}
f( a );
}
int main()
{
f1();
f2();
}
クラス c
はメンバ変数 m_cond
によって実行時に if 文によって分岐を行う。クラス b
と d
は同様の分岐処理を仮想関数によって実現する。
各種変数の初期化には標準ライブラリの <random>
をし、時間の計測は <chrono>
を使用した。
実行結果は以下のとおり。
Start
total : -6772912
time : 282255
total : 729240
time : 248928
0
Finish
仮想関数版の方が速い。
そもそも、関数呼び出しという処理が超々大雑把に言ってしまえば
- レジスタの退避
- 引数とか
- コールスタック云々
- ジャンプ
という手順で行われる。仮想関数の呼び出しと通常関数の呼び出しの違いは、
ジャンプ先のアドレスが実行時に決定される変数なのか、コンパイル時に決定される定数なのか、だ。
よって if による分岐だけ、遅くなる。
関数呼び出しは、言ってしまえば「構造化された高級なジャンプ命令」である。
C 言語が高級アセンブラと呼ばれるのも頷ける。
が、最適化によってこの結果はひっくりかえる。
Start
total : 9144038
time : 56230
total : -1639019
time : 138375
0
Finish
コンパイルオプション -O2 -march=native
をつければこの通り。
見事に速度が逆転し、 if 文を使用する方が早くなっている。
最適化によって関数が inline
化されると、関数呼び出しに纏わる処理が不要になるため、
inline
化困難な仮想関数と比べると if による分岐の方が大幅に高速な結果となった。
勿論、 if 文内に入る評価式の実行が重い場合はこの限りではないし、
inline
化が行われない程度に長い関数であっても話は変わってくるだろう。
それだけでなく、最初にオブジェクトを生成する時に、動的確保を行うか否か、
その際に placement new
を用いるか否か、等々、状況によってどの部分にマシンパワーを割くべきかは変わってくる。
一先ず、このように十分に単純な内容であれば、仮想関数の呼び出しを行わない方が高速である。
追記
最適化によって処理順が入れ替わると困ると思ったので begin
と end
の宣言に volatile
をつけたら演算子が未定義であるというエラーメッセージと共に算術演算が出来なくなった。普段 volatile
をあまり使ったことが無いのだが、使うべき状況を勘違いしているのだろうか。