web-dev-qa-db-ja.com

参照渡しよりも値渡しが速い

値渡しと参照渡しという2つのアプローチのパフォーマンスを比較するために、c ++で簡単なプログラムを作成しました。実際には、値渡しは参照渡しよりも優れたパフォーマンスを発揮します。

結論は、値渡しに必要なクロックサイクル(命令)が少なくなることです。

誰かが詳細に説明できると本当にうれしいです理由値渡しに必要なクロックサイクルが少なくなります。

#include <iostream>
#include <stdlib.h>
#include <time.h>

using namespace std;

void function(int *ptr);
void function2(int val);

int main() {

   int nmbr = 5;

   clock_t start, stop;
   start = clock();
   for (long i = 0; i < 1000000000; i++) {
       function(&nmbr);
       //function2(nmbr);
   }
   stop = clock();

   cout << "time: " << stop - start;

   return 0;
}

/**
* pass by reference
*/
void function(int *ptr) {
    *ptr *= 5;
}

/**
* pass by value
*/
void function2(int val) {
   val *= 5;
}
44

違いがある理由を見つける良い方法は、分解をチェックすることです。 Visual Studio 2012を使用してマシンで取得した結果を次に示します。

最適化フラグを使用すると、両方の関数が同じコードを生成します。

_009D1270 57                   Push        edi  
009D1271 FF 15 D4 30 9D 00    call        dword ptr ds:[9D30D4h]  
009D1277 8B F8                mov         edi,eax  
009D1279 FF 15 D4 30 9D 00    call        dword ptr ds:[9D30D4h]  
009D127F 8B 0D 48 30 9D 00    mov         ecx,dword ptr ds:[9D3048h]  
009D1285 2B C7                sub         eax,edi  
009D1287 50                   Push        eax  
009D1288 E8 A3 04 00 00       call        std::operator<<<std::char_traits<char> > (09D1730h)  
009D128D 8B C8                mov         ecx,eax  
009D128F FF 15 2C 30 9D 00    call        dword ptr ds:[9D302Ch]  
009D1295 33 C0                xor         eax,eax  
009D1297 5F                   pop         edi  
009D1298 C3                   ret  
_

これは基本的に次と同等です:

_int main ()
{
    clock_t start, stop ;
    start = clock () ;
    stop = clock () ;
    cout << "time: " << stop - start ;
    return 0 ;
}
_

最適化フラグがなければ、おそらく異なる結果が得られます。

関数(最適化なし):

_00114890 55                   Push        ebp  
00114891 8B EC                mov         ebp,esp  
00114893 81 EC C0 00 00 00    sub         esp,0C0h  
00114899 53                   Push        ebx  
0011489A 56                   Push        esi  
0011489B 57                   Push        edi  
0011489C 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
001148A2 B9 30 00 00 00       mov         ecx,30h  
001148A7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
001148AC F3 AB                rep stos    dword ptr es:[edi]  
001148AE 8B 45 08             mov         eax,dword ptr [ptr]  
001148B1 8B 08                mov         ecx,dword ptr [eax]  
001148B3 6B C9 05             imul        ecx,ecx,5  
001148B6 8B 55 08             mov         edx,dword ptr [ptr]  
001148B9 89 0A                mov         dword ptr [edx],ecx  
001148BB 5F                   pop         edi  
001148BC 5E                   pop         esi  
001148BD 5B                   pop         ebx  
001148BE 8B E5                mov         esp,ebp  
001148C0 5D                   pop         ebp  
001148C1 C3                   ret 
_

function2(最適化なし)

