性能:memset

14

我有一段简单的C代码,其功能如下(伪代码):

#define N 100000000
int *DataSrc = (int *) malloc(N);
int *DataDest = (int *) malloc(N);
memset(DataSrc, 0, N);
for (int i = 0 ; i < 4 ; i++) {
    StartTimer();
    memcpy(DataDest, DataSrc, N);
    StopTimer();
}
printf("%d\n", DataDest[RandomInteger]);

我的电脑配置: Intel Core i7-3930,搭载有 4x4GB DDR3 1600 内存,运行 RedHat 6.1 64 位操作系统。

第一个 memcpy() 的速度为 1.9 GB/s,而接下来的三个则以 6.2 GB/s 的速度执行。 由于缓存效应无法导致这么大的差异,所以我的第一个问题是:

  • 为什么第一个 memcpy() 如此缓慢?也许是因为 malloc() 直到使用时才完全分配了内存?

如果我取消 memset(),那么第一个 memcpy() 的速度将约为 1.5 GB/s, 而接下来的三个则以 11.8 GB/s 的速度运行。 速度提升了近2倍。 我的第二个问题是:

  • 如果我不调用 memset(),为什么 memcpy() 会快2倍?

2
如果你从未初始化的源中进行memcpy,那不是UB吗?你使用什么编译器以及什么优化?通过将数据大小增加10倍或更多来使时间更可靠。 - usr
1
数据将是随机的,只要您不以可能引入未定义行为的方式使用数据,就没有未定义行为。示例中没有任何代码会这样做。 - this
1
顺便说一句:11.8GB/s的总线速度对我来说似乎有点太快了。 - wildplasser
1
读取未初始化的变量不会触发未定义行为,但是错误地使用该值会触发。例如,使用该值来访问数组偏移量将触发未定义行为。我想从技术上讲(标准),你是正确的。 - this
2
可能是正确的,但OP特别提到了gcc和linux。此外:对于int类型来说,不存在陷阱表示(而且这些int类型从未被使用,只是被复制)。否则,从未知的磁盘文件中读取随机数据也可能会引起问题。 - wildplasser
显示剩余8条评论
2个回答

15

正如其他人已经指出的那样,Linux采用乐观内存分配策略

第一个和后续的memcpy之间的区别在于DataDest的初始化。

正如你已经看到的那样,当你消除memset(DataSrc, 0, N)时,第一个memcpy甚至更慢,因为源的页面也必须被分配。当你初始化DataSrcDataDest时,例如

memset(DataSrc, 0, N);
memset(DataDest, 0, N);

所有的 memcpy 函数都会以大致相同的速度运行。

对于第二个问题:当您用 memset 初始化分配的内存时,所有页面都将依次布置。另一方面,当内存被分配为复制时,源页面和目标页面将交替分配,这可能会有所不同。


太棒了,@Olaf Dietsche的回答! - Deepak Tatyaji Ahire

7
这很可能是由于您的VM子系统中的惰性分配所致。通常情况下,当您分配大量内存时,只有前N页实际上被分配并连接到物理内存。当您访问超出这些前N页时,会生成页面故障,并且进一步的页面将按照“按需”基础分配并连接。
至于问题的第二部分,我认为一些VM实现实际上会跟踪归零的页面并特殊处理它们。尝试将初始化为实际(例如随机)值并重复测试。

3
+1 - 预先“弄脏”(写入)所有页面确实可以使事情清晰明了,可以尝试使用 calloc():https://dev59.com/1XI_5IYBdhLWcg3wEexu - Sam
1
@Sam:在那个链接的问题中,排名第一的答案在我修复之前是不正确的;在大多数主流操作系统上,calloc会从内核获取零页,因此它们仍然是惰性分配的,并且在读取或写入时会出现页面错误。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接