質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

Q&A

解決済

5回答

7471閲覧

組み込み型、イテレータ、関数オブジェクトは、値渡しの方が効率が良い理由

JADEN

総合スコア106

C++

C++はC言語をもとにしてつくられた最もよく使われるマルチパラダイムプログラミング言語の1つです。オブジェクト指向、ジェネリック、命令型など広く対応しており、多目的に使用されています。

0グッド

0クリップ

投稿2016/04/17 12:22

Effective C++の20項に、組み込み型、イテレータ、関数オブジェクトは、しばしば、const参照渡しよりも値渡しの方が効率的であると書かれています。

なぜ、その様なことがいえるのでしょうか。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答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
catsforepaw

総合スコア5938

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

JADEN

2016/04/19 12:34

回答ありがとうございます。 >>Visual C++では、x64用ビルド時は、デフォルトでレジスター渡しになっています これは、設定か何かで変えることができるのでしょうか。
catsforepaw

2016/04/19 13:30

VC++では呼び出し規約は __cdecl/__stdcall/__fastcall/__vectorcall のいずれかをコンパイルオプションまたは関数プロトタイプで指定きるのですが、x64では、そのうちの__cdeclと__vectorcallのみ有効のようです。どちらも引数渡しにレジスターが使われるため、x64ではデフォルトというよりは常にレジスター渡しということになりますね。 詳しくはMSDNの呼び出し規約の説明を参照してください。 → https://msdn.microsoft.com/ja-jp/library/46t77ak2.aspx
guest

0

ベストアンサー

値渡し(pass-by-value)と参照渡し(pass-by-value)を比較した場合、値渡しには下記のようなメリットがあります。

  • メンバへの間接参照を行わないため、メモリアクセスのコストを削減できる。(参照渡しはポインタ渡し相当のアセンブリコードになります)
  • 型のサイズが十分小さい場合、関数呼び出し規約によりCPUレジスタが利用されてメモリアクセスを完全に省ける。
  • 関数実装側でのメモリアクセス先が局所化されることで、メモリキャッシュ機構を効率的に利用できる。

一方で、値渡しのデメリットは「追加のコピー処理が必要となること」ですが、その型のサイズが十分に小さく(環境によりますがint型3~4個程度以下)、コピーコンストラクタはメンバ変数を単純コピーするだけで、デストラクタは何も行わないような型では、前掲メリットの方が大きくなります。

組み込み型、イテレータ、関数オブジェクト 等は、まさにこのような条件に適合する型と言えます。

投稿2016/04/18 10:11

yohhoy

総合スコア6191

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

JADEN

2016/04/19 12:17

回答ありがとうございます。 3番目の「関数実装側でのメモリアクセス先が局所化される」の詳細を教えてもらいたいです。 2番目で、メモリアクセスを省けるとありますが、それは3番目と矛盾しないのかなと。
episteme

2016/04/19 20:27

「空間局所化」のおハナシかな。 値渡しすると引数の多くはスタックのアタマ付近に並んでます(一般に)。 なので必要な情報(=引数)が比較的狭いメモリ領域に局所化されてます。 キャッシュにはそこそこのサイズの領域をまとめて読み込まれますから、 引数がキャッシュから読みだされる頻度が上がります。
yohhoy

2016/04/20 13:54

3番目は episteme さんに補足説明いただいた通りです。Thanks! 2番目は3番目と排他する表現になってしまいましたが、「特定条件を満たせば2、それ以外でも3の恩恵がある」と解釈しておいてください。
JADEN

2016/04/26 11:22

説明ありがとうございます。
guest

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

raccy

総合スコア21735

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

JADEN

2016/04/19 12:28 編集

回答ありがとうございます。 call_by_sharingという用語は、初めて聞いたので参考になりました。
guest

0

const参照渡し - ゲームが作れるようになるまでがんばる日記

によると、

関数にオブジェクトを渡すときは値渡しよりもconst参照渡しを使うほうが良い。値渡しではオブジェクトのコピーが行われるためコンストラクタやデストラクタが実行されることになる。const参照渡しなら新しいオブジェクトの生成が行われないため効率が良い。

とあり、これに対して、

値渡しでも効率が悪く無いものもある。組み込み型やSTLの反復子・関数オブジェクト。これらは普通、値渡しで良い。

と言うことのようですよ。

投稿2016/04/18 05:42

chun

総合スコア324

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

JADEN

2016/04/19 12:18

回答ありがとうございます。
guest

0

参照渡しだとどこかで参照外しが必要で、かたや値渡しだとどこかで値のコピーが必要となるはず。
参照外し/コピーに要する(時間/空間的)コストを比較すると、
組み込み型、イテレータ、関数オブジェクトではコピーの方がお得な場合が多い。

...ってことじゃないかと考えますです。

投稿2016/04/18 03:10

episteme

総合スコア16614

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

JADEN

2016/04/19 12:19

回答ありがとうございます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問