CPU Cache原理与示例 (4)

测试一下,在下表中, 表头是步长,也就是每次跳多少个整数,而纵向是这个数组可以跳几次(可以理解为要几条 Cache Line),于是表中的任何一项代表了这个数组有多少,而且步长是多少。

比如:横轴是 512,纵轴是4,意思是,这个数组有 4*512 = 2048 个长度,访问时按512步长访问,也就是访问其中的这几项:[0, 512, 1024, 1536] 这四项。

表中同的项是,是循环 1000 万次的时间,单位是“微秒”(除以1000后是毫秒)

| count | 1 | 16 | 512 | 1024 | ------------------------------------------ | 1 | 17539 | 16726 | 15143 | 14477 | | 2 | 15420 | 14648 | 13552 | 13343 | | 3 | 14716 | 14463 | 15086 | 17509 | | 4 | 18976 | 18829 | 18961 | 21645 | | 5 | 23693 | 23436 | 74349 | 29796 | | 6 | 23264 | 23707 | 27005 | 44103 | | 7 | 28574 | 28979 | 33169 | 58759 | | 8 | 33155 | 34405 | 39339 | 65182 | | 9 | 37088 | 37788 | 49863 |156745 | | 10 | 41543 | 42103 | 58533 |215278 | | 11 | 47638 | 50329 | 66620 |335603 | | 12 | 49759 | 51228 | 75087 |305075 | | 13 | 53938 | 53924 | 77790 |366879 | | 14 | 58422 | 59565 | 90501 |466368 | | 15 | 62161 | 64129 | 90814 |525780 | | 16 | 67061 | 66663 | 98734 |440558 | | 17 | 71132 | 69753 |171203 |506631 | | 18 | 74102 | 73130 |293947 |550920 |

可以看到,从 [9,1024] 以后,时间显著上升。包括 [17,512] 和 [18,512] 也显著上升。这是因为,机器的 L1 Cache 是 32KB, 8 Way 的,前面说过,8 Way 的有 64 组,每组 8 个 Cache Line,当 for-loop步长超过 1024 个整型,也就是正好 4096 Bytes 时,也就是导致内存地址的变化是变化在高位的 24bits 上,

而低位的1 2bits 变化不大,尤其是中间6bits没有变化,导致全部命中同一组 set,导致大量的 cache 冲突,导致性能下降,时间上升。[16, 512]也是一样的,其中的几步开始导致L1 Cache开始冲突失效。

示例三

接下来,再来看个示例。下面是一个二维数组的两种遍历方式,一个逐行遍历,一个是逐列遍历,这两种方式在理论上来说,寻址和计算量都是一样的,执行时间应该也是一样的。

const int row = 1024; const int col = 512 int matrix[row][col]; //逐行遍历 int sum_row=0; for(int _r=0; _r<row; _r++) { for(int _c=0; _c<col; _c++){ sum_row += matrix[_r][_c]; } } //逐列遍历 int sum_col=0; for(int _c=0; _c<col; _c++) { for(int _r=0; _r<row; _r++){ sum_col += matrix[_r][_c]; } }

然而,并不是,在机器上,得到下面的结果。

Ÿ   逐行遍历:0.081ms

Ÿ   逐列遍历:1.069ms

执行时间有十几倍的差距。其中的原因,就是逐列遍历对于 CPU Cache 的运作方式并不友好,所以,付出巨大的代价。

示例四

接下来,来看一下多核下的性能问题,参看如下的代码。两个线程在操作一个数组的两个不同的元素(无需加锁),线程循环1000万次,做加法操作。在下面的代码中,高亮了一行,就是p2指针,要么是p[1],或是 p[30],理论上来说,无论访问哪两个数组元素,都应该是一样的执行时间。

void fn (int* data) { for(int i = 0; i < 10*1024*1024; ++i) *data += rand(); } int p[32]; int *p1 = &p[0]; int *p2 = &p[1]; // int *p2 = &p[30]; thread t1(fn, p1); thread t2(fn, p2);

然而,并不是,在机器上执行下来的结果是:

Ÿ   对于 p[0] 和 p[1] :560ms

Ÿ   对于 p[0] 和 p[30]:104ms

这是因为 p[0] 和 p[1] 在同一条 Cache Line 上,而 p[0] 和 p[30] 则不可能在同一条Cache Line 上 ,CPU 的缓存最小的更新单位是 Cache Line,所以,这导致虽然两个线程在写不同的数据,但是因为这两个数据在同一条 Cache Line 上,就会导致缓存需要不断进在两个 CPU 的 L1/L2 中进行同步,从而导致了 5 倍的时间差异

示例五

接下来,再来看一下另外一段代码:统计一下一个数组中的奇数个数,但是这个数组太大了,希望可以用多线程来完成这个统计。下面的代码中,为每一个线程传入一个 id ,然后通过这个 id 来完成对应数组段的统计任务。这样可以加快整个处理速度

int total_size = 16 * 1024 * 1024; //数组长度

int* test_data = new test_data[total_size]; //数组

int nthread = 6; //线程数(因为机器是6核的)

int result[nthread]; //收集结果的数组

void thread_func (int id)

{

result[id] = 0;

int chunk_size = total_size / nthread + 1;

int start = id * chunk_size;

int end = min(start + chunk_size, total_size);

for ( int i = start; i < end; ++i )

{

if (test_data[i] % 2 != 0 )

++result[id];

}

}

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zgypyx.html