同一程序的不同变体没有性能差异
No performance difference in different variations of the same program
我复制了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 抛出一个 error/warning,因为使用的对齐方式小于类型大小(在我的例子中是 64 位机器,uint_fast16_t 是 8 个字节),但是有 none。
然后是另一个变化,即添加缓存(在 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
循环并在有和没有缓存行的情况下再次测量几次,但我没有注意到性能也有任何变化。
更多信息:
- 使用CentOS8 64bit最新内核
- 使用 GCC 9.2.1 20191120
- 使用
-O3 -Wall -pthread -lanl -Wno-missing-braces -Wmissing-field-initializers
编译,编译期间没有错误/警告
- 事情没有被优化掉(检查 asm 输出)
我很确定我不知道某事/我做错了什么。
完整代码可用 here。
register uint_fast16_t
是过早的优化,让编译器决定哪些变量放在寄存器中。将 register
视为几乎已过时的关键字。
如评论中所述,uint_fast16_t i = 0; i < 10000000
要么是错误,要么是不良做法。你也许应该做这样的事情:
const uint_fast16_t MAX = 10000000;
... i < MAX
在这种情况下,如果值不合适,您应该会在初始化时遇到编译器错误。或者,使用静态断言检查值。
更好的是,在这种情况下使用 size_t
作为循环迭代器。
-
__attribute__ ((aligned (64) ))
"In this case I would expect faster code"
为什么?是什么让您认为阵列一开始就错位了?编译器不会仅仅为了它而错位变量。特别是当数组成员声明为 uint_fastnn
时 - 使用 uint_fast16_t
的全部意义实际上是为了获得正确的对齐方式。
在这种情况下,数组导致 x86/64 的 gcc 和 clang 喷出一堆 .quad
汇编程序指令,从而产生完美对齐的数据。
关于缓存命令,我对它们的工作原理知之甚少,无法发表评论。但是,在这种情况下,您可能已经拥有理想的数据缓存性能 - 数组应该在数据缓存中。
至于指令缓存,它不太可能在二分查找期间发挥多大作用,二分查找本质上带有大量分支。出于这个原因,在某些情况下,强力线性搜索可能会胜过二分搜索。基准并查看。 (当事实证明蛮力比二进制搜索快得多时,一定要用大 O 重击你的老计算机科学算法老师。)
rand() % 62
可能是瓶颈,也可能不是。 rand 函数和模数都可能意味着大量开销,具体取决于系统。
我复制了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 抛出一个 error/warning,因为使用的对齐方式小于类型大小(在我的例子中是 64 位机器,uint_fast16_t 是 8 个字节),但是有 none。 然后是另一个变化,即添加缓存(在 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
循环并在有和没有缓存行的情况下再次测量几次,但我没有注意到性能也有任何变化。
更多信息:
- 使用CentOS8 64bit最新内核
- 使用 GCC 9.2.1 20191120
- 使用
-O3 -Wall -pthread -lanl -Wno-missing-braces -Wmissing-field-initializers
编译,编译期间没有错误/警告 - 事情没有被优化掉(检查 asm 输出)
我很确定我不知道某事/我做错了什么。
完整代码可用 here。
register uint_fast16_t
是过早的优化,让编译器决定哪些变量放在寄存器中。将register
视为几乎已过时的关键字。如评论中所述,
uint_fast16_t i = 0; i < 10000000
要么是错误,要么是不良做法。你也许应该做这样的事情:const uint_fast16_t MAX = 10000000; ... i < MAX
在这种情况下,如果值不合适,您应该会在初始化时遇到编译器错误。或者,使用静态断言检查值。
更好的是,在这种情况下使用
size_t
作为循环迭代器。-
__attribute__ ((aligned (64) ))
"In this case I would expect faster code"为什么?是什么让您认为阵列一开始就错位了?编译器不会仅仅为了它而错位变量。特别是当数组成员声明为
uint_fastnn
时 - 使用uint_fast16_t
的全部意义实际上是为了获得正确的对齐方式。在这种情况下,数组导致 x86/64 的 gcc 和 clang 喷出一堆
.quad
汇编程序指令,从而产生完美对齐的数据。 关于缓存命令,我对它们的工作原理知之甚少,无法发表评论。但是,在这种情况下,您可能已经拥有理想的数据缓存性能 - 数组应该在数据缓存中。
至于指令缓存,它不太可能在二分查找期间发挥多大作用,二分查找本质上带有大量分支。出于这个原因,在某些情况下,强力线性搜索可能会胜过二分搜索。基准并查看。 (当事实证明蛮力比二进制搜索快得多时,一定要用大 O 重击你的老计算机科学算法老师。)
rand() % 62
可能是瓶颈,也可能不是。 rand 函数和模数都可能意味着大量开销,具体取决于系统。