【发布时间】:2017-08-27 08:57:22
【问题描述】:
我想使用memcpy 测量内存带宽。我修改了这个答案的代码:why vectorizing the loop does not have performance improvement,它使用memset 来测量带宽。问题是memcpy 只比memset 慢一点,而我预计它会慢两倍,因为它在两倍的内存上运行。
更具体地说,我通过以下操作运行了 1 GB 数组 a 和 b(分配将 calloc)100 次。
operation time(s)
-----------------------------
memset(a,0xff,LEN) 3.7
memcpy(a,b,LEN) 3.9
a[j] += b[j] 9.4
memcpy(a,b,LEN) 3.8
注意memcpy 只比memset 慢一点。操作a[j] += b[j](其中j 超过[0,LEN))应该比memcpy 长三倍,因为它对三倍的数据进行操作。然而,它的速度只有memset 的 2.5 倍左右。
然后我用memset(b,0,LEN) 将b 初始化为零并再次测试:
operation time(s)
-----------------------------
memcpy(a,b,LEN) 8.2
a[j] += b[j] 11.5
现在我们看到memcpy 的速度大约是memset 的两倍,而a[j] += b[j] 的速度大约是memset 的三倍,正如我预期的那样。
至少在memset(b,0,LEN) 之前,我预计memcpy 在100 次迭代中的第一次将是slower because the of lazy allocation (first touch)。
为什么我只能在memset(b,0,LEN) 之后得到预期的时间?
test.c
#include <time.h>
#include <string.h>
#include <stdio.h>
void tests(char *a, char *b, const int LEN){
clock_t time0, time1;
time0 = clock();
for (int i = 0; i < 100; i++) memset(a,0xff,LEN);
time1 = clock();
printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
time0 = clock();
for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
time1 = clock();
printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
time0 = clock();
for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j];
time1 = clock();
printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
time0 = clock();
for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
time1 = clock();
printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
memset(b,0,LEN);
time0 = clock();
for (int i = 0; i < 100; i++) memcpy(a,b,LEN);
time1 = clock();
printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
time0 = clock();
for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j];
time1 = clock();
printf("%f\n", (double)(time1 - time0) / CLOCKS_PER_SEC);
}
main.c
#include <stdlib.h>
int tests(char *a, char *b, const int LEN);
int main(void) {
const int LEN = 1 << 30; // 1GB
char *a = (char*)calloc(LEN,1);
char *b = (char*)calloc(LEN,1);
tests(a, b, LEN);
}
使用 (gcc 6.2) gcc -O3 test.c main.c 编译。 Clang 3.8 给出了基本相同的结果。
测试系统:i7-6700HQ@2.60GHz (Skylake), 32 GB DDR4, Ubuntu 16.10。在我的 Haswell 系统上,带宽在 memset(b,0,LEN) 之前是有意义的,即我只在我的 Skylake 系统上看到了问题。
我首先从a[j] += b[k] 操作in this answer 中发现了这个问题,它高估了带宽。
我想出了一个更简单的测试
#include <time.h>
#include <string.h>
#include <stdio.h>
void __attribute__ ((noinline)) foo(char *a, char *b, const int LEN) {
for (int i = 0; i < 100; i++) for(int j=0; j<LEN; j++) a[j] += b[j];
}
void tests(char *a, char *b, const int LEN) {
foo(a, b, LEN);
memset(b,0,LEN);
foo(a, b, LEN);
}
这个输出。
9.472976
12.728426
但是,如果我在 calloc 之后在 main 中执行 memset(b,1,LEN)(见下文),那么它会输出
12.5
12.5
这让我认为这是操作系统分配问题,而不是编译器问题。
#include <stdlib.h>
int tests(char *a, char *b, const int LEN);
int main(void) {
const int LEN = 1 << 30; // 1GB
char *a = (char*)calloc(LEN,1);
char *b = (char*)calloc(LEN,1);
//GCC optimizes memset(b,0,LEN) away after calloc but Clang does not.
memset(b,1,LEN);
tests(a, b, LEN);
}
【问题讨论】:
-
有很多事情可以在后台产生影响。例如,除非您确保您的分配正确对齐,否则可能会或可能不会使用内在函数,从而导致时序变化而代码没有其他变化。如果您真的想追求这一点,我认为您最好分析生成的程序集,而不是在 C 级别查看它。
-
@DavidHoelzer,你说得对,我应该看看组装。我不知道为什么我没有。我通常这样做。我刚刚在 main.c (单独的目标文件)中尝试了
memset,它没有任何区别。这表示它必须是编译器问题,而不是操作系统分配问题。顺便说一句,在我发现这个(不是在这个问题中)的原始测试中,数组必须是 32 字节对齐的。 -
@DavidHoelzer,快速浏览一下程序集,我看不出
memset(b,0,LEN)有什么不同。这是一个简单的版本godbolt.org/g/z6EM2b。我测试了这个简单的版本,在memset之前还是太快了。 -
Then I initialized b to zero with memset(b,0,LEN) and test again:如果内存之前是未初始化的(但通过 malloc 新获得),它可能已映射到/dev/zero(预计稍后会被 COWed)。并且 dev/zero 非常快......并且它会产生更少的缓存未命中。找出答案的最佳方法是在此过程中监控 RSS -
restrict现在与众不同。我看到你关于编译器在memcpy之前优化掉memset的观点。 GCC 和 Clang 都没有这样做,我不知道为什么。 GCC 确实在calloc离开之后优化了memset(0),但 Clang 没有。