我会期望对一个char *
缓冲区的写入,在内存中原有内容的差异不会影响它们的运行时间1。你也是这样认为吗?
然而,当我在追查基准测试中的一致性问题时,我发现了一个看似不成立的案例。一个全部为零的缓冲区在性能上与一个填充了42
的缓冲区有很大的差别。
图形化地表示,如下所示(细节请参阅以下内容):
以下是我用于生成上述结果的代码3:
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <string.h>
#include <time.h>
volatile char *sink;
void process(char *buf, size_t len) {
clock_t start = clock();
for (size_t i = 0; i < len; i += 678)
buf[i] = 'z';
printf("Processing took %lu μs\n",
1000000UL * (clock() - start) / CLOCKS_PER_SEC);
sink = buf;
}
int main(int argc, char** argv) {
int total = 0;
int memset42 = argc > 1 && !strcmp(argv[1], "42");
for (int i=0; i < 5; i++) {
char *buf = (char *)malloc(BUF_SIZE);
if (memset42)
memset(buf, 42, BUF_SIZE);
else
memset(buf, 0, BUF_SIZE);
process(buf, BUF_SIZE);
}
return EXIT_SUCCESS;
}
我在我的Linux系统上编译它,如下:
gcc -O2 buffer_weirdness.cpp -o buffer_weirdness
...当我运行零缓冲版本时,我得到:
./buffer_weirdness zero
Processing took 12952 μs
Processing took 403522 μs
Processing took 626859 μs
Processing took 626965 μs
Processing took 627109 μs
请注意,第一次迭代是快速的,而其余迭代可能需要 50倍 的时间。
当缓冲区首次填充42
时,处理始终很快:
./buffer_weirdness 42
Processing took 12892 μs
Processing took 13500 μs
Processing took 13482 μs
Processing took 12965 μs
Processing took 13121 μs
行为取决于“BUF_SIZE(例如上面的1GB)”-更大的尺寸更有可能显示出问题,并且还取决于当前主机状态。如果我让主机保持不动一段时间,那么缓慢的迭代可能需要60,000微秒,而不是600,000微秒-因此快10倍,但仍然比快处理时间慢约5倍。最终,时间会恢复到完全缓慢的行为。行为至少部分取决于透明巨页-如果我禁用它们2,则缓慢迭代的性能将提高约3倍,而快速迭代则不变。
最后要注意的是,进程的总运行时间比仅计时进程例程要接近得多(实际上,填充为零、THP关闭版本比其他版本快大约2倍,它们大致相同)。
这里发生了什么?
1除了编译器理解缓冲区已经包含的值并省略写入相同值的情况之外,其他情况都在这里没有发生。
2
sudo sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled"
3这是原始基准测试的浓缩版本。是的,我泄漏了分配,但这导致了更简洁的示例。原始示例没有泄漏。实际上,当您不泄漏分配时,行为会发生变化:可能是因为
malloc
只能重用下一个分配的区域,而不是向操作系统请求更多内存。
BUF_SIZE
或运行更多迭代才能看到效果。就像存在一些有限的资源,与内存成比例,会被用尽。 - BeeOnRope