2017-2018-1 20155231 《信息安全系统设计基础》第13周学习总结
第六章 存储器层次结构
第一节 存储技术
- 所谓“随机存取”,指的是当存储器中的数据被读取或写入时,所需要的时间与这段信息所在的位置或所写入的位置无关。相对的,读取或写入顺序访问(Sequential Access)存储设备中的信息时,其所需要的时间与位置就会有关系。它主要用来存放操作系统、各种应用程序、数据等
- 当电源关闭时RAM不能保留数据。如果需要保存数据,就必须把它们写入一个长期的存储设备中(例如硬盘)。
- 动态DRAM(DRAM):将每个位存储为对电容的充电。电容约为30×10-15F。
- 磁盘存储:公式:磁盘容量=字节数/扇区平均盘区数/磁道磁道数/表面表面数/盘片盘片数/磁盘
- 任何时刻,所有的读写头都位于同一柱面上。
磁盘以扇区大小的块来读写数据。
传送时间:当目标扇区的第一个位位于读写头下时,驱动器就可以开始读或者写该扇区的内容。依赖于旋转速度和每条磁道的扇区数目。
平均传送时间 = (1/最大旋转数率) * (1/每磁道的平均扇区数) - 磁盘中有一个小的硬件/固件设备,称为磁盘控制器,维护着逻辑块号和实际(物理)扇区之间的映射关系。
第二节 局部性
- 有良好局部性的程序比局部性差的程序运行得更快
- 步长为1的引用模式:就是顺序访问一个向量的每个元素,有时也被称为顺序引用模式,是程序中空间局部性常见和重要的来源。
一般来说,随着步长增加,空间局部性下降。 - 语言中数组在存储器中是按照行顺序存放的,所以按照行优先顺序执行的代码的局部性要好于按列优先顺序的代码。
- 量化评价一个程序中局部性的简单原则
- 重复引用同一个变量的程序有良好的时间局部性
- 对于具有步长为k的引用模式的程序,步长越小,空间局部性越好
- 对于取指令来说,循环有好的时间和空间局部性。循环体越小,循环迭代次数越多,局部性越好。
第三节 存储器层次结构
- 高层往底层走,存储设备变的更慢、更便宜、更大。每一层存储设备都是下一层的缓存。
- 存储器层次结构的中心思想:对于每个k,位于k层的更快更小的存储设备作为位于k+1层的更大更慢的存储设备的缓存。即每层存储设备都是下一层的“缓存”
数据总是以块大小为传送单元在第k层与第k+1层之间来回拷贝。任一对相邻的层次之间块大小是固定的,但是其他的层次对之间可以有不同的块大小。 - 替换策略:决定替换哪个块
- 随机替换策略:随机选择一个牺牲块
- 最近最少被使用替换策略(LRU):选择最后被访问的时间距现在最远的块
- 冲突不命中:由于一个放置策略:将第k+1层的某个块限制放置在第k层块的一个小的子集中,这就会导致缓存没有满,但是那个对应的块满了,就会不命中。
- 容量不命中:当工作集的大小超过缓存的大小时,缓存会经历容量不命中,就是说缓存太小了,不能处理这个工作集。
第四节 高速缓存存储器
- 高速缓存大小:较大的高速缓存可能会提高命中率,但使大存储器运行的更快是更难一些的。
- 块大小:较大的块能利用程序中可能存在的空间局部性,帮助提高命中率;但块越大意味着高速缓存行较少,损害时间局部性。
- 相联度:相联度较大(E值较大)优点是降低了高速缓存由于冲突不命中出现抖动的可能性,但成本较高。
- 写策略:直写高速缓存易实现,而且能使用独立于高速缓存的写缓冲区,用来更新存储器,不命中开销小。写回高速缓存引起的传送比较少,允许更多的到存储器的宽带用于执行DMA的I/O设备。越下层,越可能用写回而不是直写。
第五节 编写高速缓存友好的代码
确保代码高速缓存友好的基本方法:
- 让最常见的情况运行的快
- 在每个循环内部缓存不命中数量最小
- 两种局限性:空间、时间
教材学习中的问题和解决过程
- 问题1: pwd 命令查看默认工作目录的完整路径
- 问题1解决方案:
[root@localhost ~]# pwd
/root
[root@localhost ~]#
- 问题2:pwd 命令查看指定文件夹
- 问题2解决方案:
[root@localhost ~]# cd /opt/soft/
[root@localhost soft]# pwd
/opt/soft
[root@localhost soft]#
代码调试中的问题和解决过程
- 问题1:实现pwd
- 解决问题:
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <unistd.h>
#define MAX_DIR_DEPTH (256) //限制最大的目录深度
#define TRUE 1
#define FALSE 0
//根据文件名获取文件的inode-number
ino_t get_ino_byname(char *filename)
{
struct stat file_stat;
if(0 != stat(filename, &file_stat)) //stat()通过文件名filename获取文件信息,并保存在buf所指的结构体stat中
{
perror("stat");
exit(-1);
}
return file_stat.st_ino;
}
//根据inode-number, 在当前目录中查找对呀的文件名
char *find_name_byino(ino_t ino)
{
DIR *dp = NULL;
struct dirent *dptr = NULL;
char *filename = NULL;
if(NULL == (dp = opendir("."))) //opendir()打开一个目录,在失败的时候返回一个空的指针,成返回DIR结构体
{
fprintf(stderr, "Can not open Current Directory
");
exit(-1);
}
else
{
while(NULL != (dptr = readdir(dp))) //readdir()用来读取目录。返回是dirent结构体指针
{
if(dptr->d_ino == ino)
{
filename = strdup(dptr->d_name); //strdup()将串拷贝到新建的位置处,返回一个指针,指向为复制字符串分配的空间;如果分配空间失败,则返回NULL值.
break;
}
}
closedir(dp);
}
return filename;
}
int main(int argc, char *argv[])
{
//记录目名的栈
char *dir_stack[MAX_DIR_DEPTH];
unsigned current_depth = 0;
while(TRUE)
{
ino_t current_ino = get_ino_byname("."); //通过特殊的文件名"."获取当期目录的inode-number
ino_t parent_ino = get_ino_byname(".."); //通过特殊的文件名".."获取当前目录的父目录的inode-number
if(current_ino == parent_ino)
break; //达到根目录,推出循环
/*两个inode-number不一样*/
chdir(".."); //更改当前工作目录,变为当前目录的父目录
dir_stack[current_depth++] = find_name_byino(current_ino); //"文件名"地址存放
if(current_depth >= MAX_DIR_DEPTH) //路径名太深
{
fprintf(stderr, "Directory tree is too deep.
");
exit(-1);
}
}
int i = current_depth - 1;
for(i = current_depth - 1; i >= 0; i--) //打印路径
{
fprintf(stdout, "/%s", dir_stack[i]);
}
fprintf(stdout, "%s
", current_depth == 0 ? "/" : "");
return 0;
}
代码托管
课后习题
6.22
假设磁道沿半径均匀分布,即总磁道数和(1-x)r成正比,设磁道数为(1-x)rk;
由题单个磁道的位数和周长成正比,即和半径xr成正比,设单个磁道的位数为xrz;
其中r、k、z均为常数。
所以C = (1-x)rk * xrz = (-x^2 + x) * r^2 * kz,即需要-x^2 + x最大,得到x = 0.5。
6.23
seek time : 4 ms
average rotational latency : 0.5 * 60 / 15000 * 1000 = 2 ms
transfer time : 60 / 15000 / 800 * 1000= 0.005 ms
4 + 2 + 0.005 = 6.005 ms
6.24
A.
2MB = 512 bytes * 4096,即需要读取4096个扇区。
定位时间为:4 + 0.560/150001000 = 6 ms
最理想的情况下,这4096个扇区都在一个柱面上(一个磁道读完后继续读下一个,磁头不用移动),也就是4096/1000 = 5个磁道。
即transfer time = 4096 / 1000 * 60 / 15000 * 1000 = 16.384 ms
所以理想时间为:6 + 16.384 = 22.384 ms
B.
2MB = 512 bytes * 4096,即需要读取4096个扇区。
定位时间为:4 + 0.560/150001000 = 6 ms
在完全随机的情况下,这4096个扇区分布在不同的磁道上,每一个扇区读完以后磁头都要再次去定位。
所以总的时间为:6 * 4096 + transfer time = 24592.384 ms (这里书上有一个类似的题目,但是没有加上transfer time ,我觉得还是要加上)
6.25
S t s b
- 64 24 6 2
- 1 30 0 2
- 128 22 7 3
- 1 29 0 3
- 32 22 5 5
- 8 24 3 5
6.26
m C B E S t s b
-
2048 256
-
4 4
-
25 6
-
32 5
6.27
A.
0x08A4 0x08A5 0x08A6 0x08A7
0x0704 0x0705 0x0706 0x0707
B.
0x1238 0x1239 0x123A 0x123B
6.28
A.
None
B.
0x18F0 0x18F1 0x18F2 0x18F3
0x00B0 0x00B1 0x00B2 0x00B3
C.
0x0E34 0x0E35 0x0E36 0x0E37
D.
0x1BDC 0x1BDD 0x1BDE 0x1BDF
6.29
A.
CTCTCTCTCTCTCTCT, CICI, COCO
B.
Hit? Read value(or unknown)
N -
Y unknown
Y 0xC0
6.30
A.
C = S * E * B = 128 Bytes
B.
CTCTCTCTCTCTCTCT, CICICI, COCO
6.31
A.
00111000, 110, 10
B.
CO 0x2
CI 0x6
CT 0x38
Cache hit? Y
Cache byte returned 0xEB
6.32
A.
10110111, 010, 00
B.
CO 0x0
CI 0x2
CT 0xB7
Cache hit? N
Cache byte returned -
6.33
0x1788 0x1789 0x178A 0x178B
0x16C8 0x16C9 0x16CA 0x16CB
6.34
cache共有两个block,分别位于两个set中,设他们为b1和b2。每个block可以放下4个int类型的变量,也就是数组中的一行。在这一题中,源数组和目的数组是相邻排列的。所以内存和cache的映射情况是这样的:
b1 : src[0][] src[2][] dst[0][] dst[2][]
b2 : src[1][] src[3][] dst[1][] dst[3][]
dst array src array
Col.0 Col.1 Col.2 Col.3 Col.0 Col.1 Col.2 Col.3
Row0 m m h m m m m m
Row1 m h m h m m m m
Row2 m m h m m m m m
Row3 m h m h m m m m
6.35
cache共有八个block,分别位于八个set中,设他们为b1、b2、b3、b4、b5、b6、b7、b8。每个block可以放下4个int类型的变量,也就是数组中的一行。在这一题中,源数组和目的数组是相邻排列的。所以内存和cache的映射情况是这样的:(这时不会有冲突)
b1 : src[0][]
b2 : src[1][]
b3 : src[2][]
b4 : src[3][]
b5 : dst[0][]
b6 : dst[1][]
b7 : dst[2][]
b8 : dst[3][]
dst array src array
Col.0 Col.1 Col.2 Col.3 Col.0 Col.1 Col.2 Col.3
Row0 m h h h m h h h
Row1 m h h h m h h h
Row2 m h h h m h h h
Row3 m h h h m h h h
6.36
A.
cache共有32个block,分别位于32个set中,每个block可以放下4个int类型的变量,所以所有的block可以放下x数组中的一行。由映射关系,x[0][i]和x[1][i]对应的set是一样的。所以每一次的运算都会发生miss的情况,所以miss rate = 100%。
B.
cache共有64个block,分别位于64个set中,每个block可以放下4个int类型的变量,所以所有的block可以放下x数组中的两行,即全部放入。每四次读取中的第一次会发生miss,所以miss rate = 25%。
C.
cache共有32个block,分别位于16个set中,每个block可以放下4个int类型的变量,每个set可以放下8个int类型的变量,所有的block可以放下x数组中的一行。由映射关系,x[0][i]和x[1][i]对应的set是一样的,x[y][i]和x[y][i+64]对应的set也是一样的。
对于x[0][0] * x[1][0] ~ x[0][63] * x[1][63] ,每四次运算会有第一次miss。
对于x[0][64] * x[1][64] ~ x[0][127] * x[1][127] ,每四次运算会有第一次miss(擦去前面warm up的cache)。
综上,miss rate = 25%。
D.
不会,因为此时block大小是限制因素(每四次读取第一次miss)。
E.
会,更大的block会降低miss rate,因为miss只发生在第一次读入block的时候,所以更大的block会使得miss占总读取的比例降低。
6.37
cache共有256个block,分别位于256个set中,每个block可以放下4个int类型的变量,所有的block可以放下1024个int类型的变量。
当N = 64:
映射关系:a[0][0] ~ a[15][63]、a[16][0] ~ a[31][63]、a[32][0] ~ a[47][63]、a[48][0] ~ a[63][63] 互相重叠。
sumA按照行来读取,所以每四次读取第一次都会miss,即miss rate = 25%。
sumB按照列来读取,所以每一次读取都会发生miss(读取后的block又会被覆盖),即miss rate = 100%。
sumC按照列来读取,但是每次读取后都会按照行再读取一次,所以每四次读取会有两次miss,即miss rate = 50%。
当N = 60
映射关系:a[0][0] ~ a[17][3]、a[17][4] ~ a[34][7]、a[34][8] ~ a[51][11]、a[51][12] ~ a[59][59]互相重叠,其中最后的a[51][12] ~ a[59][59]没有到达cache的尾部。
sumA按照行来读取,所以每四次读取第一次都会miss,即miss rate = 25%。
sumB按照列来读取,这里的情况有些复杂,我写了一个程序来分析:
#include <stdio.h>
#define SIZEOFCACHE 256
#define SIZEOFBLOCK 4
#define N 60
int main()
{
int cache[SIZEOFCACHE];
for (int k = 0; k < SIZEOFCACHE; ++k)
{
cache[k] = -1;
}
int read = 0;
int miss = 0;
for (int j = 0; j < N; ++j)
{
for (int i = 0; i < N; ++i)
{
//read a[i][j]
++read;
int position = i * N + j;
int need_start = position/SIZEOFBLOCK;
if (cache[need_start%SIZEOFCACHE] != need_start)
{
++miss;
cache[need_start%SIZEOFCACHE] = need_start;
}
}
}
printf("%g
", miss/(double)read);
return 0;
}
输出结果为25%。
C.
将上面程序的循环部分更改为:
for (int j = 0; j < N; j+=2)
{
for (int i = 0; i < N; i+=2)
{
//read a[i][j] a[i+1][j] a[i][j+1] a[i+1][j+1]
++read;
int position = i * N + j;
int need_start = position/SIZEOFBLOCK;
if (cache[need_start%SIZEOFCACHE] != need_start)
{
++miss;
cache[need_start%SIZEOFCACHE] = need_start;
}
++read;
position = (i+1) * N + j;
need_start = position/SIZEOFBLOCK;
if (cache[need_start%SIZEOFCACHE] != need_start)
{
++miss;
cache[need_start%SIZEOFCACHE] = need_start;
}
++read;
position = i * N + j + 1;
need_start = position/SIZEOFBLOCK;
if (cache[need_start%SIZEOFCACHE] != need_start)
{
++miss;
cache[need_start%SIZEOFCACHE] = need_start;
}
++read;
position = (i+1) * N + j + 1;
need_start = position/SIZEOFBLOCK;
if (cache[need_start%SIZEOFCACHE] != need_start)
{
++miss;
cache[need_start%SIZEOFCACHE] = need_start;
}
}
}
输出结果为25%。
6.38
这个cache有64个block,每个block可以放4个int类型的变量,也就是一个point_color的结构体,即cache总共可以放置64个结构体。
映射关系为:square[0][0] ~ square[3][15]、square[4][0] ~ square[7][15]、square[8][0] ~ square[11][15]、square[12][0] ~ square[15][15] 互相重叠。
A.
16 * 16 * 4 = 1024
B.
这个程序是按照行来写的,所以每四次写入只有第一次miss,即miss的次数为1024 / 4 = 256
C.
25%
6.39
这个cache有64个block,每个block可以放4个int类型的变量,也就是一个point_color的结构体,即cache总共可以放置64个结构体。
映射关系为:square[0][0] ~ square[3][15]、square[4][0] ~ square[7][15]、square[8][0] ~ square[11][15]、square[12][0] ~ square[15][15] 互相重叠。
A.
16 * 16 * 4 = 1024
B.
这个程序是按照列来写的,每四次写入只有第一次miss(每次都完整利用了一个block,没有读入block的浪费,此时miss rate只取决于block的大小),即miss的次数为1024 / 4 = 256
C.
25%
6.40
这个cache有64个block,每个block可以放4个int类型的变量,也就是一个point_color的结构体,即cache总共可以放置64个结构体。
映射关系为:square[0][0] ~ square[3][15]、square[4][0] ~ square[7][15]、square[8][0] ~ square[11][15]、square[12][0] ~ square[15][15] 互相重叠。
A.
16 * 16 + 3 * 16 * 16 = 1024
B.
对于第一个循环,每一次写入都会发生miss的情况,最后cache中保存的是square[12][0] ~ square[15][15],而第二个循环又从头开始写入,所以每三次写入的第一次都会发生miss。总的miss次数就是16 * 16 * 2 = 512。
C.
50%
6.41
这个cache有16K个block,每个block可以放4个char类型的变量,也就是一个pixel的结构体,即cache总共可以放置16K个结构体。buffer里面一共有480 * 640 = 300K个结构体,所以映射时会有18个完全重叠的,最后一次重叠3/4.
这个程序按照列来写,每四次写入只有第一次miss(每次都完整利用了一个block,没有读入block的浪费,此时miss rate只取决于block的大小),所以miss rate = 25%。
6.42
这个cache有16K个block,每个block可以放4个char类型的变量,也就是一个pixel的结构体,即cache总共可以放置16K个结构体。buffer里面一共有480 * 640 = 300K个结构体,所以映射时会有18个完全重叠的,最后一次重叠3/4.
这个程序实际上就是按照行来写的指针版本,每四次写入只有第一次miss,miss rate = 25%。
6.43
这个cache有16K个block,每个block可以放4个char类型的变量,也就是一个pixel的结构体,即cache总共可以放置16K个结构体。buffer里面一共有480 * 640 = 300K个结构体,所以映射时会有18个完全重叠的,最后一次重叠3/4.
这个程序实际上还是按照行来写的指针版本,但是只写了buffer数组的1/4。每四次写入只有第一次miss,miss rate = 25%。
6.44
为了辨识缓存的大小,选取中间的列(例如S8)来判断——避免CPU的prefetching带来干扰。可以看出,在32K和512K以及8M的地方有明显的落差,所以判断L1:32k、L2:512k、L3:8M。
6.45
这个题要求我们利用第5章和第6章中学到的优化知识。对于第5章,就是减少循环的数据依赖,从而利用流水线并行执行;对于第6章,则是从两个方面(temporary、spatial)利用数据的“本地性”。
void transpose(int dst, int src, int dim)
{
int i, j;
for (i = 0; i < dim; ++i)
{
for (j = 0; j < dim; ++j)
{
dst[jdim + i] = src[idim +j] /* ! */
}
}
}
以上的关键语句中的乘法和加法已经实现了循环之间独立,src也是按照行读入的,但是dst却是按照列读入的,这样没有充分利用每一次读入的block。于是我们想到可不可以每一次读入dst[jdim + i]所在的block之后继续写入例如dst[jdim + i + 1] dst[jdim + i + 2]这样的变量,但是这样有需要src的部分变为src[(i+1)dim +j]等等,所以我们现在不仅要“横向”扩展dst,还要“纵向”扩展src,其实这是一种叫做blocking的技术,即每次读入一块数据,对此块数据完全利用后抛弃,然后读取下一个块。可以参考csapp网上给的注解:MEM:BLOCKING — Using blocking to increase temporal locality
设我们的数据块的宽度是B,由于我们要对两个数组进行读写操作,所以2B^2 < C(其中C是cache的容量),在此限制下B尽可能取大。
#define B chunkdatas_length_of_side
void faster_transpose(int *dst, int *src, int dim)
{
long limit = dim * dim;
for (int i = 0; i < dim; i += B)
{
for (int j = 0; j < dim; j += B)
{
/* Using blocking to improve temporal locality */
for (int k = i; k < i+B; ++k)
{
for (int l = j; l < j+B; ++l)
{
/* independent calculations */
int d = l*dim + k;
int s = k*dim + l;
if (s < limit && d < limit)
{
dst[d] = src[s]
}
}
}
}
}
}
用lscpu验证分析正确:
6.46
这个题仅仅是6.45的一个实践版。但这个题有一个小技巧,就是G[d] || G[s]这个运算对于G[d]和G[s]都是一样的,所以只用计算一次,从而我们只用计算对角线的上半部分的内容,也就是第二外层循环的j不用从0而是从最外层循环的i开始。
#define B chunkdatas_length_of_side
void faster_col_convert(int *G, int dim)
{
long limit = dim * dim;
for (int i = 0; i < dim; i += B)
{
for (int j = i; j < dim; j += B)
{
/* Using blocking to improve temporal locality */
for (int k = i; k < i+B; ++k)
{
for (int l = j; l < j+B; ++l)
{
/* independent calculations */
int d = l*dim + k;
int s = k*dim + l;
if (s < limit && d < limit)
{
_Bool temp = G[d] || G[s];
G[d] = temp;
G[s] = temp;
}
}
}
}
}
}
结对及互评
点评模板:
- 博客中值得学习的或问题:
- xxx
- xxx
- ...
- 代码中值得学习的或问题:
- xxx
- xxx
- ...
- 其他
本周结对学习情况
- [结对同学学号1](博客链接)
- 结对照片
- 结对学习内容
- XXXX
- XXXX
- ...
其他(感悟、思考等,可选)
本周学习东西很多。有些实验还没完成,还在学习。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第9周 | 299/2700 | 1/5 | 11/1000 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
-
计划学习时间:16小时
-
实际学习时间:11小时
-
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)