【问题标题】:No performance difference in different variations of the same program同一程序的不同变体中没有性能差异
【发布时间】:2021-03-09 10:45:26
【问题描述】:

我复制了 glibc 的二分搜索算法实现,然后对其进行了一些修改以满足我的需要。我决定测试它和我学到的关于 GCC 的其他东西(属性和内置)。 代码如下:

int main() {
  uint_fast16_t a[61] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61 };
  uint64_t t1 = Time(0);
  for(register uint_fast16_t i = 0; i < 10000000; ++i) {
    binary_search(rand() % 62, a, 61);
  }
  printf("%ld\n", Time(0) - t1);
  return 0;
}

现在,这个程序运行得很好。当我添加更多代码行时,问题就开始了,例如:

uint_fast16_t a[61] __attribute__ ((aligned (64) )) = /* ... */

在这种情况下,我希望代码更快,但经过多次测试(数十次测试)后性能没有改变。 我还测试了 8 和 1 对齐的程序 - 没有变化。我什至预计 gcc 会抛出错误/警告,因为使用的对齐小于类型大小(在我的情况下,64 位机器,uint_fast16_t 是 8 个字节),但没有。 然后是另一个更改,即添加缓存(在 GCC 9 中引入)。我在 for 循环之前添加了以下代码:

caches(a, uint_fast16_t, uint_fast16_t, 61, 0, 3);
// where "caches" is:
#define caches(x, type, data_type, size, rw, l) ({ \
  for(type Q2W0 = 0; Q2W0 < size; Q2W0 += 64 / sizeof(data_type)) { \
    __builtin_prefetch(x + Q2W0, rw, l); \
  } \
})

性能也没有变化。我发现也许我的 CPU 在第一次 binary_search 之后自动缓存了数组,所以我消除了 for 循环并在有和没有缓存线的情况下再次测量了几次,但我也没有注意到性能有任何变化。 更多信息:

  1. 使用 CentOS8 64bit 最新内核
  2. 使用 GCC 9.2.1 20191120
  3. 使用-O3 -Wall -pthread -lanl -Wno-missing-braces -Wmissing-field-initializers 编译,编译期间没有错误/警告
  4. 事情没有被优化掉(检查 asm 输出)

我很确定我不知道某事/我做错了什么。

完整代码here.

【问题讨论】:

  • 您的更改几乎没有提高性能。你确定他们做到了?
  • 仅仅因为您指定了最小对齐方式,这并不意味着对象必须 未对齐。您的main 中有许多“优化”,但是binary_search()rand() 这么快以至于微优化很重要?你应该看看生成的程序集。
  • 这就是重点。为什么他们根本没有改变任何东西?我的意思是,我做了一些事情来提高和降低性能(增加对齐和减少对齐,缓存和不缓存)。也许我所做的改变真的无关紧要,这就是原因。
  • 另外,您的代码已损坏。 10000000 不适合 16 位,因此循环只会结束,因为 uint_fast16_t 大于 uint16_t
  • @FranciszekBalcerak 除了你是谁,你不可能记得一年后你重新访问代码时潜入代码中的所有微妙的、硬编码的废话:)

标签: c unix gcc9


【解决方案1】:
  • register uint_fast16_t 是未成熟的优化,让编译器决定将哪些变量放在寄存器中。将register 视为一个已经过时的关键字。

  • 如 cmets 中所述,uint_fast16_t i = 0; i &lt; 10000000 要么是错误,要么是不好的做法。你或许应该这样做:

    const uint_fast16_t MAX = 10000000; 
    ... i < MAX
    

    在这种情况下,如果值不合适,您应该在初始化时收到编译器错误。或者,使用静态断言检查值。

    更好的是,在这种情况下,使用size_t 作为循环迭代器。

  • __attribute__ ((aligned (64) )) "在这种情况下,我希望代码更快"

    为什么?是什么让你认为数组一开始就错位了?编译器不会仅仅为了变量而错位。特别是当数组成员被声明为 uint_fastnn 时 - 使用 uint_fast16_t 的全部目的实际上是为了获得正确的对齐。

    在这种情况下,数组导致 x86/64 的 gcc 和 clang 都吐出一堆 .quad 汇编程序指令,从而产生完全对齐的数据。

  • 关于缓存命令,我对它们的工作原理知之甚少,无法评论它们。然而,在这种情况下,您可能已经拥有理想的数据缓存性能 - 数组应该在数据缓存中。

    至于指令缓存,它不太可能在二分查找中发挥很大作用,因为其本质上带有大量的分支。在某些情况下,出于这个原因,蛮力线性搜索可能会胜过二分搜索。基准测试并查看。 (当蛮力被证明比二分搜索快得多时,请确保用一个大 O 来打击你的老计算机科学算法老师。)

  • rand() % 62 可能是也可能不是瓶颈。 rand 函数和模数都可能意味着大量开销,具体取决于系统。

【讨论】:

  • 更快的代码,因为内存然后对齐到一个缓存行。
  • @FranciszekBalcerak 我相信在您的 PC 中可能拥有的大多数现代处理器上,缓存行大小为 64 字节。您有 61 个条目。由于您的条目恰好是 8 个字节,即 488 个字节或 8 个缓存行。从理论上讲,对象可能被错误地对齐以跨越 9 个缓存行,而无需强制进行额外的对齐。
猜你喜欢
  • 2013-03-28
  • 2011-02-27
  • 2013-04-03
  • 2023-03-29
  • 2011-10-29
  • 1970-01-01
  • 2013-09-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多