【问题标题】:Making a C code run faster使 C 代码运行得更快
【发布时间】:2012-01-07 15:52:01
【问题描述】:

我写了一段代码,用于计算 0 到 255 之间数字的频率。

unsigned char arr[4096]; //aligned 64 bytes, filled with random characters

short counter[256]; //aligned 32 bytes

register int i;

for(i = 0; i < 4096; i++)
    ++counter[arr[i]];

执行需要很长时间;对计数器数组的随机访问非常昂贵。

有没有人有任何想法可以用来使访问顺序或我可以使用的任何其他方法?

【问题讨论】:

  • 仅供参考:register 关键字实际上被现代编译器忽略了,counter[arr[i]]++ 更具可读性(生成的代码没有区别)。
  • @randy7 - 我编辑了你的 for 循环,但是你未编辑它。有意义吗?
  • @randy7:无论如何,它本来就是一个寄存器。检查程序集。
  • @Klee1:您确实拥有counter 的空间局部性,因为它只有 512 字节,因此很容易放入 L1 缓存中。
  • 执行时间长?此代码运行时间基本上为零。你如何衡量“很多时间”?

标签: c performance optimization


【解决方案1】:

是什么让您认为对计数器数组的随机访问很昂贵?你有简介吗?试试 Valgrind,它有一个名为“cachegrind”的缓存分析工具。分析还可以让您知道代码是否真的很慢,或者您是否只是认为它很慢,因为它应该是。

这是一段非常简单的代码,在优化之前重要的是要知道它是受内存限制还是不受内存限制(w.r.t. 数据,而不是直方图)。我无法从头顶回答这个问题。尝试与仅对整个输入求和的简单算法进行比较:如果两者以大致相同的速度运行,那么您的算法受内存限制,您就完成了。

我的最佳猜测是,可能会减慢您速度的主要问题是:

   Registers                      RAM
1.  <-- read data[i] ---------------
2.  <-- read histogram[data[i]] ----
3. increment
4.  --- write histogram[data[i]] -->
5.  <-- read data[i] ---------------
6.  <-- read histogram[data[i]] ----

