* Вызов * = (или * = вызов *) медленнее, чем написание отдельных функций (для математической библиотеки)? [закрыто]

15

У меня есть несколько векторных классов, где арифметические функции выглядят так:

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

Я хочу сделать небольшую очистку, чтобы удалить дублированный код. По сути, я хочу преобразовать все operator*функции для вызова operator*=функций следующим образом:

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

Но я обеспокоен тем, не повлечет ли это какие-либо дополнительные издержки от дополнительного вызова функции.

Это хорошая идея? Плохая идея?

user112513312
источник
2
Это может отличаться от компилятора к компилятору. Вы сами пробовали? Напишите минималистичную программу, используя эту операцию. Затем сравните полученный код сборки.
Марио
1
Э - э, я не знаю , много C / C ++ , но ... похоже , *и *=делают разные вещи - бывшие добавляют отдельные значения, последних их умножения. Они также имеют подписи разных типов.
Заводная муза
3
Это похоже на чистый вопрос программирования на C ++, в котором нет ничего специфического для разработки игр. Возможно, его следует перенести в переполнение стека ?
Илмари Каронен
Если вы беспокоитесь о производительности, вам следует ознакомиться с инструкциями SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Peter
1
Пожалуйста, не пишите свою собственную математическую библиотеку по крайней мере по двум причинам. Во-первых, вы, вероятно, не являетесь экспертом в SSE, поэтому это не будет быстрым. Во-вторых, гораздо эффективнее использовать GPU для алгебраических вычислений, потому что это сделано именно для этого. Загляните в раздел «Связанные» справа: gamedev.stackexchange.com/questions/9924/…
polkovnikov.ph

Ответы:

18

На практике никаких дополнительных накладных расходов не возникает . В C ++ небольшие функции обычно указываются компилятором как оптимизация, поэтому итоговая сборка будет иметь все операции на месте вызова - функции не будут вызывать друг друга, так как функции не будут существовать в конечном коде, только математические операции.

В зависимости от компилятора, вы можете увидеть, что одна из этих функций вызывает другую без оптимизации или с низкой оптимизацией (как в отладочных сборках). При более высоком уровне оптимизации (сборка релиза) они будут оптимизированы вплоть до математики.

Если вы все еще хотите быть педантичными (скажем, вы создаете библиотеку), добавление inlineключевого слова operator*()(и аналогичных функций-оболочек) может подсказать вашему компилятору выполнение встроенного кода или использование специфичных для компилятора флагов / синтаксиса, например: -finline-small-functions, -finline-functions,-findirect-inlining , __attribute__((always_inline)) (кредит полезной информации @Stephane Hockenhull в комментариях) . Лично я склонен следовать тому, что делают фреймворки / библиотеки, которые я использую - если я использую математическую библиотеку GLKit, я просто использую GLK_INLINEмакрос, который она предоставляет.


Двойная проверка с использованием Clang (Apple LLVM версии 7.0.2 / clang-700.1.81) в Xcode 7.2 , следующей main()функции (в сочетании с вашими функциями и простой Vector3<T>реализацией):

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

компилируется в эту сборку с использованием флага оптимизации -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

В приведенном выше, __ZmlIiiE7Vector3IDTmldtfp_1xdtfp0_1xEERKS0_IT_ERKS0_IT0_Eваша operator*()функция и в конечном итогеcallq другой __…Vector3…функцией. Это довольно много сборок. Компиляция с -O1почти такой же, все еще вызывая __…Vector3…функции.

Однако, когда мы поднять его до -O2, то callqс до __…Vector3…исчезнуть, сменившись с imullинструкцией ( * a.z* 3), с addlинструкцией ( * a.y* 2), а просто используя b.xзначение прямо вверх (потому что* 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

Для этого кода, сборка на -O2, -O3, -Os, и -Ofastвсе выглядят одинаково.

Слипп Д. Томпсон
источник
Хм. Здесь я ухожу из памяти, но я вспоминаю, что они предназначены для того, чтобы всегда быть встроенными в структуру языка, и только не встроенными в неоптимизированных сборках для облегчения отладки. Может быть, я думаю о конкретном компиляторе, который я использовал в прошлом.
Слипп Д. Томпсон
@ Питер Википедия, похоже, согласна с вами. Ugg. Да, я думаю, что вспоминаю конкретную цепочку инструментов. Выложите лучший ответ, пожалуйста?
Слипп Д. Томпсон
@ Питер Верно. Я полагаю, я был увлечен шаблонным аспектом. Ура!
Слипп Д. Томпсон
Если вы добавите ключевое слово inline в функции шаблона, компиляторы с большей вероятностью будут встроены на первом уровне оптимизации (-O1). В случае GCC вы также можете включить встраивание в -O0 с помощью -finline-small-functions -finline-functions -findirect-inlining или использовать непереносимый атрибут always_inline ( inline void foo (const char) __attribute__((always_inline));). Если вы хотите, чтобы тяжелые объекты работали с разумной скоростью, но все еще можно было отлаживать.
Стефан Хоккенхалл
1
Причина, по которой она генерирует только одну инструкцию умножения, заключается в том, что константы, на которые вы умножаете. Умножение на 1 ничего не делает, а умножение на 2 оптимизируется для addl %edx, %edx(т.е. добавляет значение к себе).
Адам