6.22
相当于求函数的最值,通过初中知识可以得到当时取到最值。
6.23
6.24
A
最好情况就是全部连续。
B
6.25
6.26
6.27
A
0x8a4~0x8a7,0x704~0x707
B
0x1238~0x123b
6.28
A
无
B
0x18f0~0x18f3,0xb0~0xb3
C
0xe34~0xe37
D
0x1bdc~0x1bdf
6.29
A
CO:0~1
CI:2~3
CT:4~11
B
6.30
A
B
CO:0~1
CI:2~4
CT:5~12
6.31
A
00111000 110 10
B
CO:0x2
CI:0x6
CT:0x38
命中:是
返回:0xeb
6.32
A
10110111 010 00
B
CO:0x0
CI:0x2
CT:0xb7
命中:否
返回:—
6.33
0x1788~0x178b,0x16c8~0x16cb
6.34
dst数组:
src数组:
本来手算的,脑子不清楚算麻了,干脆写了个程序跑,省事多了。
#include
#include
using namespace std;
int cache[2] = {-1, -1};
vector src, dst;
void print(vector &vs)
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
cout << vs[i * 4 + j] << ' ';
cout << endl;
}
}
int main()
{
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
{
int bsrc = i, bdst = j + 4;
if (cache[bsrc & 1] != bsrc)
src.push_back("m"), cache[bsrc & 1] = bsrc;
else
src.push_back("h");
if (cache[bdst & 1] != bdst)
dst.push_back("m"), cache[bdst & 1] = bdst;
else
dst.push_back("h");
}
cout << "src:\n";
print(src);
cout << "dst:\n";
print(dst);
}
dst
的答案没转置,脑补一下就行了。
6.35
显然足够大,放得下所有元素,那么只有初始的未命中,剩下的全是命中了。
dst数组:
src数组:
6.36
A
B
C
D
不会,块与块之间已经不会互相冲突,不会出现抖动现象。
E
会,只有对块的第一次访问会不命中,当块增大,访问块的次数增多,会导致命中率上升。
6.37
我对答案的那哥们好像做错了,主要不一样的地方在于第一列的第二和第三行。
对于的情况来说,每次步长为,缓存为,能够放下次循环,而每次循环次数为次,后面的会覆盖之前缓存中的内容,因为是在一次内层循环中,缓存被全部换了一遍,外层循环只不过是在重复这个过程,所以不命中率为,和之前抖动的现象类似。
对于的情况,和上面一种类似,也是抖动,只不过有一个改进是每次取的一个矩阵,这样右边的两个元素会命中,所以不命中率下降到了。
对于右边的情况,我们可以看到没有做任何事情,单单改变了元素数量命中率就会上升。思考了一下出现这种现象的原因。每次步长为,块大小为,所以步长为块,需要访问次,而缓存一共有块,发现这两个数字互素,只有塞满缓存后才开始替换,只有第一个元素会不命中,所以不命中率大约为,感觉和哈希用素数取模是同一个道理,倒不如说直接映射缓存其实就是一个哈希表。
这位老哥把缓存的图画出来了,可以帮助理解。
6.38
A
B
C
6.39
A
B
C
连续访问个元素,只有第一个不命中。
6.40
A
B
C
6.41
6.42
6.43
6.44
运行结果:
Clock frequency is approx. 2496.0 MHz
Memory mountain (MB/sec)
s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15
128m 16431 9272 6687 4910 3933 3396 2824 2481 2251 2114 1980 1854 1723 1652 1605
64m 16924 9866 6896 4929 4066 3407 2899 2643 2390 2185 2037 1872 1767 1695 1655
32m 17100 10608 7226 5359 4410 3682 3178 2835 2614 2248 2245 2092 2011 1896 1852
16m 18372 11009 8467 6270 5125 4269 3758 3344 3115 2902 2488 2631 2523 2375 2465
8m 20366 13656 10551 7949 5321 4516 4272 3454 3772 3593 3684 3844 3848 3951 4227
4m 28843 20763 16701 13705 11511 9892 8512 7540 7754 7437 7167 6920 6751 6211 6108
2m 27698 21271 17533 13978 11617 10004 8760 7758 7421 7178 6906 6676 6496 6346 6241
1024k 27726 20927 17356 13963 11671 10034 8810 7794 7461 7179 6895 6662 6491 6332 6243
512k 27800 21148 17539 14195 11940 10234 8978 7929 7544 7264 7074 6919 6844 6824 6827
256k 27900 23239 21056 18338 16104 14107 12500 11044 10965 11262 11206 11973 11848 12114 13013
128k 28870 23503 23341 21706 19779 16891 15620 13797 13810 13827 12269 12598 11995 12456 12725
64k 29284 24951 24255 22923 20046 16704 15037 12991 12246 12505 12370 11771 12240 13217 23401
32k 32900 31361 30494 28638 28200 28279 27819 27631 27869 28591 32039 30973 27587 26792 29952
16k 31313 30337 29761 29894 28196 30973 27813 31170 26722 30973 28589 25426 26652 26548 27256
观察s15
列,从下往上,寻找梯度下降最快的几个点,数据趋势大约是30000->13000->6200->1700
,比较明显地下降了三次,分别对应三级缓存,分别大约是128KB
,1.0MB
,32MB
。实际上查看任务管理器中三级缓存的数据,分别为:256KB
,1.0MB
和6.0MB
,和预估的有差距。说明这种方式只能大概地估计缓存大小,并不准确。
6.45
一开始觉得没什么好提升的,后来看了提示,仔细分析缓存的访问,发现对于src
的访问已经没有提升了,而对于dst
的访问还有提升的空间。
因为dst
是按照列访问的,原来的访问模式每次将一块加载到缓存中,然后只使用一次就离开去别的块了,所以我们可以将原矩阵分块,使得每次加载一块进入缓存后,多使用几次再去下一个循环,而这个分块的大小,是根据缓存中一行的大小来决定的,在不清楚数据的情况下只能多次尝试。
代码如下:
#include
#include
#include
using namespace std;
const int N = 8192;
int dst[N][N], src[N][N];
void gen()
{
mt19937 rng(time(0));
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
src[i][j] = rng();
}
void test(void (*fun)(int *, int *, int, int), int block = -1)
{
clock_t st = clock();
fun(&dst[0][0], &src[0][0], N, block);
cout << clock() - st << '\n';
}
void transpose(int *dst, int *src, int dim, int block)
{
for (int i = 0; i < dim; i++)
for (int j = 0; j < dim; j++)
dst[j * dim + i] = src[i * dim + j];
}
void transpose_optimized(int *dst, int *src, int dim, int block)
{
for (int i = 0; i < dim - block + 1; i += block)
for (int j = 0; j < dim - block + 1; j += block)
for (int m = 0; m < block; m++)
for (int n = 0; n < block; n++)
dst[(j + n) * dim + i + m] = src[(i + m) * dim + j + n];
for (int ii = i; ii < dim; ii++)
for (int jj = 0; jj < dim; jj++)
dst[jj * dim + ii] = src[ii * dim + jj];
for (int ii = 0; ii < i; ii++)
for (int jj = j; jj < dim; jj++)
dst[jj * dim + ii] = src[ii * dim + jj];
}
int main()
{
gen();
cout << "origin: ";
test(transpose);
for (int block = 2; block <= 16; block++)
{
cout << "block size= " << block << ": ";
test(transpose_optimized, block);
}
}
用c++
写了个测试程序,先用随机数填充了一个8192*8192
的矩阵,然后对原代码和优化后的代码分别进行时间的测量得到结果:
origin: 984
block size= 2: 454
block size= 3: 326
block size= 4: 270
block size= 5: 275
block size= 6: 250
block size= 7: 246
block size= 8: 219
block size= 9: 220
block size= 10: 229
block size= 11: 260
block size= 12: 288
block size= 13: 327
block size= 14: 326
block size= 15: 336
block size= 16: 356
发现运行时间先减少再增大,这样的结果是意料之中的,首先随着分块的变大运行速度提高,因为缓存的利用率得到了提高,到达一定程度后,每次分块都超出了缓存一行的大小,在内循环中都需要多次访问内存,缓存的利用率反而下降,运行速度下降。
经过多次实验,得到当block size=8
时取得最小值,运行速度提高了倍左右。
6.46
和上一题几乎一模一样,改改代码就完事了。
注意细节,利用对称性能够减少一些计算量。
#include
#include
#include
using namespace std;
const int N = 8192;
int G[N][N];
void gen()
{
mt19937 rng(time(0));
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
G[i][j] = rng() % 2;
}
void test(void (*fun)(int *, int, int), int block = -1)
{
clock_t st = clock();
fun(&G[0][0], N, block);
cout << clock() - st << '\n';
}
void col_convert(int *G, int dim, int block)
{
for (int i = 0; i < dim; i++)
for (int j = 0; j < dim; j++)
G[j * dim + i] = G[j * dim + i] | G[i * dim + j];
}
void col_convert_optimized(int *G, int dim, int block)
{
for (int i = 0; i < dim - block + 1; i += block)
for (int j = i; j < dim - block + 1; j += block)
for (int m = 0; m < block; m++)
for (int n = 0; n < block; n++)
G[(i + m) * dim + j + n] = G[(j + n) * dim + i + m] = G[(j + n) * dim + i + m] | G[(i + m) * dim + j + n];
for (; i < dim; i++)
for (j = 0; j <= i; j++)
G[i * dim + j] = G[j * dim + i] = G[i * dim + j] | G[j * dim + i];
}
int main()
{
gen();
cout << "origin: ";
test(col_convert);
for (int block = 2; block <= 16; block++)
{
cout << "block size= " << block << ": ";
test(col_convert_optimized, block);
}
}
运行结果:
origin: 838
block size= 2: 440
block size= 3: 352
block size= 4: 316
block size= 5: 271
block size= 6: 235
block size= 7: 210
block size= 8: 177
block size= 9: 211
block size= 10: 255
block size= 11: 293
block size= 12: 339
block size= 13: 455
block size= 14: 532
block size= 15: 532
block size= 16: 527
番外
查看缓存架构。
输入命令:
cd /sys/devices/system/cpu/cpu0/cache/index0
这是cpu0
的第一个缓存数据。
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ ls
coherency_line_size level physical_line_partition shared_cpu_map type ways_of_associativity
id number_of_sets shared_cpu_list size uevent
下面解释一下部分文件中的数据。
文件 | 注释 |
---|---|
coherency_line_size |
行/块大小 |
level |
在第几层(Ln中的n) |
number_of_sets |
组数 |
size |
缓存总大小 |
type |
类型(Data/Instruction/Unified) |
ways_of_associativity |
相联数(每组行数) |
shared_cpu_map |
十六进制数字,代表哪几个cpu共享 |
shared_cpu_list |
共享的cpu的列表 |
我的机器cpu
是Intel Core i5
,经过查看数据,与下图层次结构一致。
接下来举个例子进行验证:
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cd /sys/devices/system/cpu/cpu1/cache/index3
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat level
3
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat size
6144K
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat type
Unified
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat number_of_sets
8192
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat ways_of_associativity
12
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat coherency_line_size
64
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu1/cache/index3$ cat shared_cpu_list
0-3
这是cpu1
中的第四个缓存,对应图中的L3统一的高速缓存(所有的核共享)
,我们可以看到打印出的数据表明:
- 在第三层
- 总大小为
6.0MB
- 统一的
- 组数为
8192
- 相联数为
12
- 每一行大小位
64B
- 被所有的
cpu
共享
其他的缓存都能在这些地方中查看到数据。
再举个例子:
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cd /sys/devices/system/cpu/cpu0/cache/index0
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat level
1
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat size
32K
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat type
Data
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat number_of_sets
64
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat ways_of_associativity
8
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat coherency_line_size
64
dyume@LAPTOP-LLU88NPC:/sys/devices/system/cpu/cpu0/cache/index0$ cat shared_cpu_list
0
这是cpu0
中的第一个缓存,对应图中的L1d-cache
,我们可以看到打印出的数据表明:
- 在第一层
- 总大小为
32KB
- 数据缓存
- 组数为
64
- 相联数为
8
- 每一行大小位
64B
- 只被第一个处理器独享