組み込み型、イテレータ、関数オブジェクトは、値渡しの方が効率が良い理由
解決済
回答 5
投稿
- 評価
- クリップ 0
- VIEW 4,793
Effective C++の20項に、組み込み型、イテレータ、関数オブジェクトは、しばしば、const参照渡しよりも値渡しの方が効率的であると書かれています。
なぜ、その様なことがいえるのでしょうか。
-
気になる質問をクリップする
クリップした質問は、後からいつでもマイページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
クリップを取り消します
-
良い質問の評価を上げる
以下のような質問は評価を上げましょう
- 質問内容が明確
- 自分も答えを知りたい
- 質問者以外のユーザにも役立つ
評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。
質問の評価を上げたことを取り消します
-
評価を下げられる数の上限に達しました
評価を下げることができません
- 1日5回まで評価を下げられます
- 1日に1ユーザに対して2回まで評価を下げられます
質問の評価を下げる
teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。
- プログラミングに関係のない質問
- やってほしいことだけを記載した丸投げの質問
- 問題・課題が含まれていない質問
- 意図的に内容が抹消された質問
- 過去に投稿した質問と同じ内容の質問
- 広告と受け取られるような投稿
評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。
質問の評価を下げたことを取り消します
この機能は開放されていません
評価を下げる条件を満たしてません
質問の評価を下げる機能の利用条件
この機能を利用するためには、以下の事項を行う必要があります。
- 質問回答など一定の行動
-
メールアドレスの認証
メールアドレスの認証
-
質問評価に関するヘルプページの閲覧
質問評価に関するヘルプページの閲覧
+3
参照渡しは、内部的にはその値が格納されている領域へのポインタを渡しているので、実際の値を取り出すためにはポインタを通して間接的なメモリアクセスをすることになり、速度低下の原因となり得ます。おそらくそのことを言っているのだと思います。
また、値渡しでは、コンパイルオプションや明示的な呼び出し規約の指定により、値をレジスターで渡すことができるようになるので、メモリアクセスすら発生しない高速な関数呼び出しが可能になります。Visual C++では、x64用ビルド時は、デフォルトでレジスター渡しになっています。
ただし、レジスターの数には限りがあるので、引数の数が規定数を上回る場合はスタックが使われます。
私も実際にどのようなコードになるのか試してみました。
x64/Releaseビルドでコンパイルしましたが、デフォルトではグローバルな最適化が有効になっていて別ファイルにしてもインライン展開してしまうため、そのオプションだけ無効化しています。
int testfunc_reference(const int &a, const int &b)
{
return a + b;
}
int testfunc_value(int a, int b)
{
return a + b;
}
// 呼び出し側
volatile int X;
int main()
{
X = testfunc_reference(123, 456);
//X = testfunc_value(123, 456);
return 0;
}
出力されたアセンブリコード
; testfunc_reference
mov eax, DWORD PTR [rcx]
add eax, DWORD PTR [rdx]
ret 0
; testfunc_value
lea eax, DWORD PTR [rcx+rdx]
ret 0
; 呼び出し側
; X = testfunc_reference(123, 456);
lea rdx, QWORD PTR $T1[rsp]
mov DWORD PTR $T1[rsp], 456 ; 000001c8H
lea rcx, QWORD PTR $T2[rsp]
mov DWORD PTR $T2[rsp], 123 ; 0000007bH
call ?testfunc_reference@@YAHAEBH0@Z ; testfunc_reference
mov DWORD PTR ?X@@3HC, eax ; X
; X = testfunc_value(123, 456);
mov edx, 456 ; 000001c8H
mov ecx, 123 ; 0000007bH
call ?testfunc_value@@YAHHH@Z ; testfunc_value
mov DWORD PTR ?X@@3HC, eax ; X
明らかにコード量が違いますね。参照渡しの方は引数にリテラル値を渡した場合、いったんメモリに格納してからそのアドレスを渡しているので、その分余計に値渡しよりも効率が悪くなっています。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
checkベストアンサー
+2
値渡し(pass-by-value)と参照渡し(pass-by-value)を比較した場合、値渡しには下記のようなメリットがあります。
- メンバへの間接参照を行わないため、メモリアクセスのコストを削減できる。(参照渡しはポインタ渡し相当のアセンブリコードになります)
- 型のサイズが十分小さい場合、関数呼び出し規約によりCPUレジスタが利用されてメモリアクセスを完全に省ける。
- 関数実装側でのメモリアクセス先が局所化されることで、メモリキャッシュ機構を効率的に利用できる。
一方で、値渡しのデメリットは「追加のコピー処理が必要となること」ですが、その型のサイズが十分に小さく(環境によりますがint型3~4個程度以下)、コピーコンストラクタはメンバ変数を単純コピーするだけで、デストラクタは何も行わないような型では、前掲メリットの方が大きくなります。
組み込み型、イテレータ、関数オブジェクト 等は、まさにこのような条件に適合する型と言えます。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
+1
検証用に下記のコードを書いてみました。
#include <chrono>
#include <cstdint>
#include <functional>
#include <iostream>
#define benchmark_m(name, func, times) \
{ \
auto start_time = std::chrono::high_resolution_clock::now(); \
for (int x = 0; x < (times); ++x) { \
(func); \
} \
auto end_time = std::chrono::high_resolution_clock::now(); \
std::cout << (name) << ": " \
<< std::chrono::duration_cast< \
std::chrono::microseconds>( \
end_time - start_time) \
.count() \
<< " usec" << std::endl; \
}
uint64_t benchmark(const std::function<void()> &func, int times)
{
auto start_time = std::chrono::high_resolution_clock::now();
for (int i = 0; i < times; ++i) {
func();
}
auto end_time = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(
end_time - start_time)
.count();
}
int call_by_referenc(const int &i)
{
return i + 1;
}
int call_by_value(int i)
{
return i + 1;
}
int call_by_sharing(const int *i)
{
return *i + 1;
}
int main(int argc, char const *argv[])
{
int i = 0;
int *p = &i;
int times = 10000000;
std::cout << "[lambda] call_by_referenc: "
<< benchmark([i]() { call_by_referenc(i); }, times) << " usec"
<< std::endl;
std::cout << "[lambda] call_by_value: "
<< benchmark([i]() { call_by_value(i); }, times) << " usec"
<< std::endl;
std::cout << "[lambda] call_by_sharing: "
<< benchmark([p]() { call_by_sharing(p); }, times) << " usec"
<< std::endl;
benchmark_m("[micro] call_by_referenc", call_by_referenc(i), times);
benchmark_m("[micro] call_by_value", call_by_value(i), times);
benchmark_m("[micro] call_by_sharing", call_by_sharing(p), times);
return 0;
}
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"で最適化すると、ラムダ式版は似たような結果で、マクロ版は最適化でごっそり処理が省略されて計測できませんでした。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
参照渡しだとどこかで参照外しが必要で、かたや値渡しだとどこかで値のコピーが必要となるはず。
参照外し/コピーに要する(時間/空間的)コストを比較すると、
組み込み型、イテレータ、関数オブジェクトではコピーの方がお得な場合が多い。
...ってことじゃないかと考えますです。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
0
const参照渡し - ゲームが作れるようになるまでがんばる日記
によると、
関数にオブジェクトを渡すときは値渡しよりもconst参照渡しを使うほうが良い。値渡しではオブジェクトのコピーが行われるためコンストラクタやデストラクタが実行されることになる。const参照渡しなら新しいオブジェクトの生成が行われないため効率が良い。
とあり、これに対して、
値渡しでも効率が悪く無いものもある。組み込み型やSTLの反復子・関数オブジェクト。これらは普通、値渡しで良い。
と言うことのようですよ。
投稿
-
回答の評価を上げる
以下のような回答は評価を上げましょう
- 正しい回答
- わかりやすい回答
- ためになる回答
評価が高い回答ほどページの上位に表示されます。
-
回答の評価を下げる
下記のような回答は推奨されていません。
- 間違っている回答
- 質問の回答になっていない投稿
- スパムや攻撃的な表現を用いた投稿
評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。
15分調べてもわからないことは、teratailで質問しよう!
- ただいまの回答率 88.33%
- 質問をまとめることで、思考を整理して素早く解決
- テンプレート機能で、簡単に質問をまとめられる
2016/04/19 21:34
>>Visual C++では、x64用ビルド時は、デフォルトでレジスター渡しになっています
これは、設定か何かで変えることができるのでしょうか。
2016/04/19 22:30
詳しくはMSDNの呼び出し規約の説明を参照してください。
→ https://msdn.microsoft.com/ja-jp/library/46t77ak2.aspx