考察:関数ポインタとコンテナと設計思想

2020/02/05

C/C++ 技術 考察

t f B! P L
C/C++で実装を行うときに、「関数ポインタを介した関数の呼び出しは遅い」という論が有るようです。

実際のところ、コンパイルされて吐き出されたアセンブラを読めば、(小規模な関数ならば)呼び出し元のコードにインライン展開される通常の呼び出しより、関数ポインタのほうが呼び出しが遅くなるのは自明と言えるかもしれません。

ただ実際のところ個人的にはこれは誤差として無視してしまっていいものだと思っています。

※そもそも命令語数個の違いを意識しなければならないほど時間的精度を求められる分野ってなんだ…?

むしろ個人的には、if文やswitch文を使用した条件分岐と同等の呼び出し速度が得られるのであれば、動的に呼び順を組み替えることのできるジャンプテーブルの方が実装としては優れていると思っています。

そこで今回はジャンプテーブルを様々なコンテナを用いて実装して、どの程度差が出るものか比較してみました。

ベンチマーク処理の内容

int型のグローバル変数の値を更新する処理を、以下のパターンで実装してみて比較しました。
  • 直接書き換える
以下は関数呼び出しを介するケース
  • スイッチケースの条件分岐で関数を直接呼び出し
  • arrayコンテナのジャンプテーブルで書き換え
  • vectorコンテナのジャンプテーブルで書き換え
  • listコンテナのジャンプテーブルで書き換え
  • mapコンテナのジャンプテーブルで書き換え
これらの処理を一千万回ずつ行って時間を計測します。

予想としては、遅い順に
  1. map 連想配列は検索アルゴリズムが入るので遅い
  2. list シーケンシャルなら最速だが、擬似的にランダムアクセスしてるので
  3. switch ≒ vector = array 生成されるアセンブラ量てきにこんなもんのはず 
  4. direct  直接書くのはそりゃ早いだろうね
となるのが事前の予想でした。

まずは結論から

開発に関わる機材の性能は明かせませんが、結果としては以下のような形になりました。

実行時間 direct: 14053 μsec
実行時間 switch: 30705 μsec
実行時間 array:  45480 μsec
実行時間 vector: 30409 μsec
実行時間 list:  103001 μsec
実行時間 map:  977474 μsec


結構意外な結果です。
特に意外だったのはarrayとvectorに差が出たこと。

arrayのアクセスは基本的には定数時間でいけるはずなので、この差はおそらく実装の細かな差異と言えそうです。
というのも桁は多いですが、これμ秒オーダーの計測結果なので、上4つの内容に関しては0.05秒以内に処理を終えていることがわかります。

Listとmapが順当に遅いことに関しては予想通りです。

ただ一千万回という法外な呼び出し回数を行ったことを考えれば、1秒以内に処理を終えているmapコンテナもなかなかの実力です。

そう考えると、もはや実行速度は実装手段の選定の根拠とするには論拠として弱いと言える時代になったのではないと言えそうですね。

実際のところ実装方法を決定づけるのは

結局のところ実装方針を決定づけるのは、C/C++においては処理の可読性なのかなと、最近は考えています。

それは前項までの実験の結果からわかるとおり、処理時間に関して言えばSTLコンテナやアルゴリズムを適切に使用できれば、それほど躍起になって実装アルゴリズム高速化する意味はあまりありません。

むしろそういう切り口で考えれば、この実験結果としては低速なmapアルゴリズムも、可読性を確保しながらコマンド通信解析処理のシーケンスを組めたり、データベースアクセスを明快に組めたりと、利用する利点は多くあります。

もし実装するシステム全体を高速化したいのであれば、実際にはこうしたアルゴリズム選定よりもディスクアクセスやインターフェースの通信速度のほうがボトルネックとなってくるので、そういう重い動作をいかに少なく抑えてメモリ上で処理をするのか、といった部分に主眼をおいて考えることこそ重要だと言えるでしょう。

最後に

参考までに今回実験に使用したソースコードを以下に掲載しておきます。

ソースコードはこちら


#include < iostream >
#include < string >
#include < algorithm >
#include < array >
#include < vector >
#include < list >
#include < map >
#include < chrono >

using namespace std;
using CALLBACK = void (*)();

int g_trush;
void test1(){ g_trush = 1; }
void test2(){ g_trush = 2; }
void test3(){ g_trush = 3; }
void test4(){ g_trush = 4; }
void test5(){ g_trush = 5; }
void test6(){ g_trush = 6; }
void test7(){ g_trush = 7; }
void test8(){ g_trush = 8; }
void test9(){ g_trush = 9; }
void test0(){ g_trush = 0; }

