CSAPP学习记录 (已完结)
因为优快云太难用了,所以LAB8之后的以及代码仓库和读书笔记放在了 这里
CSAPP是一门挺有趣的计算机体系课,包含了很多东西,可以更好的了解整个计算机系统。
下面是无答案的学习记录。
代码会放在 GitHub 上, 请按需自取,不建议直接抄:Dog-Du/CSAPP-CMU15213
LAB1 DATALAB (第二章之后)
2024.10.2-2024.10.3
比起代码,更像是脑筋急转弯或者益智小游戏,比较简单,也没有什么很好说的。。。
LAB2 BOMBLAB (第三章之后)
2024.10.20-2024.10.21
有一说一,第三章非常像汇编语言和编译原理的简单杂交版,并另附了一些其他知识。
非常有趣的实验,分为六个阶段,在看代码的时候发现了隐藏阶段,但是没看明白怎么进入,所以放弃了隐藏关卡。
主要思路:
1.首先要知道寄存器的作用,函数的控制转移的过程,指针的引用和解引用,GDB的基本使用等等
2.分段看汇编,因为肯定包含了循环,分支,函数调用的部分,把代码分成一段一段看,比如一个循环做了什么给它抽象出来。哪里会引爆炸弹,去看去猜原因是什么。
phase_1:
很简单,就是得到 string_length 和 string_not_equal 两个函数,因为会使用rdi和rsi寄存器传递参数,然后根据地址获取他想要的字符串即可,用ASCII码转换即可。
phase_2:
打断点,先读read_six_numbers函数,看看他是怎么实现的,然后再去看phase_2函数,然后尽量还原出来源代码,因为C语言的代码和汇编语言的代码一一对应关系还是比较好找的。
这样会更加方便理解,最后猜到phase_2到底想干什么,就可以比较流畅的得到荅案。
phase_3:
phase_3也是如法炮制,比较简单,唯一需要注意的是 jmp *() 表示间接跳转,然后猜到后面那么多 jmp 的指令到底从何而来之后就能轻松解决。
phase_4:
先看phase_4函数再去看func4函数,phase_4先是读取整数,然后进行一些判断处理,调用了func4,如果func4返回的结果和预期不符则爆炸,最后是一个检查,检查不过则爆炸。
再看func4,可以看到用rdx,rsi,rdi传递了三个整数,之后进行了一些运算,然后在func4中又调用了func4,是递归,这里我们只需要关注递归的退出条件即可,就可以轻松解决。
phase_5:
注:汇编中 mov %fs:0x28,%rax 的作用: 栈保护功能,将一个特殊值(fs:0x28)存在栈的底部, 函数运行结束后再取出这个值和fs:0x28做比较,如果有改变就说明栈被破坏,调用__stack_chk_fail@plt。fs寄存器的值本身指向当前线程结构
这个题目最重要的是要看懂 AND edx, 15 的作用是什么即可。
phase_6:
这六个题目中最难的一个,在我还原的C语言代码中,包含了两层循环,多个循环过程,一大堆跳转看得人头晕眼花,但是还是只要进行分段,把代码分成一段一段看懂某一段是干什么用的,进行抽象即可。但是这里可能需要一点想象力,猜测作用是什么。
lab2总结:
总的来说,比起难,更是有趣,需要一点想象力和代码结合,然后进行猜测。隐藏阶段没有写~
LAB3 ATTACK LAB(第三章之后)
2025.2.2-2025.2.3
之所以隔了好几个月才写lab3,是因为2024.10.21之后就去参加oceanbase赛和天池杯了,之后2024.12.23比赛结束之后,又磨磨唧唧搞期末考试,2025.1.16放了假。该过年了,玩了几天,初四提前回家,初五2025.2.2开始才写lab3。
lab3也是一个比较有趣的实验,根据汇编语言,输入字符串,利用缓存区溢出进行ROP和缓冲区溢出hack。
需要注意的点:
1.关于栈的指针
返回栈指针在当前栈顶的上面。
因为栈是从上向下拓展,并且栈顶在最上面。
比如:当前函数帧是 [1000'000, 1000'010]。则返回栈指针为 [1000'011, 1000'018]
在当前函数帧最高位的上面,小端读取,所以反写。
在这个图中,栈是倒过来的,栈顶是最大的,栈底是最小的。
2.关于caller save调用者保存, callee save被调用者保存 (x86-64 Linux Register Usage)
书上说:
所有其它的寄存器,除了栈指针%rsp,都分类为调用者保存(caller save)寄存器。
%rbx,%r12,%r13,%r14是被调用者保存(callee save)
%rsp是一个特殊的被调用者保存(callee save), 被保存到原值用于退出函数
%rbp是被调用者保存,被调用者必须保存并存储,也许会被用作函数帧指针,可以混用和比较。
3.x86-64, 64位,所以8字节,地址也是64位,指令也是64位。
4.pushq, popq,会将数据压入栈
call是先把pushq rsp,再jump。
ret 是先popq rsp,再jump。
所以,只要利用这一点就可以进行jump。
5.rtarget没有金丝雀
6.rtarget 栈随机化,但是可执行代码部分没有随机化。
也就是可执行代码的地址是固定的,只是执行期间的栈是随机的
7.rtarget 没有栈越界检测,不然没法写
8.ctarget没有任何保护
ctarget-level1:
只需要简单的利用缓冲区溢出覆盖掉栈帧顶内存所存的栈指针即可。
在GDB中利用 disassemble <函数名> 指令可以很容易的得到函数的地址,得到touch1函数的地址之后,覆写上即可,注意需要倒写。
ctarget-level2:
开始需要注入代码。
注入代码的原理:先将代码注入缓冲区,之后利用函数返回时的ret指令并通过覆盖栈指针的方式,让机器跳转,并去执行我们所希望的指令的地址。
需要注意的是,ret指令在add $…, rsp之后,也就是说,栈空间已经被回收,之后在执行函数的时候这部分栈空间很可能被占用,所以不能用来存取变量。
注入代码的步骤:
1.首先获取缓冲区的地址。
2.写出注入的代码的汇编语言,利用gcc -c 和 objdump -d得到机器指令。(因为不使用jump,为了实现函数跳转,可以参考上面的第四点)
3.将得到的机器指令写在缓冲区的头部(不在头部也行,只要保证有足够空间容纳机器指令并且明确好机器指令的地址即可)
4.用缓冲区的地址覆盖掉栈指针
之后只需要参考touch2函数,然后把cookie存入%rdi即可。
ctarget-level3:
同样是注入代码。有两种方法。
方法一:
hex2match中,random函数为伪随机,每次运行得到的随机数一样。
hex2match用于判断给出的字符串是否和想要的字符串一样。
那么,只需要让给出的字符串的地址和想要的字符串的地址一致即可,也就是让同一个字符串进行比较。
这样就基本和ctarget-level2基本一样,只是把参数换成了地址而已。只需要先gdb跟踪,得到在比较的时候给出的cookie的字符串的地址,然后作为函数参数传递即可。
PS:这样的做法不太符合CSAPP希望做的方法。
方法二:
考虑怎么传递一个字符串,我们需要把字符串在代码注入的时候就写入内存,然后把这个地址传递给hex2match,同时保证内存不被破坏。
把字符串在注入时就写入内存不难,得到字符串的地址也不难,难点在怎么保证内存不被破坏。
考虑把字符串存在某个位置,比如:全局变量,或者栈空间。
考虑全局变量,查看汇编语言,很容易发现vlevel是一个全局变量,同时我们也知道cookie也是一个全局变量,我们可以考虑把字符串存放在这个地方。但是我没有做到,原因是空间无法使用,他会提醒我不可达内存。
考虑栈空间,从getbuf的栈空间考虑,栈是从上向下拓展的。而在hex2match以及random中,会开辟并使用到很多栈空间,也就是栈会向下拓展,并被使用到,而getbuf在ret之前会回收栈空间,因此存放在getbuf的栈空间及其下方,不合适。考虑存放在getbuf上方,查看汇编语言,发现test函数有一片栈空间,可以把字符串存放在test函数的栈空间中,具体方法为,getbuf函数的栈空间上方的栈指针的上方。
这时我们考虑好了字符串存放位置,注意要倒写,可以思考一下为什么需要倒写。
最后把字符串放在对应位置,然后把这个字符串的地址当做函数参数传递即可。
rtarget-level4
这是我唯一一个一次过的,前面几个都抠抠索索反复看了好几遍书花了好几个小时才搞明白怎么弄,只有这个弄得比较快。
rtarget开启了限制可执行区域和栈随机化,但没有开启栈破坏检测,也就是我们仍然可以覆盖栈指针,让程序跳转到我们希望的地方,这得以让rtarget可以破解。
方法依旧是通过缓冲区溢出,控制ret时的转移方向,但是注意,由于限制了可执行区域,也就是栈空间的字节不会被执行。因此不能进行注入,而是利用叫ROP的方法。
思路分析:
因为注入不了代码,所以缓冲区的部分实际上压根就没有意义,也就是这40字节不管是什么都无所谓。需要考虑的是这40字节之后的。回想ctarget-level2所注入的汇编代码,所做的事情为把%rdi赋值为cookie,在这里也是一样。只是因为无法注入代码,因此写的别扭一些。
利用movq, popq, 以及栈空间,栈指针的之间的配合实现一样的事情。至于怎么存放cookie,可以考虑放在栈空间中,然后利用上面 “需要注意的点” 中的第四点,实现赋值。最后将值传递给%rdi,再跳转到touch2。
然后盯着3A表和3B表中movq, popq这两个机器指令,去一个一个找farm.c中的函数的后几位对应的字节。拼凑出指令。
最后写出字节码即可。
lab3总结:
attacklab比bomblab更进一步,bomb只需要读懂汇编语言即可,而attack需要深入理解栈,函数调用以及中间内存分配的过程,同时运行在64位的平台,也是很贴近实际的实验。虽然以后不会做hacker,但是也能深刻的理解计算机运行的过程。最后,rtarget-level5没有写~
LAB4 BUFBOMB LAB(第三章之后)
2025.2.4-2025.2.4
这个LAB4 BUFBOMB LAB基本和LAB3 ATTACK LAB一致,只有level3和level4略微超出LAB3范围。还有就是LAB4使用的是x86-32,LAB3使用的是x86-64,这导致汇编语言有些许不同,以及其他方面在标准上不同。
需要注意的点:
1.关于函数传递
bufbomb lab是x86-32,而不是x86-64,与attack lab不同
x86-32参数传递的方法是倒序压入栈
x86-64参数传递的方式是使用%rdi等寄存器,不够用了再用栈
2.关于返回栈指针在内存地址的存放位置。
x86-64是直接放在栈帧之上。
x86-32存在一个保存原来%ebp的操作
因此栈帧再加4字节(因为x86-32是32为地址,所以加4字节)
所以返回栈指针在内存地址的存放位置为栈帧+4字节之上
3.关于push
因为是x86-32系统,push不可能一次性压入8个字节
因此可以考虑把八位立即数分成高低各4个字节,分两次压入栈中,或者进行字节对齐
这样就可以在使用的时候使用 mov 0x8(%ebp), %eax 的形式直接把立即数送给 eax
4.关于在不修复时为什么段错误
在进行level3的时候,要求修复栈,而且要求在完成getbuf返回时,local值不变。
必须要知道的是,只修改栈底的返回栈指针并不会破坏local值。
这点可以通过尝试得知,可以先注入代码,不做特别的事情,只是返回getbuf函数下方。
记住调用getbuf前的%esp, %ebp,然后gdb的x指令查看local值所在内存
可以发现,local值没有发生变化
那么为什么会在之后出现段错误,其实是因为esp和ebp被破坏导致的
在gdb下使用info frame 1,可以看到返回了错误的信息,这是导致段错误的原因。
5.关于%esp和%ebp之间的关系
x86-32中,在一个栈空间中,完成栈空间的拓展之后,leave之前,存在如下关系:
%ebp = %esp + 拓展的栈空间
6.关于项目的一个说明
buflab32.pdf的level4说:
从一次运行到另一次运行,尤其是不同用户之间,给定过程使用的精确栈位置会有所不同。
这种差异的一个原因是,当程序开始执行时,所有环境变量的值都放置在栈的底部附近。
环境变量作为字符串存储,根据其值需要不同的存储量。因此,为给定用户分配的栈空间取决于其环境变量的设 置。
其实就是传入的参数 -u <用户名> 中用户名字符串长度不同导致的
C语言的main函数声明为 int main(int argc, char *args[])
这和 char *args[] 有一定关系
level0 & level1:
level0与level1几乎和LAB3的ctarget-level1和ctarget-level2一模一样,除了一个是x86-32一个是x86-64,但是需要注意的点已经在上面标注了。
但是CSAPP所希望的level1的解法可能不是代码注入,通过覆盖栈底内存,保证跳转之后pop出来的内容正好是cookie,这是因为x86-32使用的是push和pop来访问参数的,所以可以这么做。这样的方法更加简单。
PS:有一个坑是buflab32.pdf中说缓冲区大小是32字节,但是实际上应该以汇编语言的为准。。。。
level2:
这个就和ctarget-level2更像,需要注入代码,但是他希望修改global_value的值,让他等于cookie,同时bang函数的参数根本没有用到。
`int global_value = 0;`
`void bang(int val)`
`{`
` if (global_value == cookie) {`
`printf("Bang!: You set global_value to 0x%x\n", global_value);`
`validate(2);`
`} else`
`printf("Misfire: global_value = 0x%x\n", global_value);`
`exit(0);`
`}`
所以我们不需要管这个参数的问题,只需要在注入的代码修改global_value即可,方法很简单,先gdb通过info variables找到global_value的地址,然后修改这个地址,修改为cookie,之后再跳转到bang函数即可。
level3:
level3有一些超出ctarget的范围了,要求我们复原栈空间其实上面注意的点中第四条其实说了,local值不会因为返回栈底指针那部分内存被破坏而被破坏,只要记录好ebp,esp的值,然后把返回地址记录好就可以完美还原栈空间的状态,就像是正常的调用了getbuf一样。之后就是把getbuf的返回值也就是%eax修改为cookie即可。
至于为什么只需要修复ebp,esp寄存器的值就可以完成栈的复原,其实也跟函数的调用过程有关,在x86-32中,ebp会记录返回位置,esp则在栈。可以看一眼书这里。
在level3这里我走了一个弯路,我以为在破坏getbuf函数的栈底的时候local值也会被破坏,然后想着在注入的代码中call一次uniqueval函数,但是一直不成功,后来才想起来,栈底的返回地址是通过金丝雀来保护的,而不是像local这种保护方式。
level4:
level4其实差不多和level3一样,但是更进一步,同时有一个和level0一样的坑。
方法其实还是注入代码还原现场,然后把%eax改为cookie,最后返回testn中getbufn之后的位置。
但是存在栈的随机,好在变动幅度在buflab32.pdf中给出了,是±240字节,所以只需要480字节的nop雪橇,就可以让注入的代码总是顺利执行。
不过有两个问题:
一是怎么还原现场,因为存在随机,esp和ebp的值不固定。
二是雪橇应该从哪里开始滑
对于问题一上面需要注意的点第五条已经差不多指出了,因为我们需要把现场还原成testn调用getbufn之后的样子,也就是testn函数本来的样子。所以只需要去研究testn函数中esp和ebp之间的关系即可,最后发现,esp和ebp的关系后。分析怎么写,因为在getbufn结尾的ret之前有一个leave,leave会pop %ebp,所以ebp的值会被污染,而esp的值不为所动,所以只需要用esp的值推导出ebp的值即可。
对于问题二,由于不清楚这个栈的变动范围,预期读代码去判定到底是怎么个变动范围,不如直接尝试。先gdb去查看第一个测试样例中 getbufn 中缓冲区的位置,然后设置雪橇;之后通过第一个测试样例之后,再去查看第二个测试样例缓冲区的位置,依次类推。最后根据五个样例所推导出雪橇即可。
lab4总结:
总的来说,LAB4 BUFBOMB LAB和ATTACK LAB差不多,都是根据缓冲区漏洞去输入特定的字符串。只不过一个是x86-32,一个是x86-64,写完lab3之后写lab4很快,一天就写完了。这可能也是CSAPP的教学目的,让学生既熟悉x86-64也了解x86-32。
LAB 5 & LAB6 Architecture Lab 放弃(第四章之后)
因为一些个人原因,我放弃了 Architecture Lab 。
其实是我实在是不想写汇编,读汇编姑且可以,但是真的不想写汇编了。正好 2015 年 cmu15213 没这章课程。
如果以后有机会的话,会回来补坑。
LAB 7 CACHE LAB(第五章之后?)
2025.2.6-2025.6.8
PART A:
PART A其实还是很简单的,只需要照猫画虎写出一个 lru 算法即可。而且不考虑越界的情况,因此比较简单。
需要注意的是地址的结构: 六十四位地址, 2^s组,每组 E 块,每块 2^b 字节。
所以地址构成为:Tag(64 - s -b 位) | Index(s 位) | Offset(b 位)
至于为什么这么设置,其实更多可以在第四章或者计算机组成原理中找到答案。
hit 则是,通过中间的 Index 的 s 位,也就是 (address >> b) & ((1 << s) -1) 获得组号,然后在组内先检查Tag,如果有某一块 Tag 相等,则 hit。
这是因为 cache.pdf 中指明了,不会出现块越界的情况。比如:块为[0x0001000, 0x0001111],有一个访问为 0x0001110 开始的 2 个字节,那么就需要读取 [0x0001110, 0x001000] 这两个字节,就出现了越块的情况。
需要注意的是,给出地址虽然没有前缀的 0x , 但是是 16 进制的。
在 PART A 写 lru 的时候,我想到了我在复习计算机组成原理的时候看的课程中另类的适合于较小数据量的写法,但是后来发现,不行,原因在于可能同时存在两个及以上可以驱逐的块。但是通过队列实现的有且仅有一个块应该被驱逐。所以后来没有采用。
别忘了指令需要跳过,以及 LOAD 和 STORE 其实是一样的都相当于访问一次内存块,对于 lru 算法来说没有区别。
我建议在写 PART A 的时候实现 -v 选项,方便调试和发现错误。
PART B:
PART B 比较难了。有一说一,我第一时间没想到要分块来着,后来参考了别人的思路才想到的。
为什么分块可以减少miss?
for (i = 0; i < N; i++) {
for (j = 0; j < M; j++) {
tmp = A[i][j];
B[j][i] = tmp;
}
}
上面代码是原版代码。
miss的主要原因在于 B, 因为 A 是按行读,其读取是连续的,符合 cache 的工作原理。
但是 B 是按列写,其地址不连续,会频繁的导致缓冲需要重新读取内存,频繁地 evict 导致miss。
那么分块之后呢?
miss主要原因在 B 按列写,导致每次写的时候只写了一个地址,缓冲块内其他内存废弃了。
比如:写 [0x000102, 0x000105],那么缓冲很可能会读入 [0x000100, 0x000111] ,也就是 [0x000100, 0x000101] 和 [0x000106, 0x000111] 这部分都没有用到,而且读取的列越多,之前读取的内存就越可能被驱逐,导致下次用到的时候反而不在缓冲中,而且miss。
分块的意义就在于利用好这原本会被废弃的内存。
例如: 8 * 8 分块,因为会直接利用这 8 * 8 的区域,利用好了缓冲会一次性读取一片内存,并且充分利用这部分内存。之后再也不需要这片内存,就减少了miss。
对于 32 * 32
首先,cache的参数为 s=5 E=1 b=5,一块可以存8个int,一共有32块,最多256个int。每次读入int会一次性填满整个块,也就是每次都会一次性读入8个int。
对于一个地址 Tag(64 - 5 -5) | Index(5) | Offset(5),让我们考虑什么时候一块会被驱逐:
首先Index会相等,其次根据PART A所说,Tag不相等。至于Offset,它与匹配无关。
也就是Index相等,而Tag不等,会导致一块被驱逐。显然,那就是每过 2 ^ 10 字节,就会导致一块被驱逐。
那么对于 32 * 32 每行 32 个int,128字节, 2 ^ 10 / 128 = 8,所以当访问 A[8][0] 的时候, A[0][0] 会被驱逐,反之一样。因为他们的低 10 位相等,而Tag位不等。
考虑到分块方法,既然每次都会读入8个int又考虑到 A[8][0] 和 A[0][0] 不能同时出现,那么自然会想到 8 * 8 分块。
循环中每次利用8个临时变量按行一次性读取 8 个A数组的数字,然后写入对应的 B 数组中 ,然后重复 8 次。
考虑怎么写入 B 数组,既然 A 数组有按行读,那么 B 数组能否按行写?
答案是可以的。
我们先把每一行 8 个的 A 数组直接先按行写入 B 数组中,然后在 8 * 8 写入 B 数组之后,对 B 进行转置即可。
为什么可以直接转置? 因为这个时候 B 数组的这 8 * 8 位全部都在缓冲中。
对于 64 * 64
可以参考:CSAPP - Cache Lab的更(最)优秀的解法 - 知乎
我的解法说是在这篇文章中抄来的也不为过。
首先,直接把 32 * 32 的代码放上去会有很高的miss。
分析:
cache 是 32 组, 每组一块, 一块内 32 字节
地址组成: Tag(64 - 5 - 5) | Index(5位) | Offset(5位)
所以对于 A[4][0] 和 A[0][0], 因为一行为 64 个int,共 256 字节。
256 * 4 = 1024 字节,这个时候 正好 A[4][0] 和 A[0][0] 的地址的低 10 位相同。
所以会发生分到同一组的情况,并且导致 A[0][0] 被驱逐。因此 miss 率超高。
一次最多处理 4 行,考虑 4 * 8, 也不行,因为这样解决不了写入 B 的时候同样情况。
必须以 4 * 4 为单位进行处理,可是这样会导致每一行同时读入的 8 个 int 中后面会有 4 个int 用不到。
下次再使用的时候会导致重新读入。
-------
| A| B|
-------
| C| D|
-------
上面 ABCD 四个区域均为 4 * 4,用 AA 表示 A 的左上,BC表示 B 的左下,由此类推。
先复制, AA -> BA, AB -> BB, 注意这个地方复制的时候,利用 32 * 32 的临时变量一次读取会更优。
转置, 转置 BA, 转置 BB
再复制, AC -> BC, AD -> BD
转置, 转置 BC, 转置 BD
这个时候 BA, BD 已经完成,不需要管。
只需要按行交换 BC 即可。
需要注意的是,按行交换 BC 的时候注意方法。具体可以看上面的知乎文章。
对于 61 * 67
其实 61 * 67 才是我第一个通过的一个测试点。因为最开始的时候我希望一份代码过三个测试。但是后来发现不太行,在这期间发现61 * 67很容易通过。
只需要分块大小为 16 即可。
这个简单,就不遮遮掩掩了:
for (i = 0; i < N; i += 16) {
for (j = 0; j < M; j += 16) {
for (k = i; k < i + 16 && k < N; ++k) {
for (l = j; l < j + 16 && l < M; ++l) {
B[l][k] = A[k][l];
}
}
}
}
总结:
最后成绩:
Cache Lab summary:
Points Max pts Misses
Csim correctness 27.0 27
Trans perf 32x32 8.0 8 260
Trans perf 64x64 8.0 8 1196
Trans perf 61x67 10.0 10 1993
Total points 53.0 53
哎呀,这个 LAB 真是头大,优化之优化,只能说写到最后头疼,不得已第一次看了别人分享的思路题解。
LAB 8 Perference LAB(第五章之后)
LAB 8可以说是LAB 7 的全面劣化版了,首先评分没有确定的标准,没有满分线,性能测试却没有进行环境隔离排除环境因素,甚至每次测试的结果都不一样,而且测试出来的结果依赖于机器。如果时间比较紧的话,这个 LAB 完全可以跳过,认真完成 LAB 7 CACHE LAB即可。
2025.2.9-2025.2.11
首先回顾基本的优化方法:
1.循环展开
2.减少内存引用
3.减少循环低效率
4.提高机器级并行度
因为 Perf LAB 没有像 CACHE LAB 限制临时变量和数组等条件,为了提高代码的通用性和锻炼自己,我将在 Perf LAB 中遵守 CACHE LAB 所提出的 code rule。
ROTATE :
char naive_rotate_descr[] = "naive_rotate: Naive baseline implementation";
void naive_rotate(int dim, pixel *src, pixel *dst)
{
int i, j;
for (i = 0; i < dim; i++)
for (j = 0; j < dim; j++)
dst[RIDX(dim-1-j, i, dim)] = src[RIDX(i, j, dim)];
}
看到这个naive_rotate很难不让人想起 CACHE LAB 的 PART B 优化 Transpose 函数,不说是一模一样,也可以说是长得很像。很容易想到进行分块即可。
那么分块,8 * 8, 16 * 16, 32 * 32。 在我的电脑上最佳的是 16 * 16。
之后我尝试把 CACHE LAB 的其他优化点也加入,发现没啥用,提升的速度还没有误差来的大。于是放弃。
SMOOTH :
static pixel avg(int dim, int i, int j, pixel *src)
{
int ii, jj;
pixel_sum sum;
pixel current_pixel;
initialize_pixel_sum(&sum);
for(ii = max(i-1, 0); ii <= min(i+1, dim-1); ii++)
for(jj = max(j-1, 0); jj <= min(j+1, dim-1); jj++)
accumulate_sum(&sum, src[RIDX(ii, jj, dim)]);
assign_sum_to_pixel(¤t_pixel, sum);
return current_pixel;
}
char naive_smooth_descr[] = "naive_smooth: Naive baseline implementation";
void naive_smooth(int dim, pixel *src, pixel *dst)
{
int i, j;
for (i = 0; i < dim; i++)
for (j = 0; j < dim; j++)
dst[RIDX(i, j, dim)] = avg(dim, i, j, src);
}
分析:
1.大量函数调用开销 -> 只需要把函数调用换成宏即可。
2.大量重复计算导致的高计算开销。 -> 需要换成更优的算法。
重复计算在哪?
比如计算 dst[1][1],需要计算 src[0][0] -> src[2][0] src[0][1] - > src[2][1] src[0][2] - > src[2][2] 九个点。
接着计算 dst[1][2],需要计算 src[0][1] -> src[2][1] src[0][2] - > src[2][2] src[0][3] - > src[2][3] 九个点。
很容易发现,这中间有六个点重复。
记 a = src[0][0] -> src[2][0], b = src[0][1] - > src[2][1], c = src[0][2] - > src[2][2], d = a + b;
那么计算 dst[1][2] 的时候只需要计算 e = src[0][3] - > src[2][3] 然后 e + d 即是dst[1][2]答案。
接着,令 d = e + b 即可,a,c只需要在计算边界的时候需要,之后循环中,只需要 d,e,b即可。
这么做之后,计算一个点只需要 src[i +1][j - 1] - > src[i +1][j + 1] 两次加法,加上 e + d 一次加法和 d = e + b 一次赋值。
大大减少了计算量。
注意处理好边界条件即可。
另一种方法:
可以考虑前缀和,那么一个点就是 sum[i + 1][j + 1] - sum[i - 2][j + 1] - sum[i + 1][j - 2] + sum[i - 2][j - 2]。
但是这么做其实并不会快多少,因为每次的计算量没少太多。而且考虑到尽量减少数组的设立,利用分块 + 前缀和的方法的话,边界条件处理很麻烦,而且也不通用,最后我没有实现,
总结:
perf lab是很简单的一个实验,因为没有一个具体的评分标准,其含金量不如 CACHE lab。而且因为误差导致的波动比优化带来的提升大的多。
我的最终结果:
Teamname: DogDu
Member 1: DogDu
Email 1: dog_du@foxmail.com
Rotate: Version = naive_rotate: Naive baseline implementation:
Dim 64 128 256 512 1024 Mean
Your CPEs 2.3 2.5 2.7 2.9 6.7
Baseline CPEs 14.7 40.1 46.4 65.9 94.5
Speedup 6.3 16.3 17.5 22.7 14.2 14.2
Rotate: Version = rotate 16 * 16分块:
Dim 64 128 256 512 1024 Mean
Your CPEs 2.6 2.6 2.7 2.6 2.8
Baseline CPEs 14.7 40.1 46.4 65.9 94.5
Speedup 5.7 15.5 17.4 24.9 34.3 16.8
Smooth: Version = smooth -- 重复使用计算结果,可适用于 dim >= 2:
Dim 32 64 128 256 512 Mean
Your CPEs 14.0 14.1 14.3 14.7 15.1
Baseline CPEs 695.0 698.0 702.0 717.0 722.0
Speedup 49.7 49.4 49.0 48.7 47.7 48.9
Smooth: Version = smooth -- 分块使用前缀和,仅可用于 dim % 16 == 0, 因为边界条件比较复杂而且不通用,懒得实现了。:
Dim 32 64 128 256 512 Mean
Your CPEs 88.2 89.2 89.2 92.1 92.2
Baseline CPEs 695.0 698.0 702.0 717.0 722.0
Speedup 7.9 7.8 7.9 7.8 7.8 7.8
Smooth: Version = naive_smooth: Naive baseline implementation:
Dim 32 64 128 256 512 Mean
Your CPEs 87.7 88.7 89.2 89.5 90.1
Baseline CPEs 695.0 698.0 702.0 717.0 722.0
Speedup 7.9 7.9 7.9 8.0 8.0 7.9
Summary of Your Best Scores:
Rotate: 16.8 (rotate 16 * 16分块)
Smooth: 48.9 (smooth -- 重复使用计算结果,可适用于 dim >= 2)