* Chamando * = (ou * = chamando *) é mais lento que escrever funções separadas (para biblioteca de matemática)? [fechadas]

15

Eu tenho algumas classes de vetores onde as funções aritméticas se parecem com isso:

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

Quero fazer um pouco de limpeza para remover o código duplicado. Basicamente, quero converter todas as operator*funções para chamar operator*=funções 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;
}

Mas estou preocupado em saber se haverá alguma sobrecarga adicional com a chamada de função extra.

É uma boa ideia? Péssima ideia?

user112513312
fonte
2
Isso pode ser diferente de compilador para compilador. Você já experimentou? Escreva um programa minimalista usando essa operação. Em seguida, compare o código de montagem resultante.
Mario
11
Eu não conheço muito C / C ++, mas ... parece *e *=está fazendo duas coisas diferentes - a primeira adiciona os valores individuais e a segunda os multiplica. Eles também parecem ter assinaturas de tipo diferentes.
Clockwork-Muse
3
Parece uma questão de programação C ++ pura, sem nada específico para o desenvolvimento de jogos. Talvez deva ser migrado para o Stack Overflow ?
Ilmari Karonen
Se você está preocupado com o desempenho, consulte as instruções do SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter - Unban Robert Harvey
11
Por favor, não escreva sua própria biblioteca de matemática por pelo menos duas razões. Primeiro, você provavelmente não é especialista em intrínsecas SSE, portanto não será rápido. Segundo, é muito mais eficiente usar a GPU para o cálculo algébrico, porque foi feito exatamente para isso. Dê uma olhada na seção "relacionadas" à direita: gamedev.stackexchange.com/questions/9924/...
polkovnikov.ph

Respostas:

18

Na prática, nenhuma sobrecarga adicional será incorrida . No C ++, as pequenas funções geralmente são incorporadas pelo compilador como uma otimização, de modo que o assembly resultante terá todas as operações no local da chamada - as funções não se chamarão, pois as funções não existirão no código final, apenas as operações matemáticas.

Dependendo do compilador, você pode ver uma dessas funções chamando a outra sem ou com baixa otimização (como nas compilações de depuração). Porém, em um nível de otimização mais alto (compilações de versão), elas serão otimizadas apenas para a matemática.

Se você ainda quiser ser pedante (digamos que esteja criando uma biblioteca), adicionar a inlinepalavra-chave a operator*()(e funções semelhantes do wrapper) pode sugerir ao seu compilador que execute o inline ou use sinalizadores / sintaxe específicos do compilador, como: -finline-small-functions, -finline-functions, -findirect-inlining, __attribute__((always_inline)) (crédito para informações úteis de @Stephane Hockenhull nos comentários) . Pessoalmente, tenho a tendência de seguir o que as estruturas / bibliotecas que estou usando - se estiver usando a biblioteca de matemática do GLKit, também usarei a GLK_INLINEmacro que ele fornece.


Verifique novamente usando o Clang (Apple LLVM versão 7.0.2 / clang-700.1.81) do Xcode 7.2 , a seguinte main()função (em combinação com suas funções e uma Vector3<T>implementação ingênua ):

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 esse assembly usando o sinalizador de otimização -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

No exemplo acima, __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Eé sua operator*()função e acaba com callqoutra __…Vector3…função. Isso equivale a bastante montagem. Compilar com -O1é quase o mesmo, ainda chamando para __…Vector3…funções.

No entanto, quando aumentamos -O2, os callqs __…Vector3…desaparecem, substituídos por uma imullinstrução (o * a.z* 3), uma addlinstrução (o * a.y* 2) e apenas usando o b.xvalor diretamente (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 este código, a montagem em -O2, -O3, -Os, e -Ofasttodos parecem idênticos.

Slipp D. Thompson
fonte
Hmm. Estou ficando sem memória aqui, mas lembro que eles devem estar sempre embutidos no design do idioma, e apenas não embutidos em construções não otimizadas para ajudar na depuração. Talvez esteja pensando em um compilador específico que usei no passado.
Slipp D. Thompson
@ Peter Wikipedia parece concordar com você. Ugg. Sim, acho que estou lembrando uma cadeia de ferramentas específica. Poste uma resposta melhor, por favor?
Slipp D. Thompson
@ Peter Right. Eu acho que fui pego no aspecto do modelo. Felicidades!
precisa saber é o seguinte
Se você adicionar a palavra-chave embutida às funções de modelo, os compiladores terão mais chances de incorporar no primeiro nível de otimização (-O1). No caso do GCC, você também pode ativar o inlining em -O0 com -finline-small-functions -finline-functions -findirect-inlining ou usar o atributo always - inline não portátil ( inline void foo (const char) __attribute__((always_inline));). Se você deseja que coisas pesadas em vetor sejam executadas a uma velocidade razoável enquanto ainda são depuráveis.
Stephane Hockenhull
11
O motivo pelo qual ele gerou apenas uma única instrução de multiplicação é até as constantes pelas quais você está multiplicando. Uma multiplicação por 1 não faz nada, e a multiplicação por 2 é otimizada para addl %edx, %edx(ou seja, agrega o valor a si mesma).
Adam