Загружены ли аргументы в кеш для пустых функций? - PullRequest
0 голосов
/ 30 августа 2018

Я знаю, что компиляторы C ++ оптимизируют пустые (статические) функции.

Основываясь на этих знаниях, я написал фрагмент кода, который должен быть оптимизирован всякий раз, когда определен какой-либо идентификатор (с использованием опции -D компилятора). Рассмотрим следующий фиктивный пример:

#include <iostream>

#ifdef NO_INC

struct T {
    static inline void inc(int& v, int i) {}
};

#else

struct T {
    static inline void inc(int& v, int i) {
        v += i;
    }
};

#endif

int main(int argc, char* argv[]) {
    int a = 42;

    for (int i = 0; i < argc; ++i)
        T::inc(a, i);

    std::cout << a;
}

Желаемое поведение будет следующим: Всякий раз, когда определяется идентификатор NO_INC (при компиляции используется -DNO_INC), все вызовы T::inc(...) должны быть оптимизированы (из-за пустого тела функции). В противном случае вызов T::inc(...) должен вызвать приращение на некоторое заданное значение i.

У меня есть два вопроса по этому поводу:

  1. Верно ли мое предположение, что вызовы T::inc(...) не влияют отрицательно на производительность, когда я указываю опцию -DNO_INC, потому что вызов пустой функции оптимизирован?
  2. Интересно, все ли переменные (a и i) все еще загружаются в кеш при вызове T::inc(a, i) (при условии, что они еще не там), хотя тело функции пусто.

Спасибо за любой совет!

Ответы [ 3 ]

0 голосов
/ 30 августа 2018

Compiler Explorer - очень полезный инструмент для просмотра сборки вашей сгенерированной программы, потому что нет другого способа выяснить, оптимизировал ли компилятор что-то или нет. Демо .

При фактическом увеличении ваш main выглядит следующим образом:

main:                                   # @main
        push    rax
        test    edi, edi
        jle     .LBB0_1
        lea     eax, [rdi - 1]
        lea     ecx, [rdi - 2]
        imul    rcx, rax
        shr     rcx
        lea     esi, [rcx + rdi]
        add     esi, 41
        jmp     .LBB0_3
.LBB0_1:
        mov     esi, 42
.LBB0_3:
        mov     edi, offset std::cout
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        xor     eax, eax
        pop     rcx
        ret

Как видите, компилятор полностью встроил вызов T::inc и выполняет приращение напрямую.

Для пустого T::inc вы получите:

main:                                   # @main
        push    rax
        mov     edi, offset std::cout
        mov     esi, 42
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        xor     eax, eax
        pop     rcx
        ret

Компилятор оптимизировал весь цикл!

Верно ли мое предположение, что вызовы t.inc(...) не влияют отрицательно на производительность, когда я указываю опцию -DNO_INC, потому что вызов пустой функции оптимизирован?

Да.

Если мое предположение выполнено, верно ли оно и для более сложных функциональных тел (в ветви #else)?

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

Интересно, все ли переменные (a и i) все еще загружаются в кеш при вызове t.inc(a, i) (при условии, что они еще не там), хотя тело функции пусто.

Нет, как показано выше, цикл даже не существует.

0 голосов
/ 30 августа 2018

Верно ли мое предположение, что вызовы t.inc (...) не влияют отрицательно на производительность, когда я указываю опцию -DNO_INC, потому что вызов пустой функции оптимизирован? Если моё предположение верно, верно ли оно для более сложных тел функций (в ветви #else)?

Вы правы. Я изменил ваш пример (то есть удалил cout, который загромождает сборку) в проводнике компилятора , чтобы сделать более очевидным, что происходит.

Компилятор оптимизирует все и сразу

main:                                   # @main
        movl    $42, %eax
        retq

Только 42 ведется в eax и возвращается.

Однако в более сложном случае требуется больше инструкций для вычисления возвращаемого значения. Смотрите здесь

main:                                   # @main
        testl   %edi, %edi
        jle     .LBB0_1
        leal    -1(%rdi), %eax
        leal    -2(%rdi), %ecx
        imulq   %rax, %rcx
        shrq    %rcx
        leal    (%rcx,%rdi), %eax
        addl    $41, %eax
        retq
.LBB0_1:
        movl    $42, %eax    
        retq

Интересно, все ли переменные (a и i) все еще загружаются в кеш при вызове t.inc (a, i) (при условии, что их еще нет), хотя тело функции пусто.

Они загружаются только тогда, когда компилятор не может объяснить, что они не используются. См. Второй пример проводника компилятора.

Кстати : Вам не нужно создавать экземпляр T (т.е. T t;) для вызова статической функции внутри класса. Это побеждает цель. Назовите это как T::inc(...) rahter чем t.inc(...).

0 голосов
/ 30 августа 2018

Поскольку используется ключ inline, можно смело предположить, что 1. Использование этих функций не должно отрицательно влиять на производительность.

Выполнение вашего кода через

g ++ -c -Os -g

objdump -S

подтверждает это; Выписка:

int main(int argc, char* argv[]) {
    T t;
    int a = 42;
    1020:   b8 2a 00 00 00          mov    $0x2a,%eax
    for (int i = 0; i < argc; ++i)
    1025:   31 d2                   xor    %edx,%edx
    1027:   39 fa                   cmp    %edi,%edx
    1029:   7d 06                   jge    1031 <main+0x11>
        v += i;
    102b:   01 d0                   add    %edx,%eax
    for (int i = 0; i < argc; ++i)
    102d:   ff c2                   inc    %edx
    102f:   eb f6                   jmp    1027 <main+0x7>
        t.inc(a, i);
    return a;
}
    1031:   c3                      retq

(я заменил cout на return для лучшей читаемости)

...