* 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.