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


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

Але мене хвилює питання, чи не спричинить це додаткові накладні витрати від виклику додаткової функції.

Це гарна ідея? Погана ідея?


2
Це може відрізнятися від компілятора до компілятора. Ви спробували самі? Напишіть мінімалістичну програму, використовуючи цю операцію. Потім порівняйте отриманий код складання.
Маріо

1
Ага, я не знаю багато C / C ++, але ... це схоже *і *=робимо дві різні речі - перший додає окремі цінності, другий примножуючи їх. Вони також мають різні підписи.
Годинник-Муза

3
Це здається чистим питанням програмування на C ++, що не має нічого конкретного для розробки ігор. Можливо, його слід перенести на Stack Overflow ?
Ілмарі Каронен

Якщо ви переживаєте про ефективність роботи, перегляньте інструкції SIMD: en.wikipedia.org/wiki/Streaming_SIMD_Extensions
Пітер

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 від Apple 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, callqs __…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все виглядають однаково.


Хм. Тут я вимикаю пам’ять, але я пам'ятаю, що вони призначені завжди бути вписаними в дизайн мови, і лише не вбудовані в неоптимізовані побудови для сприяння налагодженню. Можливо, я думаю про конкретний компілятор, яким я користувався в минулому.
Сліпп Д. Томпсон

@ Петер Вікіпедія, здається, погоджується з вами. Угг. Так, я думаю, що я згадую конкретну ланцюжок інструментів. Опублікуйте, будь ласка, кращу відповідь?
Сліпп Д. Томпсон

@Петер правильно. Я здогадуюсь, що мене наздогнали шаблонні аспекти. Ура!
Сліп Д. Томпсон

Якщо ви додасте ключове слово вбудований до функцій шаблону, компілятори, швидше за все, будуть вбудовані на першому рівні оптимізації (-O1). У випадку GCC ви також можете ввімкнути вбудовування в -O0 за допомогою -finline-small-function -finline-функции -findirect-inlining або використовувати атрибут, що не переноситься always_inline ( inline void foo (const char) __attribute__((always_inline));). Якщо ви хочете, щоб вектори-важкі речі працювали з розумною швидкістю, залишаючись налагоджуваною.
Стефан Хокенхолл

1
Причина, по якій генерується лише одна інструкція множення, зводиться до констант, на які ви множите. Множення на 1 нічого не робить, а множення на 2 оптимізоване до addl %edx, %edx(тобто додавання значення до себе).
Адам
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.