関数呼び出しによるオーバーヘッドの影響 その2

前置き

 引数の値渡しと参照渡しによる違いを考えてみよう!のコーナーです。
 2つの厳密な違いは他に譲りますが、関数に引数を渡す際にはその関数で使いたい値を直接渡す方法とポインタなどを使って渡す方法が考えられます。 C++では参照で渡す便利なものもありますが。 直接値を渡した際は、呼び出された関数内ではコピーされた値が使用されます。 それに対して参照渡しされたコピーではなく、呼び出した元の場所にある値が使用されます。 従って値渡しされた場合、関数内でいくらその値を変更されようと元の場所ではその影響を受けません。 対して参照渡しされた場合は、関数内で値が変更されると呼び出し元の値も変更されます。 …というような違いがあります。
 だから変更されてもOKなら値渡し、ダメなら参照渡しとか言う使い分けが出来ます。 注意は定数は参照渡し出来ないってこと。 例えばfunction(&5)と言うような渡し方は出来ません。 え、なぜってこの5は定数であって変数の場所を表すアドレスがないからじゃん?(確信無し)
 ここでは2つの渡し方でどういう違いが出るんでしょう、を確認するコーナーです。

比較内容

 値渡しの場合、関数が実行される際に変数をコピーする時間がかかります。 従って引数のサイズが大きければ大きいほど影響が出てきます。 特に大きな構造体を扱う場合は考えなければいけません。
 で、実際に演算速度を比較してみます。 一定回数にどれくらいの時間がかかるか、を見ても良いのです。 が、ここでは一定時間内に何回の演算が行えるかで比較を行います。
 main関数で直に演算させる、関数越しで演算させる、その関数をインライン関数指定してみるの3通りでの比較を行います。 比較時間はプロセッサ時間にして1000msec、n数を400とし平均値を見ていきたいと思います。

環境など

ソースコード

#include <windows.h>
#include <stdio.h>
#include <time.h>
 
#define REPEAT 400
#define TIME   1000
#define NUM    500
 
struct sample{
    double a[NUM];
};
 
struct sample value(struct sample x);
struct sample reference(struct sample *x);
 
int main(void){
    FILE *fp = ::fopen("record.csv","w");
    clock_t start;
    struct sample x;
    unsigned long sum;
    double sum2;
 
    fprintf(fp,"value,,reference,,\n");
 
    printf("%d,%d\n",sizeof(x),sizeof(&x));
    for(int i=0; i<REPEAT; i++){
        sum = 0;
        ZeroMemory(&x,sizeof(x));
        start = clock();
        while(clock()-start < TIME){
            x=value(x);
            sum += 1;
        }
        sum2 = 0;
        for(int i=0;i<NUM; i++) sum2 += x.a[i];
        fprintf(fp,"%d,%e,",sum,sum2);
 
        sum = 0;
        ZeroMemory(&x,sizeof(x));
        start = clock();
        while(clock()-start < TIME){
            x=reference(&x);
            sum+=1;
        }
        sum2 = 0;
        for(int i=0;i<NUM; i++) sum2 += x.a[i];
        fprintf(fp,"%d,%e,\n",sum,sum2);
    }
 
    fclose(fp);
 
    return 0;
}
 
struct sample value(struct sample x){
    for(int i=0; i<NUM; i++){
        x.a[i] += 1.0;
    }
    return x;
}
 
struct sample reference(struct sample *x){
    for(int i=0; i<NUM; i++){
        x->a[i] += 1.0;
    }
    return *x;
}
 構造体のサイズは4000バイトです。対してアドレスのサイズは4バイトと1000倍違います。 …さすがにこれだけ違えば計算に違いが出るでしょうという違いを出してみました。
 なぜ今回は戻り値があるかと言いますと、Release版でコンパイルすると値渡しの関数が無視されているようだったからです。 …戻り値がなければ関数内で完全に自己完結する全く意味を成さない関数ですから。 全くコンパイラの最適化って凄いですね。 計算させた値の全てを足し合わせてそれをファイルに出力するようにしているのもやはり最適化対策です。
 またwindows.hをインクルードしているのは構造体の初期化としてZeroMemoryを呼び出すためです

結果・考察?

 結果を以下に載せます。 縦軸が計算回数の平均値、ラベルのvalueが値渡しの計算結果でreferenceが参照渡しの計算結果を示しています。 計算回数を縦軸に取っているので、大きければ大きいほど良い結果であったと言うことです。
 値渡しの計算結果と参照渡しの計算結果の違いについてみてみると、Releaseでビルドしたものが参照渡しの方が2.2倍大きい結果でした。 が、なぜかDebugでビルドしたものは値渡しの方が2割ほど大きいという結果でした。 …はてさて、なぜだかさっぱり分からん(汗)計算結果のばらつきについての考察はまた後ほどにします。
 最適化の程はReleaseとDebugの差を比べれば一目瞭然。 値渡しでは2.2倍ほど、参照渡しでは5.6倍ほどRelease版の方が良い結果が出ています。 前回の簡単な関数(引数無し)を呼び出した際のDebug版とRelease版の違いは2.1倍ほどでした。 そのことを踏まえるとと参照渡しの差が非常に大きいと言うことが分かります。
総合結果一覧
 Release版とDebug版のそれぞれの計算結果を次に示します。 縦軸が計算回数、横軸が繰り返し回数を表します。
 今回は前回の反省を踏まえて負荷をかけるような常駐プログラム(ウィルス対策ソフト等)を切って計測しました。 それを反映して、全体的に安定した値が取れていたことが確認出来ます。
 やはりDebug版の方ですが、値渡しの結果がより多く計算出来たことを示しています。 ごめんなさい、分かりません。誰か分かる人がいたらもし良ければご教示お願いします。
Release版結果 Debug版結果
 結論としましては、Release版でビルドする際は参照渡しの方が早いと言う結果でした。 そしてなぜがDebug版では値渡しの方が早いと言う結果でした。
 一つの推察としてはDebug版の参照渡しが何か面倒なことをしているのかもしれません。 と言うのも参照渡しのDebug版とRelease版が5倍以上違うからです。
This site is created by ez-HTML