gcc -O3的性能一定优于gcc -O0吗?

1,155 阅读2分钟

博客首发于:www.weeco.tech/9a99f71cc73…

在之前的实验中,我们发现,C语言代码编译出的二进制文件,其执行效率远高于Go语言编译出的二进制文件,其文件大小也小于Go语言编译生成的。

在编译C语言代码的时候,执行的指令是

gcc is_prime.c -O3 -o is_prime -lm

怀疑与编译指令中的O3有关,所有本篇博客我们就对这个参数进行一个探究。

实验环境介绍

虚拟机:host-CPU直通 Linux CentOS 8.2

CPU:Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz

gcc版本:8.5.0-4

g++版本:8.5.0-4

测试所用代码
#include <stdio.h>
#include <stdbool.h>
#include <math.h>bool is_prime(int n) {
    if (n < 2) {
        return false;
    }
    for (int i = 2; i <= sqrt(n); i++) {
        if (n % i == 0) {
            return false;
        }
    }
    return true;
}
​
int main() {
    for (int i = 2; i < 10000000; i++) {
        if (is_prime(i)) {
            printf("%d\n", i);
        }
    }
    return 0;
}
实验设计

分别用如下指令进行程序编译,得到5个不同的二进制文件

gcc is_prime.c -O3 -o is_prime_O3 -lm
gcc is_prime.c -O2 -o is_prime_O2 -lm
gcc is_prime.c -O1 -o is_prime_O1 -lm
gcc is_prime.c -O0 -o is_prime_O0 -lm
gcc is_prime.c -o is_prime -lm

分别统计每个程序的执行时间,可以看出,-O3的性能远高于-O0

/bin/time -vvv ./is_prime_O3 > /dev/null        => 3.19 s
/bin/time -vvv ./is_prime_O2 > /dev/null        => 3.61 s
/bin/time -vvv ./is_prime_O1 > /dev/null        => 4.02 s
/bin/time -vvv ./is_prime_O0 > /dev/null        => 8.73 s
/bin/time -vvv ./is_prime > /dev/null           => 8.75 s

通过md5sum 查看生成的二进制可执行文件,发现is_prime文件和is_prime_O0是一致的,即gcc默认采用了-O0优化

我们直接对比is_prime_O0is_prime_O3这两个程序的性能差异,通过objdump -S指令查看两者汇编代码层的差异

利用BCompare工具,截取了其中一段差异较大的汇编代码,左图为-O3程序的汇编指令,右图为-O0程序的汇编指令

01.png

对比发现,-O3程序采用了一些更高级的汇编指令。

进一步探究,猜测两者的性能差异主要来自于sqrtsd这个汇编指令。因为整个运算中最消耗计算资源的是开方操作。sqrtsd是x86架构的一条汇编指令,用于计算一个双精度浮点数的平方根。为了验证这个想法,我将代码进行了简单改造,将sqrt(n)替换为了>>1,即右移一位。

#include <stdio.h>
#include <stdbool.h>
#include <math.h>bool is_prime(int n) {
    if (n < 2) {
        return false;
    }
    for (int i = 2; i <= n >> 1; i++) {
        if (n % i == 0) {
            return false;
        }
    }
    return true;
}
​
int main() {
    for (int i = 2; i < 1000000; i++) {
        if (is_prime(i)) {
            printf("%d\n", i);
        }
    }
    return 0;
}

测试了一下gcc -O3和gcc -O0的运行时间差异,惊奇地发现,-O3-O0的执行时间完全一致,均为33.46s,也就是从侧面应证了,在上述实验中,性能差异的主要来自于开方操作。

# /usr/bin/time -vvv ./is_prime > /dev/null
        Command being timed: "./is_prime"
        User time (seconds): 33.46
        System time (seconds): 0.00
        Percent of CPU this job got: 99%
        Elapsed (wall clock) time (h:mm:ss or m:ss): 0:33.57
        Average shared text size (kbytes): 0
        Average unshared data size (kbytes): 0
        Average stack size (kbytes): 0
        Average total size (kbytes): 0
        Maximum resident set size (kbytes): 1376
        Average resident set size (kbytes): 0
        Major (requiring I/O) page faults: 0
        Minor (reclaiming a frame) page faults: 57
        Voluntary context switches: 1
        Involuntary context switches: 85
        Swaps: 0
        File system inputs: 0
        File system outputs: 0
        Socket messages sent: 0
        Socket messages received: 0
        Signals delivered: 0
        Page size (bytes): 4096
        Exit status: 0
初步结论

在大多数场景下,gcc -O3gcc -O0有更优的性能。

在某些场景下,例如程序相对简单的时候,gcc没有太多优化空间,此时gcc -O3的效果与gcc -O0的效果完全一致。

下一步计划

C/C++一般都采用动态编译,就需要依赖动态链接库,为什么不采用静态编译呢?两者有咋样的性能差异呢?下次博客我们来探索这个问题~~