*呼び出し* =(または* =呼び出し*)は、個別の関数を書くよりも遅いですか(数学ライブラリ用)?[閉まっている]


15

算術関数が次のように見えるいくつかのベクトルクラスがあります。

template<typename T, typename U>
auto operator*(const Vector3<T>& lhs, const Vector3<U>& rhs)
{
    return Vector3<decltype(lhs.x*rhs.x)>(
        lhs.x + rhs.x,
        lhs.y + rhs.y,
        lhs.z + rhs.z
        );
}

template<typename T, typename U>
Vector3<T>& operator*=(Vector3<T>& lhs, const Vector3<U>& rhs)
{
    lhs.x *= rhs.x;
    lhs.y *= rhs.y;
    lhs.z *= rhs.z;

    return lhs;
}

重複したコードを削除するために、少しクリーンアップしたいです。基本的に、すべてのoperator*関数を変換して、次のoperator*=ような関数を呼び出したいと思います。

template<typename T, typename U>
auto operator*(const Vector3<T>& lhs, const Vector3<U>& rhs)
{
    Vector3<decltype(lhs.x*rhs.x)> result = lhs;
    result *= rhs;
    return result;
}

しかし、追加の関数呼び出しから追加のオーバーヘッドが発生するかどうかは心配です。

それは良い考えですか?悪いアイデア?


2
これは、コンパイラごとに異なる場合があります。自分で試してみましたか?その操作を使用して最小限のプログラムを作成します。次に、結果のアセンブリコードを比較します。
マリオ

1
ええと、私は多くのC / C ++を知りませんが...のように見え**=2つの異なることをしています-前者は個々の値を加算し、後者はそれらを乗算します。また、異なるタイプのシグネチャを持っているようです。
時計仕掛けのミューズ

3
これは、ゲーム開発に特化した純粋なC ++プログラミングの質問のようです。おそらくStack Overflowに移行する必要がありますか?
イルマリカロネン16年

あなたは、パフォーマンスを心配している場合は、SIMD命令をご覧ください:en.wikipedia.org/wiki/Streaming_SIMD_Extensions
ピーター-禁止を解除ロバート・ハーヴェイ

1
少なくとも2つの理由から、独自の数学ライブラリを作成しないでください。まず、あなたはおそらくSSE組み込み関数の専門家ではないため、高速ではありません。第二に、代数的計算のためにGPUを使用する方がはるかに効率的です。なぜなら、GPUはそのためだけに作られているからです。右の「関連」セクションをご覧
1

回答:


18

実際には、追加のオーバーヘッドは発生しません。C ++では、通常、小さな関数は最適化としてコンパイラによってインライン化されるため、結果のアセンブリは呼び出しサイトですべての操作を実行します。関数は最終コードにのみ存在するため、関数は互いに呼び出しません。数学的操作。

コンパイラーに応じて、これらの関数の1つが最適化なしで、または最適化なしで(デバッグビルドの場合のように)他の関数を呼び出すことがあります。ただし、より高い最適化レベル(リリースビルド)では、数学のみに最適化されます。

それでもライブラリを作成したい場合(ライブラリを作成している場合など)、inlineキーワードoperator*()(および同様のラッパー関数)を追加すると、コンパイラーがインラインを実行するように示唆したり、次のようなコンパイラー固有のフラグ/構文を使用したりできます:-finline-small-functions-finline-functions-findirect-inlining__attribute__((always_inline)) (コメント欄に@Stephane Hockenhullの役立つ情報への信用)。個人的には、私が使用しているフレームワーク/ライブラリが何をするのかに従う傾向があります。GLKitの数学ライブラリを使用してGLK_INLINEいる場合は、それが提供するマクロも使用します。


