Ist * calling * = (oder * = calling *) langsamer als das Schreiben separater Funktionen (für die Mathematikbibliothek)? [geschlossen]

15

Ich habe einige Vektorklassen, in denen die arithmetischen Funktionen so aussehen:

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

Ich möchte ein bisschen aufräumen, um doppelten Code zu entfernen. Grundsätzlich möchte ich alle operator*Funktionen konvertieren , um operator*=Funktionen wie diese aufzurufen :

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

Ich bin jedoch besorgt darüber, ob der zusätzliche Funktionsaufruf zusätzliche Kosten verursacht.

Ist das eine gute Idee? Schlechte Idee?

user112513312
quelle
2
Dies kann von Compiler zu Compiler unterschiedlich sein. Hast du es selbst ausprobiert? Schreiben Sie mit dieser Operation ein minimalistisches Programm. Vergleichen Sie dann den resultierenden Assemblycode.
Mario
1
Äh, ich kenne nicht viel von C / C ++, aber ... es sieht so aus *und *=macht zwei verschiedene Dinge - der erste addiert die einzelnen Werte, der zweite multipliziert sie. Sie scheinen auch unterschiedliche Typensignaturen zu haben.
Clockwork-Muse
3
Dies scheint eine reine C ++ - Programmierfrage zu sein, die sich nicht speziell auf die Spieleentwicklung bezieht. Vielleicht sollte es nach Stack Overflow migriert werden ?
Ilmari Karonen
Wenn Sie sich Sorgen um die Leistung machen,
lesen
1
Bitte schreiben Sie aus mindestens zwei Gründen keine eigene Mathematikbibliothek. Erstens sind Sie wahrscheinlich kein Experte für SSE-Eigenheiten, es wird also nicht schnell gehen. Zweitens ist es viel effizienter, die GPU für algebraische Berechnungen zu verwenden, weil sie genau dafür gemacht ist. Werfen Sie einen Blick in die „Verwandte“ Abschnitt rechts: gamedev.stackexchange.com/questions/9924/...
polkovnikov.ph

Antworten:

18

In der Praxis fallen keine zusätzlichen Gemeinkosten an . In C ++ werden kleine Funktionen normalerweise vom Compiler als Optimierung eingefügt, sodass die resultierende Assembly alle Operationen auf der aufgerufenen Seite enthält - die Funktionen rufen sich nicht gegenseitig auf, da die Funktionen nur im endgültigen Code nicht vorhanden sind die mathematischen Operationen.

Abhängig vom Compiler wird möglicherweise eine dieser Funktionen angezeigt, die die andere mit keiner oder geringer Optimierung aufruft (wie bei Debugbuilds). Bei höheren Optimierungsstufen (Release-Builds) werden sie jedoch nur auf die Mathematik hin optimiert.

Wenn Sie immer noch umständlich vorgehen möchten (wenn Sie beispielsweise eine Bibliothek erstellen), kann das Hinzufügen des inlineSchlüsselworts zu operator*()(und ähnlichen Wrapper-Funktionen) dazu führen, dass Ihr Compiler die Inline-Funktion ausführt, oder Sie verwenden compilerspezifische Flags / Syntax wie: -finline-small-functions, -finline-functions, -findirect-inlining, __attribute__((always_inline)) (Kredit @Stephane Hockenhull des hilfreichen Informationen in den Kommentaren) . Persönlich neige ich dazu, dem Framework / den Bibliotheken zu folgen, die ich verwende - wenn ich die Mathematikbibliothek von GLKit verwende, verwende ich nur das GLK_INLINEMakro, das es auch bereitstellt.


Überprüfen Sie mit Clang (Xcode 7.2s Apple LLVM Version 7.0.2 / clang-700.1.81) die folgende main()Funktion (in Kombination mit Ihren Funktionen und einer naiven Vector3<T>Implementierung):

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

Kompiliert mit dem Optimierungsflag zu dieser Assembly -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

Oben __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Eist Ihre operator*()Funktion und endet mit einer callqanderen __…Vector3…Funktion. Es ist ein ziemlicher Montageaufwand. Das Kompilieren mit -O1ist fast dasselbe und ruft immer noch __…Vector3…Funktionen auf.

Wenn wir es jedoch anstoßen -O2, müssen die callqs __…Vector3…verschwinden, durch eine imullAnweisung (das * a.z* 3), eine addlAnweisung (das * a.y* 2) und einfach den b.xWert straight-up (weil * a.x* 1) ersetzt werden.

    .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

Für diesen Code, wobei die Anordnung an -O2, -O3, -Os, und -Ofastalle identisch aussehen.

Slipp D. Thompson
quelle
Hmm. Ich habe hier keinen Speicherplatz mehr, aber ich erinnere mich, dass sie im Design der Sprache immer inline sein sollen und nur in nicht optimierten Builds nicht inline, um das Debuggen zu unterstützen. Vielleicht denke ich über einen bestimmten Compiler nach, den ich in der Vergangenheit verwendet habe.
Slipp D. Thompson
@ Peter Wikipedia scheint Ihnen zuzustimmen. Ugg. Ja, ich denke, ich erinnere mich an eine bestimmte Toolchain. Bitte geben Sie eine bessere Antwort ein.
Slipp D. Thompson
@ Peter Richtig. Ich glaube, ich war von dem Aspekt der Vorlage fasziniert. Prost!
Slipp D. Thompson
Wenn Sie das Schlüsselwort inline zu den Vorlagenfunktionen hinzufügen, werden Compiler eher auf der ersten Optimierungsebene (-O1) eingebunden. Im Fall von GCC können Sie Inlining bei -O0 auch mit -finline-small-functions -finline-functions aktivieren -findirect- aktivieren oder das nicht portable always_inline- Attribut ( inline void foo (const char) __attribute__((always_inline));) verwenden. Wenn Sie möchten, dass vektorlastige Dinge mit einer angemessenen Geschwindigkeit ausgeführt werden, während sie noch debuggbar sind.
Stephane Hockenhull
1
Der Grund, warum nur ein einziger Multiplikationsbefehl generiert wird, liegt in den Konstanten, mit denen Sie multiplizieren. Eine Multiplikation mit 1 bewirkt nichts, und die Multiplikation mit 2 ist optimiert für addl %edx, %edx(dh addiert den Wert zu sich selbst).
Adam