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

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

ただいまの
回答率

90.47%

  • C++

    3625questions

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

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

解決済

回答 5

投稿

  • 評価
  • クリップ 0
  • VIEW 2,836

JADEN

score 97

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

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

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

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 5

+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


明らかにコード量が違いますね。参照渡しの方は引数にリテラル値を渡した場合、いったんメモリに格納してからそのアドレスを渡しているので、その分余計に値渡しよりも効率が悪くなっています。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/19 21:34

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

    キャンセル

  • 2016/04/19 22:30

    VC++では呼び出し規約は __cdecl/__stdcall/__fastcall/__vectorcall のいずれかをコンパイルオプションまたは関数プロトタイプで指定きるのですが、x64では、そのうちの__cdeclと__vectorcallのみ有効のようです。どちらも引数渡しにレジスターが使われるため、x64ではデフォルトというよりは常にレジスター渡しということになりますね。

    詳しくはMSDNの呼び出し規約の説明を参照してください。
    → https://msdn.microsoft.com/ja-jp/library/46t77ak2.aspx

    キャンセル

checkベストアンサー

+2

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

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

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/19 21:17

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

    キャンセル

  • 2016/04/20 05:27

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

    キャンセル

  • 2016/04/20 22:54

    3番目は episteme さんに補足説明いただいた通りです。Thanks!

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

    キャンセル

  • 2016/04/26 20:22

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

    キャンセル

+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"で最適化すると、ラムダ式版は似たような結果で、マクロ版は最適化でごっそり処理が省略されて計測できませんでした。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/19 21:27 編集

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

    キャンセル

0

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/19 21:19

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

    キャンセル

0

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

によると、

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

とあり、これに対して、

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

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

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2016/04/19 21:18

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

    キャンセル

関連した質問

同じタグがついた質問を見る

  • C++

    3625questions

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