当前位置: 首页 > news >正文

C++ 中 inline 与内联优化

本文部分内容转载于此处。原文是一篇非常好的文章,值得推荐。

受我的贴子和原文作者的帖子启发。

inline 关键字是否可以促进编译器的内联优化」是一个争议较大的话题。经过查询、讨论和实验,得出以下结论。

  • 大多数情况下,inline 关键字对性能优化没有帮助,但是也不会降低性能。
  • 但是在部分编译环境下,inline 关键字可以明显促进编译器的内联优化,但是也有其他的替代方案。

「部分编译环境」,指包含编译选项 -fPIC、且开启 O2 及等优化的环境,例如洛谷的编译环境。其他 OJ 请查询对应的编译选项说明。

inline 的双重语义

inline 函数具有以下的两种语义:

  • 允许函数在多个编译单元中重定义(不会导致链接错误)。
  • 建议编译器进行内联优化。

实际上,C++ 标准已经不再承认“建议内联优化”的语义,并且实际上,编译器通常也不会听取这个建议。

尽管标准是这么说,但是很多情况下会发现给小函数加上 inline 关键字,仍然可以大幅提升调用速度。

这是有原因的。

-fPIC

根据相关公示,洛谷启用了一个名为 -fPIC 的编译选项。

这个编译选项要求编译器生成「位置无关」的代码来提升安全性,同时使得外部链接的函数(普通函数)可以被运行时替换。这需要通过一种名为 PLT 的机制来支持。

具体来讲,需要先记录对应函数的真实地址,每一次函数调用都需要替换为先获取真实地址,再进行寻址和调用。这也同时让编译器无法进行内联优化

例如以下代码:

auto f(int x) -> int {return x - 1;
}auto g(int x) -> int {return f(x) - 1;
}

在开启 -fPIC 编译选项之后,编译结果如下:

f(int):lea     eax, -1[rdi]ret
g(int):sub     rsp, 8call    f(int)@PLTadd     rsp, 8sub     eax, 1ret

即使是这样简单的函数,也进行了一次函数调用。而没有这个编译选项,编译结果如下:

f(int):lea     eax, [rdi-1]ret
g(int):lea     eax, [rdi-2]ret

这就很符合直觉了,所以我认为,-fPIC 是导致编译器内联优化失效的罪魁祸首。这甚至还会影响到全局变量和数组的访问效率,因为多了一次间接寻址。

解决方案

为了解决这个性能问题,主要的思路是避免全局函数、变量作为外部链接。有几种大体思路:

  • static 函数/变量,仅在当前文件中可见,禁止外部链接。
  • inline 函数,这种函数由于需要允许重定义,属于一种特殊的“弱符号”,也不需要支持相关机制。这里体现的仍然不是 inline 作为“内联建议”的语义。
  • 局部变量,自然不会被链接。但是需要注意,直接包含在命名空间中的变量不属于这个范畴。

具体来讲,可以选择以下几种方案。

显式标注

为全部的全局函数/变量标注 static,函数也可以选择标注 inline。二者效果在开启 O2 优化之后是相同的。

static int a[10];
inline void f() {}
static int g() { return 0; }

匿名命名空间

不同于普通的具名命名空间,匿名命名空间中的所有元素相当于自动添加了一个 static,仅在当前文件可见。

namespace {int a[10];void f() {}
}// 接下来可以直接使用 f(), a[i] 使用,无需特殊语法

成员函数

类的成员函数会自动添加 inline,和显式添加 inline 的函数具有相同效果。

class Solution {int a[100];void f() {}
public:void solve() {}
};int main() {Solution s{};  // 自动清零数组s.solve();
}

lambda 函数

lambda 函数本质上是一个成员函数,也可以解决这个问题。

实践建议

平衡性能和代码简洁性,建议采用“匿名命名空间”方案。

namespace {// 全局变量、数组、函数等,均包裹在匿名命名空间中 int a[10], n;void f() {}void solve() {}
}
int main() {// 在主函数中直接使用solve();
}

如果希望兼顾“多测清空”的需求,全部使用类封装,也是一个不错的选择。

函数内联负优化

有些情况下,可能会发现编译器进行内联优化反而变慢。有两个可能的原因:

评测机波动(较常见)

函数内联带来的优化效果不大,而评测机波动抵消了这部分优化效果。

扰乱代码布局(较少见)

一些情况下,确实可能由于内联优化导致性能损失。参考以下示例:

void err() {// 假设是特别冗长的错误处理代码std::cout << "error" << std::endl;
}int f(int x) {if (x < 0) return x >> 1;else err();return 0;
}

编译结果:

f(int):test    edi, edijs      .L23; 大量的错误处理代码
.L23:mov     eax, edisar     eaxret

此时的 err 函数被内联展开,但是实际上,如果我们每次传入的参数都不会导致出错,那么每一次都会跳过大量的代码块,造成额外开销。编译器也会根据某些特征,来尽可能猜测哪个分支可能是“热分支”。例如按照以下的常见写法,编译器生成的代码就是高效的。

int f(int x) {if (x < 0) return err(), 0;else return x >> 1;
}

编译结果:

f(int):mov     eax, edisar     eaxtest    edi, edijs      .L18ret
.L18:sub     rsp, 8call    err()  ; 正确将 err 分支识别为冷代码xor     eax, eaxadd     rsp, 8ret

这种优化通常较为玄学,以及多数情况下,这种性能差异都是可以接受的。极端卡常场景,可以使用 __builtin_expect 或者 [[likely]] 提示编译器热分支,或者使用 __attribute__((noinline)) 禁用内联优化,减小跳过的代码块大小。

强制内联

可以使用 [[gnu::always_inline]] inline 代替 inline 强制内联。

NOI 系列赛事中的内联函数

NOI 系列赛事中的评测编译选项并没有 -fPIC,因此不需要加 inline

http://www.sczhlp.com/news/4795/

相关文章:

  • 安卓加固脱壳
  • [EC Final 2022] Binary String 题解
  • 清橙基础练习之课程15
  • go学习笔记:log.Fatal的含义和作用是什么?
  • C++高性能:优化代码运行效率的艺术1 C++简介
  • 清橙基础练习之动态规划5
  • 2025.8.3总结 - A
  • Kubernetes v1.33:原地调整 Pod 资源特性升级为 Beta
  • 六爻01
  • Trail of Bits深度参与CSAW网络安全竞赛:五项CTF挑战详解
  • 第二十篇
  • Python入门学习(八)JSON、反序列化、序列化
  • 2025/8/2 总结
  • 7月30日随笔 - 20243867孙堃2405
  • VS Code中如何关闭Github Copilot - 指南
  • 记一次酣畅淋漓的js逆向
  • 7月29日随笔 - 20243867孙堃2405
  • MongoDB指定分片键
  • NSA稀疏注意力深度解析:DeepSeek如何将Transformer复杂度从O(N)降至线性,实现9倍训练加速
  • mc日记
  • flash-attn在消费级显卡上安装环境出现卡住系统的问题/无法安装
  • GraphRAG
  • [python]基于动态实例的命令处理设计
  • 培训时有些人人不会的东西
  • 深入解析:高效轻量的C++ HTTP服务:cpp-httplib使用指南
  • 28天
  • AI给老码农的小小震撼:PB/PE分位计算
  • VS2015+ Qt5.9.1 内嵌CEF 环境配置
  • Atom编辑器离线中文设置
  • 2025牛客暑期多校训练营5 K.Perfect Journey