Est-ce que * appeler * = (ou * = appeler *) est plus lent que d'écrire des fonctions séparées (pour la bibliothèque mathématique)? [fermé]

15

J'ai quelques classes vectorielles où les fonctions arithmétiques ressemblent à ceci:

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

Je veux faire un peu de nettoyage pour supprimer le code dupliqué. Fondamentalement, je veux convertir toutes les operator*fonctions pour appeler des operator*=fonctions comme ceci:

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

Mais je m'inquiète de savoir si cela entraînera des frais supplémentaires de l'appel de fonction supplémentaire.

Est-ce que c'est une bonne idée? Mauvaise idée?

user112513312
la source
2
Cela peut être différent d'un compilateur à l'autre. L'avez-vous essayé vous-même? Écrivez un programme minimaliste en utilisant cette opération. Comparez ensuite le code d'assemblage résultant.
Mario
1
Euh, je ne connais pas beaucoup de C / C ++ mais ... il ressemble *et *=fait deux choses différentes - la première ajoute les valeurs individuelles, la seconde les multipliant. Ils semblent également avoir différents types de signatures.
Clockwork-Muse
3
Cela semble être une pure question de programmation C ++ sans rien de spécifique au développement de jeux. Peut-être devrait-il être migré vers Stack Overflow ?
Ilmari Karonen
Si vous êtes préoccupé par les performances, vous devriez consulter les instructions SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter
1
Veuillez ne pas écrire votre propre bibliothèque de mathématiques pour au moins deux raisons. Tout d'abord, vous n'êtes probablement pas un expert en intrinsèques SSE, donc ce ne sera pas rapide. Deuxièmement, il est beaucoup plus efficace d'utiliser le GPU pour le bien des calculs algébriques, car il est fait juste pour cela. Jetez un œil dans la section "Connexes" à droite: gamedev.stackexchange.com/questions/9924/…
polkovnikov.ph

Réponses:

18

En pratique, aucun frais supplémentaire ne sera engagé . En C ++, les petites fonctions sont généralement intégrées par le compilateur comme une optimisation, donc l'assembly résultant aura toutes les opérations sur le site d'appel - les fonctions ne s'appelleront pas, car les fonctions n'existeront pas dans le code final, seulement les opérations mathématiques.

Selon le compilateur, vous pouvez voir l'une de ces fonctions appeler l'autre avec une optimisation nulle ou faible (comme avec les versions de débogage). Cependant, à un niveau d'optimisation plus élevé (versions de version), elles seront optimisées jusqu'au niveau mathématique.

Si vous souhaitez toujours être pédant à ce sujet (par exemple, vous créez une bibliothèque), l'ajout du inlinemot clé à operator*()(et des fonctions d'encapsulation similaires) peut suggérer à votre compilateur d'effectuer l'inline, ou en utilisant des drapeaux / syntaxe spécifiques au compilateur comme: -finline-small-functions, -finline-functions, -findirect-inlining, __attribute__((always_inline)) (crédit à des informations utiles de @Stephane Hockenhull dans les commentaires) . Personnellement, j'ai tendance à suivre ce que font les frameworks / bibliothèques que j'utilise - si j'utilise la bibliothèque mathématique de GLKit, je vais simplement utiliser la GLK_INLINEmacro qu'elle fournit également.


Double vérification en utilisant Clang (Apple LLVM version 7.0.2 / clang-700.1.81 de Xcode 7.2) , la main()fonction suivante (en combinaison avec vos fonctions et une Vector3<T>implémentation naïve ):

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

compile dans cet assembly en utilisant l'indicateur d'optimisation -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

Dans ce qui précède, __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Eest votre operator*()fonction et finit par callqune autre __…Vector3…fonction. Cela représente beaucoup de montage. Compiler avec -O1est presque le même, toujours appeler des __…Vector3…fonctions.

Cependant, lorsque nous le renforçons -O2, le callqs __…Vector3…disparaît, remplacé par une imullinstruction (le * a.z* 3), une addlinstruction (le * a.y* 2), et en utilisant simplement la b.xvaleur directement (parce que * 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

Pour ce code, l'assemblée à -O2, -O3, -Os, et -Ofasttous semblent identiques.

Slipp D. Thompson
la source
Hmm. Je manque de mémoire ici, mais je me souviens qu'ils sont destinés à être toujours intégrés dans la conception du langage, et uniquement non intégrés dans les versions non optimisées pour faciliter le débogage. Je pense peut-être à un compilateur spécifique que j'ai utilisé dans le passé.
Slipp D. Thompson
@Peter Wikipedia semble être d'accord avec vous. Ugg. Oui, je pense que je me souviens d'une chaîne d'outils spécifique. Poster une meilleure réponse s'il vous plaît?
Slipp D. Thompson
@Peter Right. Je suppose que j'ai été rattrapé par l'aspect modèle. À votre santé!
Slipp D. Thompson
Si vous ajoutez le mot clé inline aux fonctions du modèle, les compilateurs sont plus susceptibles de s'aligner au premier niveau d'optimisation (-O1). Dans le cas de GCC, vous pouvez également activer l'inline à -O0 avec -finline-small-functions -finline-functions -findirect-inlining ou utiliser l' attribut non portable always_inline ( inline void foo (const char) __attribute__((always_inline));). Si vous voulez que les choses lourdes de vecteurs fonctionnent à une vitesse raisonnable tout en étant déboguables.
Stephane Hockenhull
1
La raison pour laquelle elle n'a généré qu'une seule instruction de multiplication est due aux constantes par lesquelles vous multipliez. Une multiplication par 1 ne fait rien, et la multiplication par 2 est optimisée pour addl %edx, %edx(c'est- à -dire ajouter la valeur à elle-même).
Adam