Загружены ли аргументы в кеш для пустых функций?
Я знаю, что компиляторы 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
,
У меня есть два вопроса по этому поводу:
- Правильно ли мое предположение, что призывает
T::inc(...)
не влияют отрицательно на производительность при указании-DNO_INC
вариант, потому что вызов пустой функции оптимизирован? - Интересно, если переменные (
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 с возвратом для лучшей читаемости)