Skip to content

Commit 8d1e17a

Browse files
feat: chapter 2 updates
1 parent 2786dd6 commit 8d1e17a

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

File renamed without changes.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# 嵌入式现代 C++教程——内联函数与编译器优化
2+
3+
在嵌入式开发中,`inline` 这个关键字几乎是每个工程师都会用到的东西。它看起来简单直接,甚至带着一点“性能保证”的意味:函数短、调用频繁、对时序敏感,那就 `inline` 掉,似乎天经地义。
4+
5+
对,在过去,inline这个关键字的确是这个作用,但是实际上,在现代C++的今天,在编译器的优化做的非常出色的现在,**`inline` 并不是性能优化的按钮,它甚至经常什么都没做。**
6+
7+
## inline 从一开始,就不是为“快”而生的
8+
9+
从语言层面来说,`inline` 的核心作用其实非常克制。现代的 C++ 标准并没有承诺“你写了 inline,编译器就一定展开函数体”。它真正保证的事情只有一件:**允许该函数在多个翻译单元中出现定义,而不违反 ODR。**
10+
11+
这也是为什么大量头文件中的小函数、模板函数、traits 工具函数,天然就带着 `inline` 的气质。它解决的是“链接层面的问题”,而不是“性能问题”。至于是否真的发生内联,那完全是编译器的自由裁量权。在现代编译器面前,`inline` 更像是一句“我觉得你可以考虑展开”的建议,而不是命令。
12+
13+
------
14+
15+
## 那嵌入式里,函数调用真的慢吗?
16+
17+
很多关于 inline 的直觉,来自于对函数调用成本的恐惧。在 Cortex-M 这类架构上,一次函数调用确实意味着跳转、LR 保存、参数传递和返回路径恢复。你如果盯着汇编一条条看,很容易得出结论:这看起来不便宜。
18+
19+
问题在于,这个成本**是否真的落在了你的性能瓶颈路径上**
20+
21+
在真实的嵌入式工程中,大量函数的时间消耗根本不在“调用本身”,而是在外设访问、总线等待、Flash 读取、Cache miss,甚至是中断抢占上。你为一个 GPIO 读取函数纠结要不要 inline,往往是在一个完全不重要的层级上做优化。
22+
23+
更关键的是,在开启优化(哪怕只是 `-O2`)之后,这类短小、无副作用、语义明确的函数,**即使你不写 inline,编译器也几乎必然会自动内联**
24+
25+
今天的编译器在决定是否内联一个函数时,会综合考虑函数体大小、调用点数量、寄存器压力、指令 Cache 行为,甚至在开启 LTO 后跨文件分析调用关系。它掌握的信息,远比你在写代码时看到的那一点上下文要多。这也是为什么你经常会看到这样一种情况:你显式写了 `inline`,反汇编却发现函数依然存在;而你什么都没写,函数却悄无声息地被展开了。
26+
27+
28+
29+
------
30+
31+
## 展开成调用的inline其实不是很安全
32+
33+
如果说 PC 开发中,inline 最大的风险是“无感”,那么在嵌入式中,它真正的风险往往是**代码体积膨胀**
34+
35+
内联的本质是复制。一个被频繁调用的小函数,如果在多个位置被展开,指令会被实实在在地复制多份。在 Flash 资源紧张的 MCU 上,这种复制是不可忽视的。更微妙的一点在于,代码变大不只是占 Flash,它还会影响指令 Cache 的局部性。即使在有 I-Cache 的内核上,过度内联也可能导致更多 Cache miss,最终表现为性能下降,而不是提升。
36+
37+
------
38+
39+
## 那 inline 什么时候才真正有价值?
40+
41+
在实践中,inline 真正体现价值的场景,往往不是“为了省掉一次函数调用”,而是为了**消除抽象边界的成本**
42+
43+
例如模板函数、类型安全的寄存器访问封装、constexpr 参与的编译期计算。这些地方的 inline,使得你可以写出表达力极强的 C++ 代码,同时在生成的汇编层面,几乎和手写 C 没有区别。
44+
45+
这正是现代 C++ 在嵌入式领域最迷人的地方:**抽象不是负担,而是可以被完全优化掉的语义工具。**
46+
47+
在中断服务函数或极端热路径中,inline 也可能是合理的,但前提永远只有一个:你真的看过汇编,并且确认它解决了实际问题。
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# 嵌入式现代 C++教程——constexpr:把计算推到编译期
2+
3+
在嵌入式开发中,我们习惯于用“时间”和“空间”来衡量一段代码的好坏。函数调用要不要内联,循环能不能展开,RAM 和 Flash 有没有被浪费。这些问题都很直观,也很工程。
4+
5+
constexpr这个是一个真正的好东西——**它不优化运行时,而是干脆让运行时什么都不用做。**这正是 `constexpr` 在嵌入式现代 C++ 中最有价值、也最容易被低估的地方。
6+
7+
------
8+
9+
## constexpr 解决的不是“快不快”,而是“还需不需要算”
10+
11+
很多人第一次接触 `constexpr`,是从这种例子开始的:
12+
13+
```cpp
14+
constexpr int square(int x)
15+
{
16+
return x * x;
17+
}
18+
```
19+
20+
然后编译器把 `square(8)` 直接替换成 `64`,看起来很神奇。
21+
22+
但如果只停留在这个层面,很容易把 `constexpr` 理解成一种“更激进的 inline”。这是一个常见但并不准确的认识。
23+
24+
`inline` 的核心问题是:**要不要在运行时展开函数体**。而 `constexpr` 关注的是:**这个计算是否还有必要存在于运行时**。一旦你意识到这个差异,就会发现它们解决的是完全不同的问题。
25+
26+
------
27+
28+
## 在嵌入式里,运行时并不是免费的
29+
30+
在 PC 上,多一次整数乘法、多一次分支判断,几乎可以忽略不计。但在嵌入式系统中,运行时往往意味着更多的东西——可能是 Flash 中的指令读取,可能是 Cache miss,可能是启动阶段的额外执行路径,甚至可能影响到中断响应的确定性。很多嵌入式代码中都存在这样一种“无意识的运行时计算”:常量明明在编译期就已经确定,却依然被写成了运行时表达式。
31+
32+
例如外设寄存器位掩码、数组大小、查表索引、协议字段偏移。这些值在系统启动后永远不会改变,但却被一次次地计算、判断、组合。`constexpr` 的出现,正是为了让这些“本不该存在于运行时的计算”,彻底消失。
33+
34+
------
35+
36+
## constexpr 的本质,是一种“更强的常量语义”
37+
38+
在嵌入式 C 时代,我们已经非常熟悉 `#define` 和 `enum`。它们同样可以在编译期得到结果,也同样不会引入运行时开销。
39+
40+
但它们的问题在于:**缺乏类型、缺乏作用域、缺乏表达能力**。
41+
42+
`constexpr` 并不是为了取代它们,而是提供了一种更现代、更安全的方式,把“这是一个编译期就能确定的事实”明确地告诉编译器。当你把一个函数、一个构造函数、一个变量声明为 `constexpr`,你其实是在表达一种强约束:**如果它不能在编译期完成,那它就不该存在。**这种约束一旦成立,编译器就会毫不犹豫地把结果直接写进目标文件。
43+
44+
------
45+
46+
## 嵌入式中,constexpr 最自然的落脚点
47+
48+
在真实工程中,`constexpr` 很少是“炫技式”的。它更多是悄悄地改善系统结构。
49+
50+
- 例如寄存器地址和位定义。过去我们用宏,现在我们可以用 `constexpr` 常量,让类型系统参与进来。
51+
- 例如静态查找表的生成,编译期完成初始化,运行时只做索引。
52+
- 例如协议相关的固定字段、偏移量、长度计算,在链接前就已经完全确定。
53+
54+
这些地方使用 `constexpr`,几乎不会改变你代码的阅读方式,却能实实在在地减少运行时代码。
55+
56+
------
57+
58+
## constexpr 和嵌入式启动阶段
59+
60+
在很多嵌入式系统中,启动阶段是一个被高度关注、但又容易被忽略细节的地方。初始化代码往往集中、复杂,而且执行一次就决定了系统之后的状态。如果这些初始化中包含大量本可以在编译期完成的计算,那么启动时间、代码体积和可验证性都会受到影响。`constexpr` 在这里的意义,并不只是“快一点”,而是**让启动路径更简单、更确定**。当某些值已经被固化进二进制文件时,你甚至不需要再关心它们是怎么被算出来的。
61+
62+
------
63+
64+
## constexpr 并不是“写了就一定发生”
65+
66+
和 inline 类似,`constexpr` 也经常被误解为一种“强制指令”。事实上,它更像是一种契约。
67+
68+
你告诉编译器:这段逻辑**应该**可以在编译期完成。如果做不到,编译器要么拒绝编译,要么退化为运行时求值(取决于上下文)。这反而是一件好事。在嵌入式开发中,编译期失败往往比运行时问题要友好得多。而且越高版本的C++,越能支持更多行为的编译期计算。
69+
70+
71+

0 commit comments

Comments
 (0)