*调用* =(或* =调用*)是否比编写单独的函数(对于数学库)要慢?[关闭]


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 ++不太了解,但是...看起来好像**=在做两件事-前者将各个值相加,后者将它们相乘。它们似乎也具有不同的类型签名。
Clockwork-Muse

3
这似乎是一个纯C ++编程问题,没有专门针对游戏开发的问题。也许应该将其迁移到Stack Overflow
Ilmari Karonen

如果您担心性能,则应查看SIMD指令:en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter

1
出于至少两个原因,请不要编写自己的数学库。首先,您可能不是SSE内在函数方面的专家,所以它不会很快。其次,为了进行代数计算而使用GPU效率要高得多,因为它就是为此而设计的。看一下右侧的“相关”部分:gamedev.stackexchange.com/questions/9924/…–
polkovnikov.ph

Answers:


18

实际上,不会产生额外的开销。在C ++中,编译器通常会将小型函数作为内联函数进行优化,因此,生成的程序集将在调用位置进行所有操作-这些函数不会相互调用,因为这些函数不会在最终代码中存在,因此数学运算。

根据编译器的不同,您可能会看到其中一个函数在没有优化或优化程度较低的情况下调用另一个函数(与调试版本一样)。在更高的优化级别(发布版本),它们将被优化为仅数学形式。

如果您仍然想对其进行研究(例如,您正在创建一个库),则将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…函数。

但是,当我们将其增大到时-O2callqs __…Vector3…消失,被一条imull指令(* a.z* 3),一条addl指令(* 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所有的外观相同。


嗯 我在这里没有足够的存储空间,但是我想起来,它们总是要内联在语言的设计中,并且只有在未优化的构建中才内联以帮助调试。也许我正在考虑过去使用的特定编译器。
Slipp D. Thompson

@Peter Wikipedia似乎同意您的观点。gg 是的,我想我想起了一个特定的工具链。请发布更好的答案?
Slipp D. Thompson

@彼得权利。我想我对模板化方面了如指掌。干杯!
Slipp D. Thompson

如果将inline关键字添加到模板函数,则编译器更有可能在第一级优化(-O1)内联。对于GCC,您还可以使用-finline-small-functions -finline-functions -findirect-inlining在-O0处启用内联,或使用非可移植的always_inline属性(inline void foo (const char) __attribute__((always_inline));)。如果您希望向量繁重的事物以合理的速度运行,同时仍可调试。
Stephane Hockenhull,2016年

1
它仅生成单个乘法指令的原因在于您要乘以的常数。乘以1不会执行任何操作,并且会优化乘以2 addl %edx, %edx(即,将值加到自身)。
亚当
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.