Загружены ли аргументы в кеш для пустых функций?

Я знаю, что компиляторы 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 ответа

Решение

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) вызывается (при условии, что их еще нет), хотя тело функции пусто.

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

Верно ли мое предположение, что вызовы 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(...) скорее чем t.inc(...),

Поскольку inline Используется keword, вы можете смело предполагать, что 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 с возвратом для лучшей читаемости)

Другие вопросы по тегам