编译器和处理器不允许重新排序这里的大部分指令(除了 #1 和 #5,它们可以提前完成),因此您基本上会受到较小者的限制:您的带宽L1 缓存(直方图所在的位置)和主 RAM 的带宽,每个都乘以某个未知的常数因子。 (注意:如果展开循环,编译器只能移动 #1/5,但处理器可能无论如何都可以移动它。)

这就是为什么您在尝试变得聪明之前先进行分析 - 因为如果您的 L1 缓存有足够的带宽,那么您将永远渴望数据并且您无能为力。

脚注:

这段代码:

register int i;
for(i = 0; i < 4096; i++)
    ++counter[arr[i]];

生成与此代码相同的程序集:

int i;
for(i = 0; i < 4096; i++)
    counter[arr[i]]++;

但是这段代码更容易阅读。

【讨论】:

  • 嗯,有趣的问题是编译器是否能够确保没有别名问题(对于发布的代码应该是微不足道的),在这种情况下,他可以并行完成几乎完整的代码。所以我会检查 gcc 是否合理地对代码进行矢量化(有一些选项可以显示哪些循环被矢量化) - 如果不是,那可能是性能损失
  • @Voo:我怀疑这段代码是否可以手动矢量化,更不用说自动矢量化了,而且如果标量代码受内存限制,这并不重要。
【解决方案2】:

更惯用的:

// make sure you actually fill this with random chars
// if this is declared in a function, it _might_ have stack garbage
// if it's declared globally, it will be zeroed (which makes for a boring result)
unsigned char arr[4096]; 
// since you're counting bytes in an array, the array can't have more
// bytes than the current system memory width, so then size_t will never overflow
// for this usage
size_t counter[256];

for(size_t i = 0; i < sizeof(arr)/sizeof(*arr); ++i)
    ++counter[arr[i]];

现在的关键是用 C99 编译,还有一些严重的优化标志:

cc mycode.c -O3 -std=c99

对这样一个简单的循环进行任何优化都会使其非常快速。 不要将更多时间浪费在更快地完成如此简单的事情上。

【讨论】:

  • short 可能比size_t 更适合缓存。更好的是,int_least16_t
  • 在这个例子中不太可能有太大的不同。在大多数系统上它将是 1-2K。
【解决方案3】:

首先,我完全同意 Dietrich,请首先证明(您和我们)真正的瓶颈在哪里。

我能看到的唯一可能的改进是您的short。我猜这里的桌子大小不会是问题,但是促销和溢出。使用默认处理此问题的类型,即unsigned

无论如何,计数器应该总是unsigned(更好的是size_t),这是基数的语义。作为一个额外的优势,无符号类型不会溢出,而是以受控方式包装一个环绕。编译器不必为此使用额外的指令。

然后在 C 中进行算术运算,其宽度至少为 int。然后必须将其转换回short。

【讨论】:

    【解决方案4】:

    代码获取大小为 4k 的数据...它每 3 个连续字节相加,并将结果存储在大小为 4k 的临时缓冲区中。临时缓冲区用于生成直方图。

    向量化可以添加我使用 SIMD 指令执行的 3 个连续字节。

    按照 Dietrich 的建议,如果我不生成直方图,而是简单地将值添加到临时缓冲区中,那么它的执行速度非常快。但是直方图的生成是需要时间的部分。我使用缓存研磨对代码进行了分析...输出是:

    ==11845== 
    ==11845== I   refs:      212,171
    ==11845== I1  misses:        842
    ==11845== LLi misses:        827
    ==11845== I1  miss rate:    0.39%
    ==11845== LLi miss rate:    0.38%
    ==11845== 
    ==11845== D   refs:       69,179  (56,158 rd   + 13,021 wr)
    ==11845== D1  misses:      2,905  ( 2,289 rd   +    616 wr)
    ==11845== LLd misses:      2,470  ( 1,895 rd   +    575 wr)
    ==11845== D1  miss rate:     4.1% (   4.0%     +    4.7%  )
    ==11845== LLd miss rate:     3.5% (   3.3%     +    4.4%  )
    ==11845== 
    ==11845== LL refs:         3,747  ( 3,131 rd   +    616 wr)
    ==11845== LL misses:       3,297  ( 2,722 rd   +    575 wr)
    ==11845== LL miss rate:      1.1% (   1.0%     +    4.4%  )
    

    完整的输出是:

    I1 cache:         65536 B, 64 B, 2-way associative
    D1 cache:         65536 B, 64 B, 2-way associative
    LL cache:         1048576 B, 64 B, 16-way associative
    Command:          ./a.out
    Data file:        cachegrind.out.11845
    Events recorded:  Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
    Events shown:     Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
    Event sort order: Ir I1mr ILmr Dr D1mr DLmr Dw D1mw DLmw
    Thresholds:       0.1 100 100 100 100 100 100 100 100
    Include dirs:     
    User annotated:   
    Auto-annotation:  off
    
    --------------------------------------------------------------------------------
         Ir I1mr ILmr     Dr  D1mr  DLmr     Dw D1mw DLmw 
    --------------------------------------------------------------------------------
    212,171  842  827 56,158 2,289 1,895 13,021  616  575  PROGRAM TOTALS
    
    --------------------------------------------------------------------------------
        Ir I1mr ILmr     Dr  D1mr  DLmr     Dw D1mw DLmw  file:function
    --------------------------------------------------------------------------------
    97,335  651  642 26,648 1,295 1,030 10,883  517  479  ???:???
    59,413   13   13 13,348   886   829     17    1    0  ???:_dl_addr
    40,023    7    7 12,405    10     8    223   18   17  ???:core_get_signature
     5,123    2    2  1,277    64    19    256   64   64  ???:core_get_signature_parallel
     3,039   46   44    862     9     4    665    8    8  ???:vfprintf
     2,344   11   11    407     0     0    254    1    1  ???:_IO_file_xsputn
       887    7    7    234     0     0    134    1    0  ???:_IO_file_overflow
       720    9    7    250     5     2    150    0    0  ???:__printf_chk
       538    4    4    104     0     0    102    2    2  ???:__libc_memalign
       507    6    6    145     0     0    114    0    0  ???:_IO_do_write
       478    2    2     42     1     1      0    0    0  ???:strchrnul
       350    3    3     80     0     0     50    0    0  ???:_IO_file_write
       297    4    4     98     0     0     23    0    0  ???:_IO_default_xsputn
    

    【讨论】:

    • 计数器然后用于排序和获取前 8 个最经常出现的值。一个有趣的观察是:如果我禁用排序功能,直方图的生成执行得非常快(0 - 1 us - 使用 gettimeofday 函数)但是如果我启用排序,则生成直方图的时间会跳到 22 us。我相信 cpu 正在将数据从缓存写回内存,这需要时间。
    【解决方案5】:

    嗯,理查德当然是对的。这是因为编译器必须将数组转换为指针,但这需要一些时间,从而增加了执行时间。例如,试试这个:

    for(i = 0; i < 4096; i++)
         ++*(counter+*(arr+i));
    

    【讨论】:

    • C 没有指定任何执行时间。
    【解决方案6】:

    考虑使用指向 arr 的指针,而不是索引。

    unsigned char p = &arr;
    for (i = 4096-1; 0 <= i; --i)
      ++counter[*p++];
    

    【讨论】:

    • 更糟糕的是,char* 允许为任何内容设置别名,因此您可能会使编译器更难证明 *p 不会为其他数据设置别名,即使是其他类型的数据。还有一个错字,您将p 声明为char 而不是char *。所有这些因素加起来就是我不会理会的反对票。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-18
    • 1970-01-01
    • 1970-01-01
    • 2020-12-20
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多