Czy * wywoływanie * = (lub * = wywoływanie *) jest wolniejsze niż pisanie oddzielnych funkcji (dla biblioteki matematycznej)? [Zamknięte]

15

Mam kilka klas wektorowych, w których funkcje arytmetyczne wyglądają tak:

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

Chcę zrobić trochę czyszczenia, aby usunąć zduplikowany kod. Zasadniczo chcę przekonwertować wszystkie operator*funkcje, aby wywoływały operator*=funkcje takie jak to:

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

Jestem jednak zaniepokojony, czy nie spowoduje to dodatkowych kosztów związanych z wywołaniem funkcji dodatkowej.

Czy to dobry pomysł? Kiepski pomysł?

użytkownik112513312
źródło
2
Może się to różnić od kompilatora do kompilatora. Próbowałeś tego sam? Napisz minimalistyczny program za pomocą tej operacji. Następnie porównaj wynikowy kod zestawu.
Mario
1
Nie znam dużo C / C ++, ale ... wygląda na to *i *=robię dwie różne rzeczy - pierwsza dodaje poszczególne wartości, druga zwielokrotnia je. Wydają się również mieć podpisy innego typu.
Clockwork-Muse,
3
Wydaje się, że jest to czyste pytanie programistyczne w języku C ++, które nie ma nic konkretnego w tworzeniu gier. Być może powinien zostać przeniesiony do Przepełnienie stosu ?
Ilmari Karonen,
Jeśli martwisz się wydajnością, powinieneś zapoznać się z instrukcjami SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter
1
Nie pisz własnej biblioteki matematycznej z co najmniej dwóch powodów. Po pierwsze, prawdopodobnie nie jesteś ekspertem w dziedzinie SSE, więc nie będzie to szybkie. Po drugie, znacznie wydajniejsze jest używanie GPU ze względu na obliczenia algebraiczne, ponieważ jest właśnie do tego stworzony. Spójrz na sekcję „Powiązane” po prawej: gamedev.stackexchange.com/questions/9924/…
polkovnikov.ph 11.01.16

Odpowiedzi:

18

W praktyce nie będą ponoszone żadne dodatkowe koszty ogólne . W C ++ małe funkcje są zwykle kompilowane przez kompilator jako optymalizacja, więc powstały zestaw będzie miał wszystkie operacje w witrynie wywołań - funkcje nie będą się nawzajem wywoływać, ponieważ funkcje nie będą istnieć w końcowym kodzie, tylko operacje matematyczne.

W zależności od kompilatora jedna z tych funkcji może wywoływać drugą bez optymalizacji lub z niską optymalizacją (jak w przypadku kompilacji debugowania). Jednak na wyższym poziomie optymalizacji (kompilacje wersji) zostaną one zoptymalizowane do samej matematyki.

Jeśli nadal chcesz być pedantyczny (powiedz, że tworzysz bibliotekę), dodanie inlinesłowa kluczowego do operator*()(i podobnych funkcji opakowania) może zasugerować kompilatorowi, aby wykonał polecenie wbudowane, lub użyj specyficznych dla kompilatora flag / składni, takich jak: -finline-small-functions, -finline-functions, -findirect-inlining, __attribute__((always_inline)) (kredyt do @Stephane Hockenhull pomocne informacji w komentarzach) . Osobiście staram się podążać za tym, co robią frameworki / biblioteki, których używam - jeśli używam biblioteki matematycznej GLKit, po prostu użyję dostarczonego przez nią GLK_INLINEmakra.


Podwójne sprawdzenie za pomocą Clang (Apple LLVM Xcode 7.2 w wersji 7.0.2 / clang-700.1.81) , następująca main()funkcja (w połączeniu z twoimi funkcjami i naiwną Vector3<T>implementacją):

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

kompiluje do tego zestawu przy użyciu flagi optymalizacji -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

Powyżej __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Ejest twoja operator*()funkcja i kończy się na callqinnej __…Vector3…funkcji. Sprowadza się to do całkiem sporego montażu. Kompilacja -O1jest prawie taka sama, wciąż wywołuje __…Vector3…funkcje.

Jednak kiedy podbijemy go -O2, callqs __…Vector3…zniknie, zastąpiony imullinstrukcją ( * a.z* 3), addlinstrukcją ( * a.y* 2) i po prostu używając b.xwartości prosto (ponieważ * 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

Dla tego kodu, montaż na -O2, -O3, -Os, i -Ofastwszystkie wyglądają identycznie.

Slipp D. Thompson
źródło
Hmm W tym momencie nie mam już pamięci, ale pamiętam, że mają one być zawsze wbudowane w projekt języka i tylko nie wstawiane w niezoptymalizowanych kompilacjach w celu ułatwienia debugowania. Może myślę o konkretnym kompilatorze, którego używałem w przeszłości.
Slipp D. Thompson,
@Peter Wikipedia wydaje się zgadzać z tobą. Ugg. Tak, myślę, że przypominam sobie konkretny łańcuch narzędzi. Poproś o lepszą odpowiedź?
Slipp D. Thompson,
@Peter Right. Chyba złapałem się na templated aspekt. Twoje zdrowie!
Slipp D. Thompson
Jeśli dodasz słowo kluczowe inline do szablonów, kompilatory będą bardziej skłonne do inline na pierwszym poziomie optymalizacji (-O1). W przypadku GCC można również włączyć wstawianie w -O0 za pomocą -finline-small-functions -finline-functions -indirect-inlining lub użyć nieprzenośnego atrybutu always_inline ( inline void foo (const char) __attribute__((always_inline));). Jeśli chcesz, aby duże obiekty wektorowe działały z rozsądną prędkością, a jednocześnie były debugowalne.
Stephane Hockenhull
1
Powodem, dla którego wygenerowała tylko jedną instrukcję mnożenia, są stałe, przez które mnożymy. Mnożenie przez 1 nic nie robi, a mnożenie przez 2 jest zoptymalizowane addl %edx, %edx(tj. Dodaje wartość do siebie).
Adam