void refarence()
{
    double execute_time;
    chrono::system_clock::time_point  start, end;
    
    start = chrono::system_clock::now();
    for( auto i = 0; i < 1000000; ++i )
    {
        for( auto j = 0; j < 10; ++j )
        g_trush = j;
    }
    end = chrono::system_clock::now();
    execute_time = chrono::duration_cast(end-start).count();
    cout << "実行時間 direct:\t" << execute_time << " μsec" << endl;
}

void by_switch()
{
    double execute_time;
    chrono::system_clock::time_point  start, end;
    
    start = chrono::system_clock::now();
    for( auto i = 0; i < 1000000; ++i )
    {
        for( auto j = 0; j < 10; ++j )
        {
            switch (j)
            {
            case 0:
                test0();
                break;
            case 1:
                test1();
                break;
            case 2:
                test2();
                break;
            case 3:
                test3();
                break;
            case 4:
                test4();
                break;
            case 5:
                test5();
                break;
            case 6:
                test6();
                break;
            case 7:
                test7();
                break;
            case 8:
                test8();
                break;
            case 9:
                test9();
                break;
            default:
                break;
            };
        }
    }
    end = chrono::system_clock::now();
    execute_time = chrono::duration_cast(end-start).count();
    cout << "実行時間 switch:\t" << execute_time << " μsec" << endl;
}

void by_array()
{
    array callback_array = {
        test0, test1, test2, test3, test4, 
        test5, test6, test7, test8, test9
    };
    double execute_time;
    chrono::system_clock::time_point  start, end;
    
    start = chrono::system_clock::now();
    for( auto i = 0; i < 1000000; ++i )
    {
        for( auto j = 0; j < 10; ++j )
        {
            callback_array[j]();
        }
    }
    end = chrono::system_clock::now();
    execute_time = chrono::duration_cast(end-start).count();
    cout << "実行時間 array:\t\t" << execute_time << " μsec" << endl;
}

void by_vector()
{
    vector callback_vector{
        test0, test1, test2, test3, test4, 
        test5, test6, test7, test8, test9
    };
    double execute_time;
    chrono::system_clock::time_point  start, end;
    
    start = chrono::system_clock::now();
    for( auto i = 0; i < 1000000; ++i )
    {
        for( auto j = 0; j < 10; ++j )
        {
            callback_vector[j]();
        }
    }
    end = chrono::system_clock::now();
    execute_time = chrono::duration_cast(end-start).count();
    cout << "実行時間 vector:\t" << execute_time << " μsec" << endl;
}

void by_list()
{
    list callback_list{
        test0, test1, test2, test3, test4, 
        test5, test6, test7, test8, test9
    };
    auto it = callback_list.begin();
    double execute_time;
    chrono::system_clock::time_point  start, end;
    
    start = chrono::system_clock::now();
    for( auto i = 0; i < 1000000; ++i )
    {
        for( auto j = 0; j < 10; ++j )
        {
            (*it)();
            ++it;
        }
        it = callback_list.begin();
    }
    end = chrono::system_clock::now();
    execute_time = chrono::duration_cast(end-start).count();
    cout << "実行時間 list:\t\t" << execute_time << " μsec" << endl;
}

void by_map()
{
    map callback_map{
        {0, test0}, {1, test1}, {2, test2}, {3, test3}, {4, test4}, 
        {5, test5}, {6, test6}, {7, test7}, {8, test8}, {9, test9}
    };
    double execute_time;
    chrono::system_clock::time_point  start, end;
    
    start = chrono::system_clock::now();
    for( auto i = 0; i < 1000000; ++i )
    {
        for( auto j = 0; j < 10; ++j )
        {
            callback_map[j]();
        }
    }
    end = chrono::system_clock::now();
    execute_time = chrono::duration_cast(end-start).count();
    cout << "実行時間 map:\t\t" << execute_time << " μsec" << endl;
}


int main(int argc, char** argv)
{
    refarence();
    by_switch();
    by_array();
    by_vector();
    by_list();
    by_map();

    return 0;
}

Translate

ページビューの合計

注意書き

基本的にごった煮ブログですので、カテゴリから記事を参照していただけると読みやすいかと存じます。

ADBlocker等を使用していると、Twitterやアクセスカウンタが表示されません。

記事を読むには差し支えませんが、情報を参照したい場合には一時例外にしていただけると全てご参照いただけます。

Featured Post

ボイドラDICEの攻略法

QooQ