在 Ulrich Drepper 的论文What every developer should know about memory中,第 3 部分:CPU Caches,他展示了一个图表,显示了“工作集”大小和每个操作消耗的 cpu 周期之间的关系(在这种情况下,顺序读取)。图中有两个跳跃,分别表示 L1 缓存和 L2 缓存的大小。我编写了自己的程序来重现 c 中的效果。它只是简单地从头到尾顺序读取一个 int[] 数组,我尝试了不同大小的数组(从 1KB 到 1MB)。我将数据绘制成图表,没有跳跃,图表是一条直线。
我的问题是:
- 我的方法有问题吗?产生cpu缓存效果的正确方法是什么(查看跳转)。
- 我在想,如果是顺序读取,那么它应该是这样操作的:当读取第一个元素时,它是缓存未命中,并且在缓存行大小(64K)内,会有命中。在预取的帮助下,读取下一个缓存行的延迟将被隐藏。它将连续读取数据到 L1 缓存,即使工作集大小超过 L1 缓存大小,它也会驱逐最近最少使用的数据,并继续预取。所以大部分缓存未命中将被隐藏,从 L2 获取数据所消耗的时间将隐藏在读取活动的后面,这意味着它们是同时操作的。关联性(在我的例子中是 8 种方式)将隐藏从 L2 读取数据的延迟。那么,我的程序现象应该是正确的,我错过了什么吗?
- 是否有可能在java中获得相同的效果?
顺便说一句,我在linux中这样做。
编辑 1
感谢 Stephen C 的建议,这里有一些额外的信息:这是我的代码:
int *arrayInt;
void initInt(long len) {
int i;
arrayInt = (int *)malloc(len * sizeof(int));
memset(arrayInt, 0, len * sizeof(int));
}
long sreadInt(long len) {
int sum = 0;
struct timespec tsStart, tsEnd;
initInt(len);
clock_gettime(CLOCK_REALTIME, &tsStart);
for(i = 0; i < len; i++) {
sum += arrayInt[i];
}
clock_gettime(CLOCK_REALTIME, &tsEnd);
free(arrayInt);
return (tsEnd.tv_nsec - tsStart.tv_nsec) / len;
}
在 main() 函数中,我尝试了从 1KB 到 100MB 的数组大小,仍然相同,每个元素的平均耗时为 2 纳秒。我认为时间是L1d的访问时间。
我的缓存大小:
L1d == 32k
L2 == 256k
L3 == 6144k
编辑 2
我已将代码更改为使用链表。
// element type
struct l {
struct l *n;
long int pad[NPAD]; // the NPAD could be changed, in my case I set it to 1
};
struct l *array;
long globalSum;
// for init the array
void init(long len) {
long i, j;
struct l *ptr;
array = (struct l*)malloc(sizeof(struct l));
ptr = array;
for(j = 0; j < NPAD; j++) {
ptr->pad[j] = j;
}
ptr->n = NULL;
for(i = 1; i < len; i++) {
ptr->n = (struct l*)malloc(sizeof(struct l));
ptr = ptr->n;
for(j = 0; j < NPAD; j++) {
ptr->pad[j] = i + j;
}
ptr->n = NULL;
}
}
// for free the array when operation is done
void release() {
struct l *ptr = array;
struct l *tmp = NULL;
while(ptr) {
tmp = ptr;
ptr = ptr->n;
free(tmp);
}
}
double sread(long len) {
int i;
long sum = 0;
struct l *ptr;
struct timespec tsStart, tsEnd;
init(len);
ptr = array;
clock_gettime(CLOCK_REALTIME, &tsStart);
while(ptr) {
for(i = 0; i < NPAD; i++) {
sum += ptr->pad[i];
}
ptr = ptr->n;
}
clock_gettime(CLOCK_REALTIME, &tsEnd);
release();
globalSum += sum;
return (double)(tsEnd.tv_nsec - tsStart.tv_nsec) / (double)len;
}
最后,我将打印出 globalSum 以避免编译器优化。如您所见,它仍然是顺序读取,我什至尝试了高达 500MB 的数组大小,每个元素的平均时间约为 4 纳秒(可能是因为它必须访问数据'pad'和指针' n', 两次访问), 与 1KB 的数组大小相同。所以,我认为这是因为像预取这样的缓存优化很好地隐藏了延迟,对吗?我会尝试随机访问,稍后再放上结果。
编辑 3
我尝试了对链表的随机访问,结果如下:
第一条红线是我的 L1 缓存大小,第二条是 L2。所以我们可以看到那里有一点跳跃。有时延迟仍然被很好地隐藏起来。