惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
WordPress大学
WordPress大学
Vercel News
Vercel News
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
I
InfoQ
小众软件
小众软件
Recent Announcements
Recent Announcements
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
大猫的无限游戏
大猫的无限游戏
美团技术团队
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
MongoDB | Blog
MongoDB | Blog
V
V2EX
J
Java Code Geeks
有赞技术团队
有赞技术团队
博客园 - 聂微东
B
Blog RSS Feed
博客园 - 司徒正美

Watermelonabc的Blog

又一个黑客模拟游戏 又一个黑客模拟游戏 家庭辅导一晚,有点感想 家庭辅导一晚,有点感想 福州大学单片机原理与应用(跨学科)复习 福州大学单片机原理与应用(跨学科)复习 福州大学毛概期末复习 福州大学毛概期末复习 x86 汇编语言详解 - 2 x86 汇编语言详解 -2 CCB 2026 Half-Final Guangzhou CCB 2026 Half-Final Guangzhou 视频也可以含恶意代码吗 视频也可以含恶意代码吗 Hello,2026 Hello,2026 2025 福州大学算法与数据结构复习 2025 福州大学算法与数据结构复习 2025 福州大学习概期末复习 2025 福州大学习概期末复习 Frida 入门 Frida 入门 记一场贴吧交流 记一场贴吧交流 虚拟机保护 虚拟机保护 2025 强网杯 S9 Reverse 复现 2025 强网杯 S9 Reverse 复现 你如何在 FZU 取到快递 你如何在 FZU 取到快递
编译器为我做了什么
Watermelonabc · 2025-07-19 · via Watermelonabc的Blog

编译器为我做了什么

Backstory

考虑一下不同代码写法会不会影响性能?例如:

写法 A

1
2
3
4
5
6
7
8
9
int sum(const std::vector<int> &v)
{
int result = 0;
for(size_t i = 0; i < v.size(); ++i)
{
result += v[i];
}
return result;
}

写法 B

1
2
3
4
5
6
7
8
9
int sum(const std::vector<int> &v)
{
int result = 0;
for(int x : v)
{
result += x;
}
return result;
}

这两种写法在功能上等效。但如果想真正确认哪一种在性能上取胜,则需要使用基准测试 (benchmark)。在这里,写法 B 的性能更好。

与此同时,我们还想知道自己能否承担这种性能提升的代价,这时需要查看编译器输出的汇编结果了。

不要只看汇编结果。我们看不完那么多汇编指令,而且这还是经过编译器优化修改过的产物。如果需要比较,请一并做好基准测试。

x86_64 Assembly 101

寄存器

x86_64 架构下的寄存器有:

  • rax, rbx, rcx, rdx, rsp, rbp, rsi, rdi, r8-r15
  • xmm0-xmm15
  • rdi, rsi, rdx 等用来存储变量
  • rax 用于存储返回值

在 x86_64 架构中,rax 的长度是 64 位,eax 的长度是 32 位。当数据写入 eax 中时,实际上是将 rax 的高位的 32 个位清 0,仅在低位的 32 个位写入数据。这是一个寄存器相关的技巧。

指令格式

我们使用 Intel 语法格式:

1
2
3
4
op
op dest
op dest, src
op dest, src1, src2

这里的 op 指代任意汇编指令,比如 mov, calldestsrc 是寄存器或者内存指针,使用 “基址 + 偏移” 表示,即 base + [reg1] + [reg2 * (1, 2, 4 or 8)]

汇编指令与伪 C 代码的对照

汇编

1
2
3
4
5
6
mov eax, DWORD PTR [r14]
add rax, rdi
add eax, DWORD PTR [r14+4]
sub eax, DWORD PTR [r14+4*rbx]
lea rax, [r14+4*rbx]
xor edx, edx

伪 C 代码

1
2
3
4
5
6
int eax = *r14 
rax += rdi;
eax += r14[1];
eax -= r14[rbx];
int *rax = &r14[rbx];
edx = 0;

有人会将 edx = 0 肉编译为 mov edx, 0,但这里却是用 xor edx, edx。有两个原因:1)mov 中使用的 0 必须以 4 字节存储,体积要大得多;2)指令集已经明白 xor 有清 0 的作用,因此进行对应优化 —— 换句话说,xor 更偏向 “重置”。

Compiler Explorer

最初的 Compiler Explorer 是一个本地 shell 脚本,主要功能部分长这样:

1
2
3
g++ /tmp/test.cc -O2 -c -S -o --masm=intel \
| c++filt \
| grep -vE '\s+\.'

然后用 Unix 工具 watch 持续运行脚本,得到近实时编译结果:

1
2
3
4
5
6
7
8
sum(std::vector<int, std::allocator<int> > const&):
.LFB786:
mov rcx, QWORD PTR [rdi]
mov rax, QWORD PTR 8[rdi]
sub rax, rcx
shr rax, 2
mov rsi, rax

后面部署到线上,有了 GUI,可以在 Compiler Explorer 上探索。

通过示例,我们可以清晰地看到写法 A 和写法 B 的汇编区别:写法 B 的汇编明显更短。

本文的示例使用 x86-64 gcc 15.1 -O2 编译这些函数,而原作者使用 x86-64 gcc 8.1 编译,得到的结果会有差异。

通过引入 numeric 库,我们还有写法 C:

1
2
3
4
int sum(const std::vector<int> &v)
{
return accumulate(begin(v), end(v), 0);
}

写法 C 的汇编几乎和 B 相同,只有两个指令的差异 —— 不过是位置的变化罢了。