¿Es * invocar * = (o * = llamar *) más lento que escribir funciones separadas (para la biblioteca matemática)? [cerrado]

15

Tengo algunas clases de vectores donde las funciones aritméticas se ven así:

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;
}

Quiero hacer un poco de limpieza para eliminar el código duplicado. Básicamente, quiero convertir todas las operator*funciones para llamar a operator*=funciones como esta:

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;
}

Pero me preocupa si incurrirá en una sobrecarga adicional de la llamada de función adicional.

¿Es una buena idea? ¿Mala idea?

usuario112513312
fuente
2
Esto puede ser diferente de un compilador a otro. ¿Lo has intentado tú mismo? Escribe un programa minimalista usando esa operación. Luego compare el código de ensamblaje resultante.
Mario
1
Uh, no conozco mucho C / C ++ pero ... parece *y *=está haciendo dos cosas diferentes: la primera agrega los valores individuales, la segunda los multiplica. También parecen tener diferentes tipos de firmas.
Clockwork-Muse
3
Esto parece una pregunta pura de programación en C ++ sin nada específico para el desarrollo del juego. ¿Quizás debería migrarse a Stack Overflow ?
Ilmari Karonen
Si le preocupa el rendimiento, debe consultar las instrucciones SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter
1
No escriba su propia biblioteca de matemáticas por al menos dos razones. Primero, probablemente no seas un experto en intrínsecos de SSE, por lo que no será rápido. En segundo lugar, es mucho más eficiente usar GPU por el bien de los cálculos algebraicos porque está hecho solo para eso. Echa un vistazo a la sección "Relacionados" a la derecha: gamedev.stackexchange.com/questions/9924/…
polkovnikov.ph

Respuestas:

18

En la práctica, no se incurrirá en gastos generales adicionales . En C ++, las funciones pequeñas generalmente están alineadas por el compilador como una optimización, por lo que el ensamblado resultante tendrá todas las operaciones en el sitio de llamadas: las funciones no se llamarán entre sí, ya que las funciones no existirán en el código final, solo Las operaciones matemáticas.

Dependiendo del compilador, puede ver que una de estas funciones llama a la otra sin optimización o con poca optimización (como con las compilaciones de depuración). Sin embargo, a un nivel de optimizaciones más alto (versiones de lanzamiento), se optimizarán solo para las matemáticas.

Si aún desea ser pedante al respecto (digamos que está creando una biblioteca), agregar la inlinepalabra clave operator*()(y funciones de envoltura similares) puede sugerirle a su compilador que realice la línea, o usar indicadores / sintaxis específicos del compilador como: -finline-small-functions, -finline-functions, -findirect-inlining, __attribute__((always_inline)) (crédito a información útil de @Stephane Hockenhull en los comentarios) . Personalmente, tiendo a seguir lo que hacen el framework / libs que estoy usando; si estoy usando la biblioteca matemática de GLKit, solo usaré la GLK_INLINEmacro que proporciona también.


Verificación doble usando Clang (Apple LLVM Xcode 7.2 versión 7.0.2 / clang-700.1.81) , la siguiente main()función (en combinación con sus funciones y una Vector3<T>implementación ingenua ):

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;
}

compila a este ensamblaje utilizando el indicador de optimización -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

En lo anterior, __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Ees su operator*()función y termina callqcon otra __…Vector3…función. Se trata de una gran cantidad de montaje. Compilar con -O1es casi lo mismo, aún llamando a las __…Vector3…funciones.

Sin embargo, cuando lo subimos -O2, la callqs __…Vector3…desaparece, reemplazada por una imullinstrucción (la * a.z* 3), una addlinstrucción (la * a.y* 2), y simplemente usando el b.xvalor directamente (porque * 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

Para que este código, la asamblea en -O2, -O3, -Os, y -Ofasttoda la mirada idéntica.

Slipp D. Thompson
fuente
Hmm Aquí me estoy quedando sin memoria, pero recuerdo que están destinados a estar siempre integrados en el diseño del lenguaje, y solo no incorporados en compilaciones no optimizadas para ayudar a la depuración. Tal vez estoy pensando en un compilador específico que he usado en el pasado.
Slipp D. Thompson
@Peter Wikipedia parece estar de acuerdo contigo. Ugg Sí, creo que estoy recordando una cadena de herramientas específica. ¿Publicar una mejor respuesta por favor?
Slipp D. Thompson
@ Peter Right. Creo que estaba atrapado en el aspecto de la plantilla. ¡Salud!
Slipp D. Thompson
Si agrega la palabra clave en línea a las funciones de plantilla, es más probable que los compiladores en línea en el primer nivel de optimización (-O1). En el caso de GCC, también puede habilitar la alineación en -O0 con -finline-small-functions -finline-functions -findirect-inlining o usar el atributo no portable always_inline ( inline void foo (const char) __attribute__((always_inline));). Si desea que las cosas con muchos vectores se ejecuten a una velocidad razonable mientras aún se pueden depurar.
Stephane Hockenhull
1
La razón por la que solo genera una sola instrucción de multiplicación se debe a las constantes por las que multiplica. Una multiplicación por 1 no hace nada, y la multiplicación por 2 está optimizada para addl %edx, %edx(es decir, agregar el valor a sí mismo).
Adam