13

私は_rdtsc()時間atoi()atof()計っていましたが、かなり時間がかかっていることに気付きました。したがって、最初の呼び出しからはるかに高速なこれらの関数の独自のバージョンを作成しました。

Windows 7、VS2012 IDE を使用していますが、Intel C/C++ コンパイラ v13 を使用しています。-/O3 を有効にし、-/Ot (「優先する高速コード」) も有効にしています。私の CPU は Ivy Bridge (モバイル) です。

さらに調査したところ、atoi()atof()が呼び出された回数が多いほど、実行が速くなったようです?? 私はマグニチュードをより速く話しています:

ループの外側から呼び出すとatoi()、1 回だけ 5,892 CPU サイクルかかりますが、数千回の反復の後、これは 300 ~ 600 CPU サイクルに減少しました (実行時間の範囲が非常に大きくなります)。

atof()最初は 20,000 ~ 30,000 CPU サイクルかかり、数千回の反復の後、18 ~ 28 CPU サイクルかかりました (これは、カスタム関数が最初に呼び出される速度です)。

誰かがこの効果を説明してもらえますか?

編集:言い忘れました-私のプログラムの基本的なセットアップは、ファイルからバイトを解析するループでした。ループ内では、明らかに atof と atoi を使用して上記に気づきます。しかし、ループの前に調べたところ、atoi と atof を 2 回呼び出し、ユーザーが作成した同等の関数を 2 回呼び出しただけで、ループの実行が速くなったように見えました。ループは 150,000 行のデータを処理し、各行には 3xatof()またはatoi()s が必要でした。繰り返しになりますが、メイン ループの前にこれらの関数を呼び出すと、これらの関数を 500,000 回呼び出すプログラムの速度に影響を与える理由がわかりません。

#include <ia32intrin.h>

int main(){
    
    //call myatoi() and time it
    //call atoi() and time it
    //call myatoi() and time it
    //call atoi() and time it

    char* bytes2 = "45632";
    _int64 start2 = _rdtsc();
    unsigned int a2 = atoi(bytes2);
    _int64 finish2 = _rdtsc();
    cout << (finish2 - start2) << " CPU cycles for atoi()" << endl;
    
    //call myatof() and time it
    //call atof() and time it
    //call myatof() and time it
    //call atof() and time it
    
    
    //Iterate through 150,000 lines, each line about 25 characters.
    //The below executes slower if the above debugging is NOT done.
    while(i < file_size){
        //Loop through my data, call atoi() or atof() 1 or 2 times per line
        switch(bytes[i]){
            case ' ':
                //I have an array of shorts which records the distance from the beginning
                //of the line to each of the tokens in the line. In the below switch
                //statement offset_to_price and offset_to_qty refer to this array.

            case '\n':
                
                switch(message_type){  
                    case 'A':
                        char* temp = bytes + offset_to_price;
                        _int64 start = _rdtsc();
                        price = atof(temp);
                        _int64 finish = _rdtsc();
                        cout << (finish - start) << " CPU cycles" << endl;
                        //Other processing with the tokens
                        break;

                    case 'R':
                        //Get the 4th line token using atoi() as above
                        char* temp = bytes + offset_to_qty;
                        _int64 start = _rdtsc();
                        price = atoi(temp);
                        _int64 finish = _rdtsc();
                        cout << (finish - start) << " CPU cycles" << endl;
                        //Other processing with the tokens
                        break;
                }
            break;
        }
    }
}

ファイル内の行は次のようになります (間に空白行はありません)。

34605792 R dacb 100

34605794 A racb S 44.17 100

34605797 R kacb 100

34605799 Sacb S 44.18 100

34605800 R nacb 100

34605800 A tacb B 44.16 100

34605801 R gacb 100

atoi()「R」メッセージの 4 番目の要素と「A」メッセージの 5 番目の要素で使用しatof()、「A」メッセージの 4 番目の要素で使用しています。

4

4 に答える 4

7

atoiandの劇的な改善が見られるのにatof、独自のより単純な関数では見られない理由は、前者にはすべてのエッジケースを処理するために多数のブランチがあるためだと思います。最初の数回は、これにより多数の不適切な分岐予測が発生し、コストがかかります。しかし、数回後、予測はより正確になります。正しく予測されたブランチはほとんど無料です。これにより、最初からブランチが含まれていない単純なバージョンと競合するようになります。

キャッシュも確かに重要ですが、それはあなた自身の関数が最初から高速であり、繰り返し実行した後に関連する改善が見られなかった理由を説明しているとは思いません(私があなたを正しく理解していれば)。

于 2013-10-12T14:24:24.817 に答える
4

プロファイリングに RDTSC を使用するのは危険です。Intel プロセッサのマニュアルから:

RDTSC 命令はシリアライズ命令ではありません。カウンターを読み取る前に、前のすべての命令が実行されるまで待機する必要はありません。同様に、後続の命令は、読み取り操作が実行される前に実行を開始する場合があります。前のすべての命令がローカルで完了した後にのみ RDTSC を実行する必要がある場合、ソフトウェアは RDTSCP (プロセッサがその命令をサポートしている場合) を使用するか、シーケンス LFENCE;RDTSC を実行できます。

必然的に発生するハイゼンベルグ効果により、RDTSCP または LFENCE のコストを測定します。代わりにループを測定することを検討してください。

于 2013-10-12T14:25:04.733 に答える
3

このような単一の呼び出しのパフォーマンスを測定することはお勧めできません。電力スロットル、割り込み、その他の OS/システムの干渉、測定オーバ​​ーヘッド、および上記のように、コールド/ウォームの分散が原因で、分散が多すぎます。その上、CPU が独自の周波数を調整する可能性があるため、rdtsc は信頼できる測定値とは見なされなくなりましたが、この単純なチェックのためには、それで十分であると言えます。

コードを少なくとも数千回実行し、最初の部分を破棄してから分割して平均を取得する必要があります。これにより、(上記のコメントで述べたように) クローズ キャッシュを含む "ウォーム" パフォーマンスが得られます。コードとデータ (および TLB) の両方のヒット レイテンシー、優れた分岐予測、および一部の外部影響 (最近になって CPU をパワーダウン状態から復帰させたなど) を無効にする可能性もあります。

もちろん、実際のシナリオでは常に L1 キャッシュにヒットするとは限らないため、このパフォーマンスは楽観的すぎると主張するかもしれません. - 2 つの異なる方法 (ライブラリの ato* 関数との競合など) を比較する場合は問題ないかもしれません。 、実際の結果を当てにしないでください。また、テストを少し難しくして、キャッシュへの負荷を少し良くするより精巧な入力パターンで関数を呼び出すこともできます。

20k-30k サイクルに関する質問については、最初の数回の反復を破棄する必要があるのはまさにそのためです。これは単なるキャッシュ ミス レイテンシではなく、実際には、最初の命令がコード フェッチを実行するのを待っています。これは、コード ページ変換がページ ウォーク (複数のメモリ アクセスを伴う可能性がある長いプロセス) を実行するのも待機している可能性があります。 、そして本当に運が悪い場合は、ディスクからページをスワップすることもできます。これには、OS の支援と多くの IO レイテンシが必要です。そして、これはまだ最初の命令の実行を開始する前です。

于 2013-10-12T13:59:52.983 に答える