整理自 https://wolchok.org/posts/inlining-and-compiler-optimizations/

文章串起来了一些知识,读一读,增加一下见解。当然,学习一下clang/llvm更直接一些,这些都是二手复读

先引入两个概念,constant propagationloop-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加油