我知道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)
)是否仍被加载到缓存中(假定它们还不存在),尽管函数体为空。 谢谢您的建议!
答案 0 :(得分:3)
Compiler Explorer是查看生成程序汇编的非常有用的工具,因为没有其他方法可以确定编译器是否确定了某些优化。 Demo。
随着实际增加,您的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)
)是否仍被加载到缓存中(假设它们还不存在),尽管函数体为空。
不,如上所述,该循环甚至不存在。
答案 1 :(得分:3)
我的假设正确吗,因为对空函数的调用已优化,所以在指定-DNO_INC选项时对t.inc(...)的调用不会对性能产生负面影响?如果我的假设成立,那么它是否也适用于更复杂的函数体(在#else分支中)?
你是对的。我在compiler explorer中修改了您的示例(即删除了使程序混乱的cout),以使发生的情况更加明显。
编译器优化一切工作和出局
main: # @main
movl $42, %eax
retq
只有42位被带入eax并返回。
但是,对于更复杂的情况,需要更多指令来计算返回值。 See here
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
我想知道,尽管函数体为空,但是在调用t.inc(a,i)时是否仍将变量(a和i)加载到缓存中(假设它们还不存在)。
仅当编译器无法推理未使用它们时才加载它们。请参阅第二个编译器资源管理器示例。
顺便:您无需创建T的实例(即T t;
)即可在类中调用静态函数。这是失败的目的。像T::inc(...)
比t.inc(...)
更叫它。
答案 2 :(得分:1)
由于使用了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
(为了获得更好的可读性,我用return替换了cout)