_00FF4850 55                   Push        ebp  
00FF4851 8B EC                mov         ebp,esp  
00FF4853 81 EC C0 00 00 00    sub         esp,0C0h  
00FF4859 53                   Push        ebx  
00FF485A 56                   Push        esi  
00FF485B 57                   Push        edi  
00FF485C 8D BD 40 FF FF FF    lea         edi,[ebp-0C0h]  
00FF4862 B9 30 00 00 00       mov         ecx,30h  
00FF4867 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00FF486C F3 AB                rep stos    dword ptr es:[edi]  
00FF486E 8B 45 08             mov         eax,dword ptr [val]  
00FF4871 6B C0 05             imul        eax,eax,5  
00FF4874 89 45 08             mov         dword ptr [val],eax  
00FF4877 5F                   pop         edi  
00FF4878 5E                   pop         esi  
00FF4879 5B                   pop         ebx  
00FF487A 8B E5                mov         esp,ebp  
00FF487C 5D                   pop         ebp  
00FF487D C3                   ret  
_

最適化を行わない場合)値渡しが高速になるのはなぜですか?

function()には、2つの余分なmov操作があります。最初の余分なmov操作を見てみましょう。

_001148AE 8B 45 08             mov         eax,dword ptr [ptr]  
001148B1 8B 08                mov         ecx,dword ptr [eax]  
001148B3 6B C9 05             imul        ecx,ecx,5
_

ここでは、ポインターを逆参照しています。 function2 ()には既に値があるため、このステップは避けます。まず、ポインターのアドレスをレジスタeaxに移動します。次に、ポインターの値をレジスタecxに移動します。最後に、値に5を掛けます。

2番目の余分なmov操作を見てみましょう。

_001148B3 6B C9 05             imul        ecx,ecx,5  
001148B6 8B 55 08             mov         edx,dword ptr [ptr]  
001148B9 89 0A                mov         dword ptr [edx],ecx 
_

今、私たちは後退しています。値に5を乗算し終えたので、値をメモリアドレスに戻す必要があります。

function2 ()はポインターの参照と逆参照を処理する必要がないため、これら2つの余分なmov操作をスキップできます。

81
jliv902

参照渡しのオーバーヘッド:

  • 各アクセスには逆参照が必要です。つまり、もう1つのメモリ読み取りがあります。

値渡しによるオーバーヘッド:

  • 値はスタックまたはレジスタにコピーする必要があります

整数などの小さなオブジェクトの場合、値渡しはより高速になります。大きなオブジェクト(たとえば、大きな構造体)の場合、コピーによりオーバーヘッドが大きくなりすぎるため、参照渡しが高速になります。

17
green lantern

関数に入って、int値を入力することになっていると想像してください。関数内のコードは、そのint値で何かをしたいです。

値渡しは関数に足を踏み入れるようなもので、誰かがint foo値を要求したときに、それを渡すだけです。

参照渡しは、int foo値のアドレスを使用して関数に進みます。誰かがfooの値を必要とするときはいつでも、それを調べて調べなければなりません。みんなおかしな時間にfooを逆参照する必要があると文句を言うでしょう。私はこの関数に2ミリ秒入っており、fooを1000回検索したはずです!そもそも価値を教えてくれなかったのはなぜですか?なぜ値渡しをしなかったのですか?

この類推により、なぜ値渡しが最速の選択肢であることが多いのかがわかりました。

11
Cosmic Bacon

いくつかの理由:ほとんどの一般的なマシンでは、整数は32ビットで、ポインターは32または64ビットです

そのため、あなたはそれだけの情報を渡す必要があります。

整数を掛けるには:

それを掛けます。

ポインターが指す整数を乗算するには、次の手順を実行する必要があります。

ポインターを延期します。それを掛けます。

それが十分に明確であることを願っています:)


次に、より具体的なものについて説明します。

