Effective C++の20項に、組み込み型、イテレータ、関数オブジェクトは、しばしば、const参照渡しよりも値渡しの方が効率的であると書かれています。
なぜ、その様なことがいえるのでしょうか。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答5件
0
参照渡しは、内部的にはその値が格納されている領域へのポインタを渡しているので、実際の値を取り出すためにはポインタを通して間接的なメモリアクセスをすることになり、速度低下の原因となり得ます。おそらくそのことを言っているのだと思います。
また、値渡しでは、コンパイルオプションや明示的な呼び出し規約の指定により、値をレジスターで渡すことができるようになるので、メモリアクセスすら発生しない高速な関数呼び出しが可能になります。Visual C++では、x64用ビルド時は、デフォルトでレジスター渡しになっています。
ただし、レジスターの数には限りがあるので、引数の数が規定数を上回る場合はスタックが使われます。
私も実際にどのようなコードになるのか試してみました。
x64/Releaseビルドでコンパイルしましたが、デフォルトではグローバルな最適化が有効になっていて別ファイルにしてもインライン展開してしまうため、そのオプションだけ無効化しています。
C++
1int testfunc_reference(const int &a, const int &b) 2{ 3 return a + b; 4} 5 6int testfunc_value(int a, int b) 7{ 8 return a + b; 9}
C++
1// 呼び出し側 2volatile int X; 3int main() 4{ 5 X = testfunc_reference(123, 456); 6 //X = testfunc_value(123, 456); 7 return 0; 8}
出力されたアセンブリコード
asm
1; testfunc_reference 2 3 mov eax, DWORD PTR [rcx] 4 add eax, DWORD PTR [rdx] 5 ret 0 6 7; testfunc_value 8 9 lea eax, DWORD PTR [rcx+rdx] 10 ret 0
asm
1; 呼び出し側 2 3; X = testfunc_reference(123, 456); 4 5 lea rdx, QWORD PTR $T1[rsp] 6 mov DWORD PTR $T1[rsp], 456 ; 000001c8H 7 lea rcx, QWORD PTR $T2[rsp] 8 mov DWORD PTR $T2[rsp], 123 ; 0000007bH 9 call ?testfunc_reference@@YAHAEBH0@Z ; testfunc_reference 10 mov DWORD PTR ?X@@3HC, eax ; X 11 12; X = testfunc_value(123, 456); 13 14 mov edx, 456 ; 000001c8H 15 mov ecx, 123 ; 0000007bH 16 call ?testfunc_value@@YAHHH@Z ; testfunc_value 17 mov DWORD PTR ?X@@3HC, eax ; X
明らかにコード量が違いますね。参照渡しの方は引数にリテラル値を渡した場合、いったんメモリに格納してからそのアドレスを渡しているので、その分余計に値渡しよりも効率が悪くなっています。
投稿2016/04/17 13:18
編集2016/04/17 15:40総合スコア5938
0
ベストアンサー
値渡し(pass-by-value)と参照渡し(pass-by-value)を比較した場合、値渡しには下記のようなメリットがあります。
- メンバへの間接参照を行わないため、メモリアクセスのコストを削減できる。(参照渡しはポインタ渡し相当のアセンブリコードになります)
- 型のサイズが十分小さい場合、関数呼び出し規約によりCPUレジスタが利用されてメモリアクセスを完全に省ける。
- 関数実装側でのメモリアクセス先が局所化されることで、メモリキャッシュ機構を効率的に利用できる。
一方で、値渡しのデメリットは「追加のコピー処理が必要となること」ですが、その型のサイズが十分に小さく(環境によりますがint型3~4個程度以下)、コピーコンストラクタはメンバ変数を単純コピーするだけで、デストラクタは何も行わないような型では、前掲メリットの方が大きくなります。
組み込み型、イテレータ、関数オブジェクト 等は、まさにこのような条件に適合する型と言えます。
投稿2016/04/18 10:11
総合スコア6189
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/04/19 20:27
2016/04/20 13:54
2016/04/26 11:22
0
検証用に下記のコードを書いてみました。
C++
1#include <chrono> 2#include <cstdint> 3#include <functional> 4#include <iostream> 5 6#define benchmark_m(name, func, times) \ 7 { \ 8 auto start_time = std::chrono::high_resolution_clock::now(); \ 9 for (int x = 0; x < (times); ++x) { \ 10 (func); \ 11 } \ 12 auto end_time = std::chrono::high_resolution_clock::now(); \ 13 std::cout << (name) << ": " \ 14 << std::chrono::duration_cast< \ 15 std::chrono::microseconds>( \ 16 end_time - start_time) \ 17 .count() \ 18 << " usec" << std::endl; \ 19 } 20 21uint64_t benchmark(const std::function<void()> &func, int times) 22{ 23 auto start_time = std::chrono::high_resolution_clock::now(); 24 for (int i = 0; i < times; ++i) { 25 func(); 26 } 27 auto end_time = std::chrono::high_resolution_clock::now(); 28 return std::chrono::duration_cast<std::chrono::microseconds>( 29 end_time - start_time) 30 .count(); 31} 32 33int call_by_referenc(const int &i) 34{ 35 return i + 1; 36} 37 38int call_by_value(int i) 39{ 40 return i + 1; 41} 42 43int call_by_sharing(const int *i) 44{ 45 return *i + 1; 46} 47 48int main(int argc, char const *argv[]) 49{ 50 int i = 0; 51 int *p = &i; 52 int times = 10000000; 53 std::cout << "[lambda] call_by_referenc: " 54 << benchmark([i]() { call_by_referenc(i); }, times) << " usec" 55 << std::endl; 56 std::cout << "[lambda] call_by_value: " 57 << benchmark([i]() { call_by_value(i); }, times) << " usec" 58 << std::endl; 59 std::cout << "[lambda] call_by_sharing: " 60 << benchmark([p]() { call_by_sharing(p); }, times) << " usec" 61 << std::endl; 62 benchmark_m("[micro] call_by_referenc", call_by_referenc(i), times); 63 benchmark_m("[micro] call_by_value", call_by_value(i), times); 64 benchmark_m("[micro] call_by_sharing", call_by_sharing(p), times); 65 return 0; 66}
Mac OS XでHomebrew gcc 5.3.0版を使いg++-5 -Wall -std=c++14 -O0 -o watashi watashi.cpp
でコンパイルした場合の結果は下記のようになりました。
$ ./watashi [lambda] call_by_referenc: 235433 usec [lambda] call_by_value: 230840 usec [lambda] call_by_sharing: 232594 usec [micro] call_by_referenc: 26055 usec [micro] call_by_value: 28254 usec [micro] call_by_sharing: 26421 usec
はっきりって誤差です。何回か繰り返すと順番が入れ替わります。
ただ、gobjdumpでみると
000000010000087b <__Z16call_by_referencRKi>: 10000087b: 55 push %rbp 10000087c: 48 89 e5 mov %rsp,%rbp 10000087f: 48 89 7d f8 mov %rdi,-0x8(%rbp) 100000883: 48 8b 45 f8 mov -0x8(%rbp),%rax 100000887: 8b 00 mov (%rax),%eax 100000889: 83 c0 01 add $0x1,%eax 10000088c: 5d pop %rbp 10000088d: c3 retq 000000010000088e <__Z13call_by_valuei>: 10000088e: 55 push %rbp 10000088f: 48 89 e5 mov %rsp,%rbp 100000892: 89 7d fc mov %edi,-0x4(%rbp) 100000895: 8b 45 fc mov -0x4(%rbp),%eax 100000898: 83 c0 01 add $0x1,%eax 10000089b: 5d pop %rbp 10000089c: c3 retq 000000010000089d <__Z15call_by_sharingPKi>: 10000089d: 55 push %rbp 10000089e: 48 89 e5 mov %rsp,%rbp 1000008a1: 48 89 7d f8 mov %rdi,-0x8(%rbp) 1000008a5: 48 8b 45 f8 mov -0x8(%rbp),%rax 1000008a9: 8b 00 mov (%rax),%eax 1000008ab: 83 c0 01 add $0x1,%eax 1000008ae: 5d pop %rbp 1000008af: c3 retq
となっており、call_by_valueの方が"mov"命令が一個だけ少ないので、その分速いかも知れません。"-O0"で最適化無しにしていますが、"-O3"で最適化すると、ラムダ式版は似たような結果で、マクロ版は最適化でごっそり処理が省略されて計測できませんでした。
投稿2016/04/17 13:38
総合スコア21733
0
const参照渡し - ゲームが作れるようになるまでがんばる日記
によると、
関数にオブジェクトを渡すときは値渡しよりもconst参照渡しを使うほうが良い。値渡しではオブジェクトのコピーが行われるためコンストラクタやデストラクタが実行されることになる。const参照渡しなら新しいオブジェクトの生成が行われないため効率が良い。
とあり、これに対して、
値渡しでも効率が悪く無いものもある。組み込み型やSTLの反復子・関数オブジェクト。これらは普通、値渡しで良い。
と言うことのようですよ。
投稿2016/04/18 05:42
総合スコア324
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/04/19 12:34
2016/04/19 13:30