(译)Inlining and Compiler Optimizations
整理自 https://wolchok.org/posts/inlining-and-compiler-optimizations/
文章串起来了一些知识,读一读,增加一下见解。当然,学习一下clang/llvm更直接一些,这些都是二手复读
先引入两个概念,constant propagation 和loop-invariant code motion (LICM). 循环不变量外提
第一个概念就是立即数生成,只要是常数复制,直接有优化成立即数,避免取地址
第二个概念就是字面意思,看代码
void multiplyArrayByTwoConstants(std::span<int> arr, int x, int y) {
for (int& element: arr) {
element *= x * y;
}
}
x*y和循环没关系,会被外提到for之前算好
讨论inline和这两种优化结合的场景
inline + constant propagation
inline + const出现的路径都会优化成立即数
如果不是inline(实现不在同一个文件),就会退化成调用函数,godbolt
inline + virtual + constant propagation
考虑引入virtual,如果不是传参数,优化效果是一样的,但是如果是传参数,就不一样了
class MyInt {
public:
explicit MyInt(int x) : val(x) {}
virtual int get() const noexcept {
return val;
};
private:
int val;
};
MyInt nonConstNum(23);
// noinline to make the other print functions'
// assembly easy to read.
__attribute__((noinline))
void printNum(int num) {
std::printf("%d\n", num);
}
void printArgByValue(MyInt num) {
printNum(num.get());
}
void printArgByConstReference(const MyInt& num) {
printNum(num.get());
}
由于类具有多态性,函数不能确认是基类还是子类,所以还是调用虚函数的get
inline + LICM
同样inline的能被优化,放到for循环外提前执行
class MyInt {
public:
explicit MyInt(int x) : val(x) {}
int get() const noexcept {
return val;
};
private:
int val;
};
void multiplyArrayByTwoConstants(std::span<int> arr, MyInt x, MyInt y) {
for (int& element: arr) {
element *= x.get() * y.get();
}
}
如果不是inline(实现没有放在一个文件中) 就会变成调用,这和get是不是const没关系,主要是inline起作用
在没见到get的实现之前,val是不是被改动了,很难说,即使你的get是const的,内部的实现还是有可能出现const_cast之类的骚操作
一个经典LICM例子 strlen
经常建议strlen放到for循环外面
但是放到循环内部也是可以有LICM优化 的
可以看到strlen只调用一次,这种场景,由于isupper不会改动改动s,可以放心优化strlen
再举一个反例
这个黄色的部分,callq strlen是随着循环一起执行的,调用N次,这是由于循环内改了s
当然,用指针更干净一些,用不上判断长度
#include <cctype>
#include <cstring>
void toUpperCase(char *s) {
while (*s) {
*s = std::toupper(*s);
s++;
}
}
实际优化后的效果是一样的
那是不是 应该全放到头文件里实现?
首先,编译时间太坑爹了,另外某些场景是不允许放到头文件的
另外,链接期间会有LTO / thinLTO 这个期间也会inline,所以说拆开就inline是不正确的,但是LTO thinLTO的威力就很难保证了,clang/llvm加油