値渡しと参照渡しという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;
}
違いがある理由を見つける良い方法は、分解をチェックすることです。 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
操作をスキップできます。
参照渡しのオーバーヘッド:
値渡しによるオーバーヘッド:
整数などの小さなオブジェクトの場合、値渡しはより高速になります。大きなオブジェクト(たとえば、大きな構造体)の場合、コピーによりオーバーヘッドが大きくなりすぎるため、参照渡しが高速になります。
関数に入って、int値を入力することになっていると想像してください。関数内のコードは、そのint値で何かをしたいです。
値渡しは関数に足を踏み入れるようなもので、誰かがint foo値を要求したときに、それを渡すだけです。
参照渡しは、int foo値のアドレスを使用して関数に進みます。誰かがfooの値を必要とするときはいつでも、それを調べて調べなければなりません。みんなおかしな時間にfooを逆参照する必要があると文句を言うでしょう。私はこの関数に2ミリ秒入っており、fooを1000回検索したはずです!そもそも価値を教えてくれなかったのはなぜですか?なぜ値渡しをしなかったのですか?
この類推により、なぜ値渡しが最速の選択肢であることが多いのかがわかりました。
いくつかの理由:ほとんどの一般的なマシンでは、整数は32ビットで、ポインターは32または64ビットです
そのため、あなたはそれだけの情報を渡す必要があります。
整数を掛けるには:
それを掛けます。
ポインターが指す整数を乗算するには、次の手順を実行する必要があります。
ポインターを延期します。それを掛けます。
それが十分に明確であることを願っています:)
次に、より具体的なものについて説明します。
指摘されているように、値渡し関数は結果に対して何もしませんが、ポインタ渡し関数は実際に結果をメモリに保存します。なぜあなたは貧弱なポインタにそんなに不公平なのですか? :( (冗談だ)
コンパイラにはあらゆる種類の最適化が詰め込まれているため、ベンチマークがどれほど有効であるかを言うのは困難です。 (もちろん、コンパイラの自由度を制御できますが、その情報は提供していません)
そして最後に(そしておそらく最も重要な)、ポインター、値、または参照は、それに関連付けられた速度を持ちません。ご存知のように、ポインターを使用した方が高速で、値を使用するのに苦労するマシン、またはその逆を見つけることができます。わかりました、わかりました、ハードウェアには何らかのパターンがあり、この仮定をすべて行います。最も広く受け入れられているのは次のようです。
単純なオブジェクトを値で渡し、より複雑なオブジェクトを参照(またはポインター)で渡します(しかし、やはり複雑なものは何ですか?単純なものはありますか?ハードウェアが進むにつれて時間とともに変化します)
だから最近、私は標準的な意見が次第になっていると感じています:価値を渡し、コンパイラを信頼してください。そしてそれはクールです。コンパイラーは、長年にわたる専門知識の開発と、常に改善することを要求する怒っているユーザーに支えられています。
値渡しする場合、値渡しするエンティティのコピーを作成するようコンパイラーに指示します。
参照渡しの場合、参照が指している実際のメモリを使用する必要があることをコンパイラに伝えています。コンパイラーは、最適化の試みでこれを行っているのか、参照された値が他のスレッド(たとえば)で変更されている可能性があるのかを知りません。そのメモリ領域を使用する必要があります。
参照渡しは、プロセッサがその特定のメモリブロックにアクセスする必要があることを意味します。これは、レジスタの状態に応じて、最も効率的なプロセスである場合とそうでない場合があります。参照渡しすると、スタック上のメモリを使用できるため、キャッシュ(はるかに高速)のメモリにアクセスする機会が増えます。
最後に、マシンのアーキテクチャと渡すタイプによっては、参照が実際にコピーする値よりも大きくなる場合があります。 32ビット整数のコピーには、64ビットマシンで参照を渡すよりも少ないコピーが含まれます。
したがって、参照渡しは、参照が必要な場合(値を変更するため、または値が他の場所で変更される可能性があるため)、または参照されるオブジェクトのコピーが必要なメモリの逆参照よりも高価な場合にのみ行う必要があります。
その最後のポイントは簡単ではありませんが、大まかなルールはJavaが行うことです:基本型を値で渡し、複合型を(定数)参照で渡します)。
値による受け渡しは、ほとんどのタイプが最新のシステム(64ビット)のポインターよりも小さいため、小さなタイプの場合は非常に高速です。値によって渡されるときに、特定の最適化が行われる場合もあります。
原則として、組み込み型を値で渡します。
この場合、コンパイラーは、乗算の結果が値渡しの場合には使用されていないと認識し、それを完全に最適化しました。逆アセンブルされたコードを見ずに確認することは不可能です。
プロセッサは関係なく64ビット命令を実行する必要があるため、ネイティブ64ビットプラットフォームでは32ビットメモリ操作命令の実行がかなり頻繁に遅くなります。コンパイラによって正しく実行された場合、32ビット命令は命令キャッシュで「ペア」になりますが、32ビット読み取りが64ビット命令で実行されると、追加の4バイトがいっぱいとしてコピーされ、破棄されます。要するに、値がポインターサイズよりも小さいということは、必ずしも高速であることを意味するわけではありません。状況とコンパイラーに依存し、値がポインターよりも絶対に1倍大きい複合型を除き、または絶対的な最高のパフォーマンスが必要な場合を除いて、パフォーマンスについては絶対に考慮しないでください。移植性を考慮しない特定のプラットフォーム。参照渡しまたは値渡しのどちらを選択するかは、呼び出されたプロシージャが渡されたオブジェクトを変更できるようにするかどうかのみに依存する必要があります。 128ビットより小さい型の読み取りのみで、値渡しの場合は、より安全です。