无论内存 1 的内容是什么,我都希望写入char *
缓冲区的时间相同。不是吗?
然而,虽然缩小了基准测试中的不一致性,但我遇到了一个显然不是这样的情况。包含全零的缓冲区在性能方面与填充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μs而不是600,000 - 所以快10倍,但仍然比快速处理时间慢约5倍。最终,时间会回到完全缓慢的行为。
行为也至少部分取决于透明的大页面 - 如果我禁用它们 2 ,慢速迭代的性能提高约3倍,而快速迭代不变。
最后一点是该进程的总运行时比简单地计时进程例程更接近(实际上,零填充,THP关闭版本是关于比其他人快2倍,大致相同)。
这里发生了什么?
1 在一些非常异常优化之外,例如编译器了解缓冲区已包含的值和删除相同值的写入,这在此处不会发生。
2 sudo sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled"
3 这是原始基准的蒸馏版本。是的,我正在泄漏分配,克服它 - 这导致了一个更简洁的例子。最初的例子没有泄露。实际上,当您不泄漏分配时,行为会发生变化:可能是因为malloc
可以重新使用该区域进行下一次分配,而不是要求操作系统获得更多内存。
答案 0 :(得分:17)
这似乎难以重现,所以它可能是编译器/ libc特定的。
我最好的猜测:
当您致电malloc
时,您会将内存映射到您的进程空间,不意味着操作系统已从其中获取必要的页面可用内存池,但它只是在一些表中添加了条目。
现在,当你尝试访问那里的内存时,你的CPU / MMU会引发一个错误 - 操作系统可以捕获它,并检查该地址是否属于“已经在内存空间中的类别,但实际上还没有分配给流程“。如果是这种情况,则会找到必要的可用内存并将其映射到进程的内存空间。
现在,现代操作系统通常有一个内置选项,可以在(重新)使用之前将页面“清零”。如果这样做,则memset(,0,)
操作变得不必要。对于POSIX系统,如果使用calloc
而不是malloc
,内存将被清零。
换句话说,当您的操作系统支持时,您的编译器可能已经注意到并完全省略了memset(,0,)
。这意味着您在process()
中写入页面的那一刻是他们被访问的第一时刻 - 并且触发了操作系统的“即时页面映射”机制。
memset(,42,)
当然不能被优化掉,所以在这种情况下,页面实际上是预分配的,你没有看到在process()
函数中花费的时间。
您应该使用/usr/bin/time
将整个执行时间与process
中花费的时间进行实际比较 - 我的怀疑意味着process
中保存的时间实际上花费在main
上},可能在内核上下文中。
更新:使用优秀Godbolt Compiler Explorer进行测试:是的,使用-O2
和-O3
,现代gcc只是省略了零记忆(或者更确切地说,只需将其融合到calloc
,malloc
并将其归零):
#include <cstdlib>
#include <cstring>
int main(int argc, char ** argv) {
char *p = (char*)malloc(10000);
if(argc>2) {
memset(p,42,10000);
} else {
memset(p,0,10000);
}
return (int)p[190]; // had to add this for the compiler to **not** completely remove all the function body, since it has no effect at all.
}
在gcc6.3上为x86_64成为
main:
// store frame state
push rbx
mov esi, 1
// put argc in ebx
mov ebx, edi
// Setting up call to calloc (== malloc with internal zeroing)
mov edi, 10000
call calloc
// ebx (==argc) compared to 2 ?
cmp ebx, 2
mov rcx, rax
// jump on less/equal to .L2
jle .L2
// if(argc > 2):
// set up call to memset
mov edx, 10000
mov esi, 42
mov rdi, rax
call memset
mov rcx, rax
.L2: //else case
//notice the distinct lack of memset here!
// move the value at position rcx (==p)+190 into the "return" register
movsx eax, BYTE PTR [rcx+190]
//restore frame
pop rbx
//return
ret
顺便说一句,如果您删除了return p[190]
,
}
return 0;
}
然后编译器根本没有保留函数体的原因 - 它的返回值在编译时很容易确定,并且没有副作用。然后整个程序编译成
main:
xor eax, eax
ret
请注意,每A xor A
0
A
为ET_VAL1
。