浅谈 O2 优化
O2 优化是什么?是自动把 O(n^2) 的算法优化为 O(n\log n) 吗?
不是。你想多了。
那么 O2 优化是什么?
简单地讲,O2 优化,就是让编译器会帮助你在编译期间完成一部分计算,简化程序执行的过程,并忽略掉一些没必要的代码。下面举几个例子。,
int sum = 0;
for (int i = 1; i <= 100; ++i)
sum += i;
std::cout << sum << std::endl;
这份代码用于输出
mov DWORD PTR [rbp-4], 0
mov DWORD PTR [rbp-8], 1
.L3:
cmp DWORD PTR [rbp-8], 100
jg .L2
mov eax, DWORD PTR [rbp-8]
add DWORD PTR [rbp-4], eax
add DWORD PTR [rbp-8], 1
jmp .L3
.L2:
mov eax, DWORD PTR [rbp-4]
mov esi, eax
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
代码中含有大量 jmp 命令(jmp,jump 的缩写),说明程序会老老实实地跑
如果开优化,会发生什么呢?
mov esi, 5050
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
直接输出
如果只是计算,不输出,那么无优化时,依旧要跑循环;有 O2 时,代码会被忽略了,不被编译,得到的汇编代码为空。因为编译器开优化之后变得聪明,认为不输出结果的计算是没用的。
还有其他例子吗?
当然有。比如,让我们搜一下,第
int cnt = 0;
for (int i = 0; ; ++i) {
if (is_prime(i)) ++cnt; // is_prime 假设已经在外面写好了
if (cnt == 123) {
std::cout << i << std::endl;
break;
}
}
无优化情况下,编译出的汇编代码比较长,我都没心情看。
// 此处省略 600+ 字符的汇编代码
一旦开了 O2 优化,汇编代码就变短许多:
xor esi, esi
.L11:
add esi, 1
.L12:
add edi, 1
.L13:
cmp edi, 123
jne .L11
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
可以看到,程序根本没有调用 is_prime 函数。这说明聪明的编译器又帮你简化了运行时计算。
同理,当你使用 STL 的时候,如果开启 O2 优化,则程序将简化掉操作,不会严格按照库中的代码实现。文章后面将会举一个例子。
O2 优化在哪些方面能够发挥作用?
几乎所有暴力代码(尤其是嵌套循环的暴力代码)开启 O2 优化之后都会大幅提速。当然,还有 STL。
- 前缀和问题。今年洛谷愚人节月赛第二题就是一道前缀和题目。当时我并没有读出那是前缀和题目,所以打了暴力模拟并手动开启优化——过了。
- Floyd 求最短路问题。洛谷 P1828 是一道最短路题目,一般情况下直接 Floyd 会 T。但开启优化之后可以 AC。
- 逆序对问题。此题正解应当是归并排序 or 树状数组。但开优化之后(可能需要 O3 优化)再暴力嵌套循环是可以 AC 的。
当然,如果只是朴素解法的 A+B,开 O2 优化不会有用。
为什么是 O2,不是 A2 / B2 / C2…… 呢?
“O”,可以理解为英文单词 optimize 的首字母,意为优化。后面的 2,可以理解为优化的等级、强度。因此,不仅有 O2 优化,还有 O、O3、Os、Ofast。其中,O 是我们平常默认的编译选项,基本没用。O3 优化,即是在 O2 优化的基础上,再次提高运行速度。Os 是用来减小生成 exe 的文件大小的。Ofast 与 O3 几乎没有区别。
既然 O2 优化这么厉害,为什么默认编译选项不包含它啊?
其实,并不是所有 O2 优化都是正优化,可能会有负优化。比如,你的代码使用了为初始化的值。编译器并不知道这个值是多少,不知道该如何为你编译。这就是传说中的 UB——未定义行为。在这种情况下,编译器给你编译出来什么东西都有可能。所以,让代码符合规范是重要的。强烈建议在本地开启 -Wall 编译选项。
在无优化情况下,一般编译器会把未初始化的变量赋值为零。但是在有优化情况下,会赋随机值。洛谷讨论区经常有人问“为什么开了 O2 优化 WA,不开 AC?”很多都是未初始化造成的。
轮子哥曾经说过:“你不开 O2,你就能感受到那些抽象带来的效果。”什么意思呢?就是说 O2 优化会对 STL 进行提速,把不必要的、浪费时间的抽象代码都去掉了。比如,你要把一个 vector 清空。如果不开优化,你怎么写,编译器就怎么办。如果开了,就会编译成内存填充的方式。所以工程上你一旦开了 O2 优化,就无法在根本层面进行 Debug。
如果你希望了解更多关于 O2 优化的内容,建议去看看 GCC 的官方文档:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
注:本文所有汇编代码均使用 https://godbolt.org/ 中的 GCC 8.1 生成。