* Calling * = (o * = calling *) è più lento della scrittura di funzioni separate (per la libreria matematica)? [chiuso]


15

Ho alcune classi vettoriali in cui le funzioni aritmetiche si presentano così:

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

Voglio fare un po 'di pulizia per rimuovere il codice duplicato. Fondamentalmente, voglio convertire tutte le operator*funzioni per chiamare operator*=funzioni come questa:

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

Ma sono preoccupato se ciò comporterà costi aggiuntivi dalla chiamata di funzione extra.

È una buona idea? Cattiva idea?


2
Questo potrebbe essere diverso da compilatore a compilatore. L'hai provato tu stesso? Scrivi un programma minimalista usando quell'operazione. Quindi confrontare il codice assembly risultante.
Mario

1
Uh, non conosco molto C / C ++ ma ... sembra *e *=stanno facendo due cose diverse: la prima aggiunge i singoli valori, la seconda li moltiplica. Sembrano inoltre avere firme di tipo diverso.
Clockwork-Muse

3
Sembra una pura domanda di programmazione C ++ senza nulla di specifico per lo sviluppo del gioco. Forse dovrebbe essere migrato in Stack Overflow ?
Ilmari Karonen,

Se sei preoccupato per le prestazioni, dovresti guardare le istruzioni SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter

1
Per favore, non scrivere la tua libreria matematica per almeno due motivi. Innanzitutto, probabilmente non sei un esperto di intrinseci SSE, quindi non sarà veloce. In secondo luogo, è molto più efficiente utilizzare la GPU per il bene dei calcoli algebrici perché è fatta apposta per quello. Dai un'occhiata alla sezione "Correlata" a destra: gamedev.stackexchange.com/questions/9924/…
polkovnikov.ph

Risposte:


18

In pratica, non saranno sostenuti costi aggiuntivi . In C ++, il compilatore solitamente definisce le piccole funzioni come un'ottimizzazione, quindi l'assemblaggio risultante avrà tutte le operazioni sul sito di chiamata - le funzioni non si chiameranno a vicenda, poiché le funzioni non esisteranno nel codice finale, solo le operazioni matematiche.

A seconda del compilatore, potresti vedere una di queste funzioni che chiama l'altra senza ottimizzazione nulla o bassa (come con build di debug). Tuttavia, a livelli di ottimizzazione più elevati (build di rilascio), saranno ottimizzati fino alla matematica.

Se desideri ancora essere pedante al riguardo (supponi di creare una libreria), l'aggiunta della inlineparola chiave a operator*()(e funzioni di wrapper simili) può suggerire al compilatore di eseguire l'inline o utilizzare flag / sintassi specifici del compilatore come: -finline-small-functions, -finline-functions, -findirect-inlining, __attribute__((always_inline)) (credito alle informazioni utili di @Stephane Hockenhull nei commenti) . Personalmente, tendo a seguire ciò che fanno il framework / libs che sto usando— se sto usando la libreria matematica di GLKit, userò solo la GLK_INLINEmacro che fornisce anche.


Doppio controllo usando Clang (Apple LLVM versione 7.0.2 / clang-700.1.81 di Xcode 7.2) , la seguente main()funzione (in combinazione con le tue funzioni e Vector3<T>un'implementazione 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 questo assembly usando il flag di ottimizzazione -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

In quanto sopra, __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Eè la tua operator*()funzione e finisce callqcon un'altra __…Vector3…funzione. Si tratta di un bel po 'di assemblaggio. Compilare con -O1è quasi lo stesso, richiamando ancora le __…Vector3…funzioni.

Tuttavia, quando lo saliamo su -O2, le callqs __…Vector3…scompaiono, sostituite con imullun'istruzione (la * a.z* 3), addlun'istruzione (la * a.y* 2), e semplicemente usando il b.xvalore direttamente (perché * 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

Per questo codice, il gruppo a -O2, -O3, -Os, e -Ofasttutto lo sguardo identico.


Hmm. Sto andando fuori di memoria qui, ma ricordo che sono destinati ad essere sempre incorporati nella progettazione del linguaggio e solo non incorporati in build non ottimizzate per facilitare il debug. Forse sto pensando a un compilatore specifico che ho usato in passato.
Slipp D. Thompson

@Peter Wikipedia sembra essere d'accordo con te. Ugg. Sì, penso di ricordare una toolchain specifica. Pubblica una risposta migliore per favore?
Slipp D. Thompson

@Peter Right. Immagino di essere stato catturato dall'aspetto del modello. Saluti!
Slipp D. Thompson

Se si aggiunge la linea parola chiave per le funzioni template compilatori hanno maggiori probabilità di linea al primo livello di ottimizzazione (-O1). Nel caso di GCC puoi anche abilitare l'allineamento a -O0 con -finline-small-funzioni -finline-funzioni -findirect-inlining o usare l' attributo always_inline non portatile ( inline void foo (const char) __attribute__((always_inline));). Se vuoi che le cose pesanti di vettore funzionino a una velocità ragionevole mentre sono ancora debuggabili.
Stephane Hockenhull,

1
Il motivo per cui viene generata un'unica istruzione moltiplicata dipende dalle costanti per le quali si moltiplica. Una moltiplicazione per 1 non fa nulla e la moltiplicazione per 2 è ottimizzata per addl %edx, %edx(cioè aggiungere il valore a se stessa).
Adam
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.