Clang (Xcode 7.2のApple LLVMバージョン7.0.2 / clang-700.1.81)を使用して、次のmain()関数(関数および単純なVector3<T>実装と組み合わせてを使用した二重チェック:

int main(int argc, const char * argv[])
{
    Vector3<int> a = { 1, 2, 3 };
    Vector3<int> b;
    scanf("%d", &b.x);
    scanf("%d", &b.y);
    scanf("%d", &b.z);

    Vector3<int> c = a * b;

    printf("%d, %d, %d\n", c.x, c.y, c.z);

    return 0;
}

最適化フラグを使用してこのアセンブリにコンパイルします-O0

    .section    __TEXT,__text,regular,pure_instructions
    .globl  _main
    .align  4, 0x90
_main:                                  ## @main
Lfunc_begin0:
    .loc    6 30 0                  ## main.cpp:30:0
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    subq    $128, %rsp
    leaq    L_.str1(%rip), %rax
    ##DEBUG_VALUE: main:argc <- undef
    ##DEBUG_VALUE: main:argv <- undef
    movl    $0, -4(%rbp)
    movl    %edi, -8(%rbp)
    movq    %rsi, -16(%rbp)
    .loc    6 31 15 prologue_end    ## main.cpp:31:15
Ltmp3:
    movl    l__ZZ4mainE1a+8(%rip), %edi
    movl    %edi, -24(%rbp)
    movq    l__ZZ4mainE1a(%rip), %rsi
    movq    %rsi, -32(%rbp)
    .loc    6 33 2                  ## main.cpp:33:2
    leaq    L_.str(%rip), %rsi
    xorl    %edi, %edi
    movb    %dil, %cl
    leaq    -48(%rbp), %rdx
    movq    %rsi, %rdi
    movq    %rsi, -88(%rbp)         ## 8-byte Spill
    movq    %rdx, %rsi
    movq    %rax, -96(%rbp)         ## 8-byte Spill
    movb    %cl, %al
    movb    %cl, -97(%rbp)          ## 1-byte Spill
    movq    %rdx, -112(%rbp)        ## 8-byte Spill
    callq   _scanf
    .loc    6 34 17                 ## main.cpp:34:17
    leaq    -44(%rbp), %rsi
    .loc    6 34 2 is_stmt 0        ## main.cpp:34:2
    movq    -88(%rbp), %rdi         ## 8-byte Reload
    movb    -97(%rbp), %cl          ## 1-byte Reload
    movl    %eax, -116(%rbp)        ## 4-byte Spill
    movb    %cl, %al
    callq   _scanf
    .loc    6 35 17 is_stmt 1       ## main.cpp:35:17
    leaq    -40(%rbp), %rsi
    .loc    6 35 2 is_stmt 0        ## main.cpp:35:2
    movq    -88(%rbp), %rdi         ## 8-byte Reload
    movb    -97(%rbp), %cl          ## 1-byte Reload
    movl    %eax, -120(%rbp)        ## 4-byte Spill
    movb    %cl, %al
    callq   _scanf
    leaq    -32(%rbp), %rdi
    .loc    6 37 21 is_stmt 1       ## main.cpp:37:21
    movq    -112(%rbp), %rsi        ## 8-byte Reload
    movl    %eax, -124(%rbp)        ## 4-byte Spill
    callq   __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_E
    movl    %edx, -72(%rbp)
    movq    %rax, -80(%rbp)
    movq    -80(%rbp), %rax
    movq    %rax, -64(%rbp)
    movl    -72(%rbp), %edx
    movl    %edx, -56(%rbp)
    .loc    6 39 27                 ## main.cpp:39:27
    movl    -64(%rbp), %esi
    .loc    6 39 32 is_stmt 0       ## main.cpp:39:32
    movl    -60(%rbp), %edx
    .loc    6 39 37                 ## main.cpp:39:37
    movl    -56(%rbp), %ecx
    .loc    6 39 2                  ## main.cpp:39:2
    movq    -96(%rbp), %rdi         ## 8-byte Reload
    movb    $0, %al
    callq   _printf
    xorl    %ecx, %ecx
    .loc    6 41 5 is_stmt 1        ## main.cpp:41:5
    movl    %eax, -128(%rbp)        ## 4-byte Spill
    movl    %ecx, %eax
    addq    $128, %rsp
    popq    %rbp
    retq
Ltmp4:
Lfunc_end0:
    .cfi_endproc

上記で、__ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Eあなたのoperator*()関数はcallq別の__…Vector3…関数になります。それは非常に多くのアセンブリになります。でのコンパイル-O1はほとんど同じですが、__…Vector3…関数を呼び出します。

しかし、我々はそれをぶつけたときに-O2callq__…Vector3…に置き換え消えて、imull命令(* a.z* 3addl命令(* a.y* 2)、そしてちょうど使用してb.xストレートアップ(ので、値を* a.x* 1)。

    .section    __TEXT,__text,regular,pure_instructions
    .globl  _main
    .align  4, 0x90
_main:                                  ## @main
Lfunc_begin0:
    .loc    6 30 0                  ## main.cpp:30:0
    .cfi_startproc
## BB#0:
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    .loc    6 33 2 prologue_end     ## main.cpp:33:2
Ltmp3:
    pushq   %rbx
    subq    $24, %rsp
Ltmp4:
    .cfi_offset %rbx, -24
    ##DEBUG_VALUE: main:argc <- EDI
    ##DEBUG_VALUE: main:argv <- RSI
    leaq    L_.str(%rip), %rbx
    leaq    -24(%rbp), %rsi
Ltmp5:
    ##DEBUG_VALUE: operator*=<int, int>:rhs <- [RSI+0]
    ##DEBUG_VALUE: operator*<int, int>:rhs <- [RSI+0]
    ##DEBUG_VALUE: main:b <- [RSI+0]
    xorl    %eax, %eax
    movq    %rbx, %rdi
Ltmp6:
    callq   _scanf
    .loc    6 34 17                 ## main.cpp:34:17
    leaq    -20(%rbp), %rsi
Ltmp7:
    xorl    %eax, %eax
    .loc    6 34 2 is_stmt 0        ## main.cpp:34:2
    movq    %rbx, %rdi
    callq   _scanf
    .loc    6 35 17 is_stmt 1       ## main.cpp:35:17
    leaq    -16(%rbp), %rsi
    xorl    %eax, %eax
    .loc    6 35 2 is_stmt 0        ## main.cpp:35:2
    movq    %rbx, %rdi
    callq   _scanf
    .loc    6 22 18 is_stmt 1       ## main.cpp:22:18
Ltmp8:
    movl    -24(%rbp), %esi
    .loc    6 23 18                 ## main.cpp:23:18
    movl    -20(%rbp), %edx
    .loc    6 23 11 is_stmt 0       ## main.cpp:23:11
    addl    %edx, %edx
    .loc    6 24 11 is_stmt 1       ## main.cpp:24:11
    imull   $3, -16(%rbp), %ecx
Ltmp9:
    ##DEBUG_VALUE: main:c [bit_piece offset=64 size=32] <- ECX
    .loc    6 39 2                  ## main.cpp:39:2
    leaq    L_.str1(%rip), %rdi
    xorl    %eax, %eax
    callq   _printf
    xorl    %eax, %eax
    .loc    6 41 5                  ## main.cpp:41:5
    addq    $24, %rsp
    popq    %rbx
    popq    %rbp
    retq
Ltmp10:
Lfunc_end0:
    .cfi_endproc

このコードの場合、アセンブリで-O2-O3-Os、&-Ofastすべて見て同じ。


うーん。ここではメモリ不足になりますが、言語の設計では常にインライン化され、デバッグを支援するために最適化されていないビルドではインライン化されないことを意図していることを思い出します。過去に使った特定のコンパイラについて考えているのかもしれません。
スリップD.トンプソン

@Peter Wikipediaはあなたに同意しているようです。うん ええ、私は特定のツールチェーンを思い出していると思います。より良い答えを投稿してください?
スリップD.トンプソン

@ピーター右。テンプレート化された側面に追いついたと思います。乾杯!
スリップD.トンプソン

テンプレート関数にインラインキーワードを追加すると、コンパイラは最適化の最初のレベル(-O1)でインライン化する可能性が高くなります。GCCの場合、-finline-small-functions -finline-functions -findirect-inliningを使用して-O0でのインライン化を有効にするか、移植性のないalways_inline属性(inline void foo (const char) __attribute__((always_inline));)を使用することもできます。ベクトルを多用するものをデバッグ可能な状態で適切な速度で実行したい場合。
ステファンホッケンハル16年

1
単一の乗算命令しか生成されない理由は、乗算する定数にあります。1を乗算しても何も行われず、2を乗算すると最適化されますaddl %edx, %edx(つまり、値をそれ自体に追加します)。
アダム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.