指摘されているように、値渡し関数は結果に対して何もしませんが、ポインタ渡し関数は実際に結果をメモリに保存します。なぜあなたは貧弱なポインタにそんなに不公平なのですか? :( (冗談だ)

コンパイラにはあらゆる種類の最適化が詰め込まれているため、ベンチマークがどれほど有効であるかを言うのは困難です。 (もちろん、コンパイラの自由度を制御できますが、その情報は提供していません)

そして最後に(そしておそらく最も重要な)、ポインター、値、または参照は、それに関連付けられた速度を持ちません。ご存知のように、ポインターを使用した方が高速で、値を使用するのに苦労するマシン、またはその逆を見つけることができます。わかりました、わかりました、ハードウェアには何らかのパターンがあり、この仮定をすべて行います。最も広く受け入れられているのは次のようです。

単純なオブジェクトを値で渡し、より複雑なオブジェクトを参照(またはポインター)で渡します(しかし、やはり複雑なものは何ですか?単純なものはありますか?ハードウェアが進むにつれて時間とともに変化します)

だから最近、私は標準的な意見が次第になっていると感じています:価値を渡し、コンパイラを信頼してください。そしてそれはクールです。コンパイラーは、長年にわたる専門知識の開発と、常に改善することを要求する怒っているユーザーに支えられています。

11
Kahler

値渡しする場合、値渡しするエンティティのコピーを作成するようコンパイラーに指示します。

参照渡しの場合、参照が指している実際のメモリを使用する必要があることをコンパイラに伝えています。コンパイラーは、最適化の試みでこれを行っているのか、参照された値が他のスレッド(たとえば)で変更されている可能性があるのか​​を知りません。そのメモリ領域を使用する必要があります。

参照渡しは、プロセッサがその特定のメモリブロックにアクセスする必要があることを意味します。これは、レジスタの状態に応じて、最も効率的なプロセスである場合とそうでない場合があります。参照渡しすると、スタック上のメモリを使用できるため、キャッシュ(はるかに高速)のメモリにアクセスする機会が増えます。

最後に、マシンのアーキテクチャと渡すタイプによっては、参照が実際にコピーする値よりも大きくなる場合があります。 32ビット整数のコピーには、64ビットマシンで参照を渡すよりも少ないコピーが含まれます。

したがって、参照渡しは、参照が必要な場合(値を変更するため、または値が他の場所で変更される可能性があるため)、または参照されるオブジェクトのコピーが必要なメモリの逆参照よりも高価な場合にのみ行う必要があります。

その最後のポイントは簡単ではありませんが、大まかなルールはJavaが行うことです:基本型を値で渡し、複合型を(定数)参照で渡します)。

6
Spacemoose

値による受け渡しは、ほとんどのタイプが最新のシステム(64ビット)のポインターよりも小さいため、小さなタイプの場合は非常に高速です。値によって渡されるときに、特定の最適化が行われる場合もあります。

原則として、組み込み型を値で渡します。

4
PureW

この場合、コンパイラーは、乗算の結果が値渡しの場合には使用されていないと認識し、それを完全に最適化しました。逆アセンブルされたコードを見ずに確認することは不可能です。

4
Mark Ransom

プロセッサは関係なく64ビット命令を実行する必要があるため、ネイティブ64ビットプラットフォームでは32ビットメモリ操作命令の実行がかなり頻繁に遅くなります。コンパイラによって正しく実行された場合、32ビット命令は命令キャッシュで「ペア」になりますが、32ビット読み取りが64ビット命令で実行されると、追加の4バイトがいっぱいとしてコピーされ、破棄されます。要するに、値がポインターサイズよりも小さいということは、必ずしも高速であることを意味するわけではありません。状況とコンパイラーに依存し、値がポインターよりも絶対に1倍大きい複合型を除き、または絶対的な最高のパフォーマンスが必要な場合を除いて、パフォーマンスについては絶対に考慮しないでください。移植性を考慮しない特定のプラットフォーム。参照渡しまたは値渡しのどちらを選択するかは、呼び出されたプロシージャが渡されたオブジェクトを変更できるようにするかどうかのみに依存する必要があります。 128ビットより小さい型の読み取りのみで、値渡しの場合は、より安全です。

3