首页 > 解决方案 > 为什么在启用 GCC 优化的情况下,这段代码使用 strlen 的速度要慢 6.5 倍?

问题描述

出于某种原因,我想对glibcstrlen功能进行基准测试,发现在 GCC 中启用优化后它的性能显然慢得多,我不知道为什么。

这是我的代码:

#include <time.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>

int main() {
    char *s = calloc(1 << 20, 1);
    memset(s, 65, 1000000);
    clock_t start = clock();
    for (int i = 0; i < 128; ++i) {
        s[strlen(s)] = 'A';
    }
    clock_t end = clock();
    printf("%lld\n", (long long)(end - start));
    return 0;
}

在我的机器上它输出:

$ gcc test.c && ./a.out
13336
$ gcc -O1 test.c && ./a.out
199004
$ gcc -O2 test.c && ./a.out
83415
$ gcc -O3 test.c && ./a.out
83415

不知何故,启用优化会使其执行时间更长。

标签: cperformancegccglibc

解决方案


在Godbolt 的 Compiler Explorer上测试您的代码提供了以下解释:

  • 无论是否优化,生成的-O0代码都会调用 C 库函数strlen
  • -O1生成的代码中,使用一条rep scasb指令进行简单的内联扩展;
  • at-O2及以上,生成的代码使用更精细的内联扩展。

反复对代码进行基准测试显示从一次运行到另一次运行存在很大差异,但增加迭代次数表明:

  • -O1代码比 C 库实现慢得多:32240vs3090
  • -O2代码比 C 库代码快,-O1但仍比 C 库代码慢得多:8570vs 3090

此行为特定于gccGNU libc。在 OS/Xclang和 Apple 的 Libc 上进行的相同测试没有显示出显着差异,这并不奇怪,因为 Godbolt 显示在所有优化级别都会clang调用 C 库。strlen

这可能被认为是 gcc/glibc 中的一个错误,但更广泛的基准测试可能表明调用的开销strlen比小字符串的内联代码缺乏性能具有更重要的影响。基准中的字符串非常大,因此将基准集中在超长字符串上可能不会产生有意义的结果。

我改进了这个基准并测试了各种字符串长度。从在 Intel(R) Core(TM) i3-2100 CPU @ 3.10GHz 上运行 gcc (Debian 4.7.2-5) 4.7.2 的 linux 基准测试中,生成的内联代码-O1总是较慢,由 as对于中等长度的字符串,它是10倍,而对于非常短的字符串,-O2它只比 libc 快一点,对于较长的字符串,它的速度只有strlen一半。从这些数据来看,GNU C 库版本strlen对于大多数字符串长度都非常有效,至少在我的特定硬件上是这样。还要记住,缓存对基准测量有重大影响。

这是更新的代码:

#include <stdlib.h>
#include <string.h>
#include <time.h>

void benchmark(int repeat, int minlen, int maxlen) {
    char *s = malloc(maxlen + 1);
    memset(s, 'A', minlen);
    long long bytes = 0, calls = 0;
    clock_t clk = clock();
    for (int n = 0; n < repeat; n++) {
        for (int i = minlen; i < maxlen; ++i) {
            bytes += i + 1;
            calls += 1;
            s[i] = '\0';
            s[strlen(s)] = 'A';
        }
    }
    clk = clock() - clk;
    free(s);
    double avglen = (minlen + maxlen - 1) / 2.0;
    double ns = (double)clk * 1e9 / CLOCKS_PER_SEC;
    printf("average length %7.0f -> avg time: %7.3f ns/byte, %7.3f ns/call\n",
           avglen, ns / bytes, ns / calls);
}

int main() {
    benchmark(10000000, 0, 1);
    benchmark(1000000, 0, 10);
    benchmark(1000000, 5, 15);
    benchmark(100000, 0, 100);
    benchmark(100000, 50, 150);
    benchmark(10000, 0, 1000);
    benchmark(10000, 500, 1500);
    benchmark(1000, 0, 10000);
    benchmark(1000, 5000, 15000);
    benchmark(100, 1000000 - 50, 1000000 + 50);
    return 0;
}

这是输出:

chqrlie> gcc -std=c99 -O0 benchstrlen.c && ./a.out
平均长度 0 -> 平均时间:14.000 ns/字节,14.000 ns/调用
平均长度 4 -> 平均时间:2.364 ns/字节,13.000 ns/调用
平均长度 10 -> 平均时间:1.238 ns/字节,13.000 ns/调用
平均长度 50 -> 平均时间:0.317 ns/字节,16.000 ns/调用
平均长度 100 -> 平均时间:0.169 ns/字节,17.000 ns/调用
平均长度 500 -> 平均时间:0.074 ns/字节,37.000 ns/调用
平均长度 1000 -> 平均时间:0.068 ns/字节,68.000 ns/调用
平均长度 5000 -> 平均时间:0.064 ns/字节,318.000 ns/调用
平均长度 10000 -> 平均时间:0.062 ns/字节,622.000 ns/调用
平均长度 1000000 -> 平均时间:0.062 ns/字节,62000.000 ns/调用
chqrlie> gcc -std=c99 -O1 benchstrlen.c && ./a.out
平均长度 0 -> 平均时间:20.000 ns/字节,20.000 ns/调用
平均长度 4 -> 平均时间:3.818 ns/字节,21.000 ns/调用
平均长度 10 -> 平均时间:2.190 ns/字节,23.000 ns/调用
平均长度 50 -> 平均时间:0.990 ns/字节,50.000 ns/调用
平均长度 100 -> 平均时间:0.816 ns/字节,82.000 ns/调用
平均长度 500 -> 平均时间:0.679 ns/字节,340.000 ns/调用
平均长度 1000 -> 平均时间:0.664 ns/字节,664.000 ns/调用
平均长度 5000 -> 平均时间:0.651 ns/字节,3254.000 ns/调用
平均长度 10000 -> 平均时间:0.649 ns/字节,6491.000 ns/调用
平均长度 1000000 -> 平均时间:0.648 ns/字节,648000.000 ns/调用
chqrlie> gcc -std=c99 -O2 benchstrlen.c && ./a.out
平均长度 0 -> 平均时间:10.000 ns/字节,10.000 ns/调用
平均长度 4 -> 平均时间:2.000 ns/字节,11.000 ns/调用
平均长度 10 -> 平均时间:1.048 ns/字节,11.000 ns/调用
平均长度 50 -> 平均时间:0.337 ns/字节,17.000 ns/调用
平均长度 100 -> 平均时间:0.299 ns/字节,30.000 ns/调用
平均长度 500 -> 平均时间:0.202 ns/字节,101.000 ns/调用
平均长度 1000 -> 平均时间:0.188 ns/字节,188.000 ns/调用
平均长度 5000 -> 平均时间:0.174 ns/字节,868.000 ns/调用
平均长度 10000 -> 平均时间:0.172 ns/字节,1716.000 ns/调用
平均长度 1000000 -> 平均时间:0.172 ns/字节,172000.000 ns/调用

推荐阅读