forked from GHScan/TechNotes
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path8.html
5 lines (4 loc) · 41.3 KB
/
8.html
1
2
3
4
5
<head>
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
</head>
<div>■ 5. CSAPP:完成Data_Labs<br>■ 6. 计算机英语:hot spot[热点]<br>■ 6. 代码大全:第25章,代码调整策略<br> ■ 性能只是软件质量的一个方面,通常不是最重要的方面;精细的代码调整是性能调整的一种方式,但通常不是决定性的方式。程序的架构、细节设计以及数据结构、算法选择通常对速度和资源占用的影响更大<br> ■ 为性能优化做好准备的最佳方式是在最初阶段编写清晰的代码,从而让后续工作更容易理解和修改<br> ■ 如果代码调整没有带来预期的优化效果,果断放弃改动<br>■ 7. 代码大全:第26章,代码调整技术<br> ■ 这一章末尾的核对表可以参考,估计一下哪些调整在所有的语言中都有用;哪些会被优化编译器给抵消掉<br> ■ 如果某项优化的重要性不够,那不值得为它做profiling,不值得为它损失可读性和可维护性<br> ■ 未经测量的代码调优对性能的改善是投机,但对可维护性的负面影响是确凿无疑的<br> ■ 优化结果在不同的语言、编译器和系统环境下,差异很大,除非进行profiling,否则无法判断改动到底是优化还是劣化<br> ■ 第一次优化通常不会是最好的,即使效果不错,也要尝试继续扩大战果<br>■ 7. 代码大全:第27章:程序规模对构建的影响<br> ■ 项目规模增大,沟通成本变高(复杂度为人数的平方),需要方法论来处理沟通中的问题。(每天起床上班,也算一个最简单的方法论)<br> ■ 大项目的每千行bug数和生成率都低于小项目<br> ■ 项目越大,构建的主导地位越低,集成和架构比例变大<br> ■ 放大轻量级的方法论比缩小重量级方法论,更有效<br>■ 8. 代码大全:第28章:管理构建<br> ■ 好的编码实践可以通过“贯彻标准”或者“使用更灵活的方法”来达到<br>■ 8. 代码大全:第29章:集成<br> ■ 增量集成有若干变型,除非项目非常小,否则任何一种形式的增量集成都比阶段集成好<br> ■ 针对特定项目,最佳的集成步骤通常是自顶向下、自底向上、风险导向及其他各种方法的组合。T型集成和竖直分块集成通常工作得很好<br> ■ daily build能减少集成问题,提高开发者士气,和提供一定的项目管理信息<br>■ 8. 代码大全:第30章:编程工具<br> ■ 程序员有时会在长达数年的时间里忽视某些最强大工具,然后才发现并使用<br> ■ 好的工具能让你日子更安逸<br> ■ 下面这些工具已经成熟了:编辑、代码质量分析、重构、版本控制、除错、测试、代码调整<br> ■ 好的工具能减少编程中最单调枯燥的工作的量,它能持续重塑"编程"的含义,但并不能消除对"编程"的需要<br>■ 10. 代码大全:第31章:布局与风格<br> ■ 可视化布局的首要任务是呈现代码的逻辑组织,评估标准包括准确性、一致性、易读性、可维护性;美观性是最不重要的,然而,一旦其他指标达到了,外观往往也不错<br>■ 10. 代码大全:32章:自说明代码<br> ■ 代码本身就是最好的说明,如果代码需要大量注释,应该改进代码而不是增加注释<br> ■ 注释应该用于说明代码无法说出的东西:如用意、概述、维护者信息、版权等<br> ■ 需要大量重复性维护工作的注释应该放弃<br>■ 10. 代码大全:第33章:个人性格<br> ■ 编程工作本质上是项无法监督的工作,因为没有人真正清楚你正在做什么<br> ■ 程序员都是大忙人,常常没有时间去考虑怎样改进自己的工作。<br> ■ 一旦决心成为出色程序员,你的发展潜力是巨大的,不同程序员创建某个程序、调试程序、程序实现规模、速度、错误率和错误数均可达到10:1<br> ■ 你无法提升自己的聪明程度,但性格可以在一定程度上改进;个人性格对于造就程序高手更具决定性作用<br> ■ Dikstra:大部分的编程工作都旨在弥补我们有限的智力。精通编程的人都是了解自己大脑有多大局限性的人,都很谦虚;而那些编程糟糕的人,总是拒绝承认自己的脑瓜不能胜任工作的事实,自负使他们无法变得优秀。承认自己智力有限并通过学习弥补,你会变得更好。越是谦卑,进步越快<br> ■ 承认智力上的局限,善用这些编程拐杖:(1)将系统分解,使之易于理解(2)进行复查、评审、详查和测试,避免人为失误(3)将子程序编写短小,减少大脑的负荷(4)基于问题而不是底层实现细节来编程,减少工作量(5)通过规范将思路从繁琐的细节中解放出来<br> ■ 培养求知欲和把学习当做第一要务:(1)建立自我意识,如果工作学不到什么,那就换一份工作(2)试验。用小程序检验某一概念比不了解大程序中的行为要好(3)阅读解决问题的方法。发明轮子注定不会成功,你发明的可能是方轮子(4)在行动之前多分析。多数程序员需要被担心的都不是分析过度(5)学习成功项目的经验。研究高手的代码,了解专家对你的看法(6)阅读文档(7)阅读其他书本。每两月一本计算机书能让你脱颖而出(8)同专业人士交往(9)向专业开发看齐<br> ■ 编程生涯成熟的部分标志是发展出一种不屈不挠的诚实感:(1)不是高手时不假装是高手(2)乐于承认错误(3)慎重对待编译器警告(4)渗透理解自己的程序,而不只是看是否能运行(5)提供实际的状况报告(6)提供现实的进度方案,在上司面前坚持自己的意见。上司有权利了解真实的项目成本,以做出全局上的判断和决策。妥协会让自己失去信用,而坚持观点可能会赢得尊敬。<br> ■ 交流和合作:真正优秀的程序员知道怎样融洽的和他人工作、娱乐。编程首先是与人交流,其次才是和计算机交流<br> ■ 创造力和纪律:不要将创造力花到无关紧要的地方,在非关键之处建立规范,在重要的地方发挥你的创造性。有很强创造力的人都极力遵守纪律,精致的程序作品也要求有许多约束<br> ■ 懒惰:编写某个工具来完成不喜欢的任务,以后便再也不用做这样的事。人们容易混淆行动与进展,混淆忙碌与多产。有效编程中最重要的工作是思考,而人思考时通常不会看上去很忙。如果和我共事的程序员一直忙个不停,那他肯定不是优秀程序员,因为他没用好他的脑袋<br> ■ 某些性格因素不像在其他领域一样起作用:(1)坚持。当在某段代码上卡壳时,不妨另辟捷径,重新设计类;或者绕过去回头再试。知道何时放弃很难,但必须面对(2)经验。软件开发的知识变化很快,晚10年毕业的人学到的有效技术可能是你的两倍,如果不能与时俱进,经验会是累赘。所谓5年经验的C程序员的说法:程序员过了前两年仍然没学好C语言,再加3年也没用。工作10年,你得到的是10年经验,还是1年经验重复10次?(3)编程狂人:可以热爱编程,但热情不能代替熟练的能力,想明白什么更重要<br> ■ 习惯:程序员大部分的事情都是无意识完成的。你不会在编程很多年后突然问“怎样才能使这个循环更快呢”,“如何让这段代码更好懂”,优秀程序员早就养成习惯。比尔盖茨:日后出色的程序员在头几年就做得很好。初涉某事,就应端正态度,积极思考,因为之后习惯会开始起作用,因此请确保习惯是好的。如果不幸养成了坏习惯,比起用“没有习惯”来代替“坏习惯”,可以试着养成“有用的新习惯”<br> ■ 总结:对编程能力影响直接的性格为:谦虚、求知欲、诚实、创造性和纪律,以及高明的偷懒<br> ■ 总结:很多程序员不愿意主动吸收新知识,只靠工作时偶尔接触新信息。如果你能抽出少量时间阅读和学习编程知识,你将鹤立鸡群<br> ■ 总结:要成为杰出程序员,先养成好习惯,其他自然水到渠成<br>■ 11. 代码大全:第34章:软件工艺的话题<br> ■ 管理复杂度是软件的首要技术使命,许多编程实践背后的动机也正是为了降低程序复杂度,复杂度几乎是衡量程序员成果的最重要依据<br> ■ 编码规范主要也是为了降低复杂度,但如果它们在更有意义的地方反而成了桎梏,就放弃<br> ■ 各种形式的抽象都是管理复杂度的强大工具,计算机科学在这方面的成就包括:从机器语言到高级语言,子程序,类,包<br> ■ 多个程序员参与的项目,组织性的重要超过了个人技能;人们一起工作时是珠联璧合还是相互抵消,取决于团队采用的开发过程。<br> ■ 你所采用的开发过程决定其需要的需求稳定度,以及能够承受怎样的不稳定。比如采用增量开发法<br> ■ 不成熟优化是一个过程性错误<br> ■ 按照先写伪代码然后再填充的步骤,能享受自顶向下的好处<br> ■ 关注过程,意味着应暂停一下去留意自己构建软件的方式<br> ■ 可读性对程序有以下正面影响:可理解性;容易复查;错误率;调试;可修改性;开发时间;外在质量<br> ■ 不要将编程思路局限于所用语言支持的范围,杰出的程序员会考虑他们要干什么,然后才是怎样用手头的工具去实现他们的目标<br> ■ 规范是一套用于管理复杂度的智力工具。(1)规范能够传达重要信息。如命名规范(2)规范可以免除各种风险。如禁止全局变量,要求表达式加括号(3)规范能够弥补语言的不足。如在动态语言里支持枚举和常量<br> ■ 大型项目有时会规范过头,光记忆就破费时间;小型项目则缺少规范,没能体会到其好处<br> ■ 基于问题域编程:处理复杂问题的特殊方式就是尽可能的工作于更高的抽象层次,针对问题来工作而非针对计算机方案。如顶层代码不应该充斥文件读写和各种数据结构实现,这些细节应该被隐藏起来<br> ■ 编程既非完全的艺术也非完全的科学,它是介于二者之间的工艺,创作过程中需要好的判断力。当代码中有tricky、类中的错误数高于平均、程序中缺陷异常多、代码有重复或若干修改很相似、某些程序难以理解、编译警告,都是危险信号,考虑是否应该重写。<br> ■ 不要盲目跟风,可以用激动人心的新方法做试验,但仍扎根于传统的可靠的方法<br> ■ 合作开发要求团队成员之间广泛沟通,甚于同计算机交流<br> ■ 编程规范一旦滥用,只会雪上加霜;使用得当则会为开发环境带来良好机制,有助于管理复杂性和沟通<br> ■ 开发时迭代次数越多,产品的质量越好<br> ■ 墨守成规的方法有悖于高质量的软件开发,请将编程工具箱中填满各种编程工具,不断提高自己挑选适合工具的能力<br> ■ 如果没有充分了解问题就定下解决方法,说明你还不够成熟。受限于所坚持的思路,你很可能与最有效的方法失之交臂<br>■ 11. 代码大全:第35章:何处有更多有用信息<br> ■ 你犯的错误别人早就犯过,不想自讨苦吃,就去读他们的书<br> ■ 三类书:(1)解释有效编程的基础概念(2)解释编程技术管理及知识背景(3)编程语言、操作系统、环境和硬件等特定主题参考书<br>■ 11. SSE test : vector transform<br>■ 12. SSE test: memcpy<br>■ 12. CSAPP: 第3章第0节<br> ■ C语言相对于汇编的优点:(1)类型检查(2)一致的访问(不会有某处用作指针而另一处用作整数的问题)(3)优化编译器能产生至少相当于汇编高手的优质代码(4)一次编写,在不同平台编译<br> ■ 程序员学习汇编的理由变了:开始是要求能直接写汇编程序,现在是要求能阅读理解优化编译器的输出,分析代码中潜在的低效<br>■ 12. CSAPP:第3章第1节:历史的观点<br> ■ intel的处理器历史:8086(16位处理器,20位的地址总线,1M中能用640K,其他给系统)->8087(FPU作为协处理器)->80286->i386(32位,平坦地址模式,首次支持Unix)->i486(将FPU整合到CPU中)->Pentium->PentiumPro->Pentium/MMX(64位的SIMD)->Pentium2(整合Pro和MMX)->Pentium3(SSE,L2)->Pentium4(SSE2)<br> ■ intel没有用i586而是用Pentium商标,是因为无法拿到CPU编号的商标保护,美国商标局不允许用数字作为商标<br> ■ intel创始人Moore提出摩尔定律<br> ■ AMD生产的是intel兼容处理器<br> ■ GCC默认为i386产生代码<br> ■ x86的CPU是little endian的,对比motorola的PowerPC是big endian<br>■ 12. CSAPP:第3章第2节:程序编码<br> ■ 汇编相对于C特有的:指令计数器;整数寄存器;浮点寄存器;条件控制字;SIMD寄存器等<br> ■ C语言将内存看做各种类型的集合,而对汇编而言,只有寄存器有类型,内存只是字节数组<br> ■ 汇编指令类型很有限:数据传输;算数运算;逻辑运算;条件跳转<br> ■ 查看汇编:(1)GCC -S main.cpp(2)用objdump来反汇编,objdump -S -d main.o<br> ■ gcc产生的汇编/objdump反汇编出来的码,叫做GAS,即Gnu assembler,一般其中以“.”开头的,是指导汇编器或者链接器的命令<br> ■ 由于.o文件是可重定位的,因此其中的非PC-relative(如jmp的相对跳转)地址被暂时填充为0,直到链接为执行文件才被重定位为真正的目标地址<br>■ 12. CSAPP:第3章第3节:数据格式<br> ■ 字(word)的概念本来是指机器字长,但因为intel的x86架构是16位出身的,由于历史原因,x86下的字是16位,32位叫双字(dword),64位叫四字(qword)<br> ■ 在GAS中,后缀b表示char,后缀w表示short,后缀l表示int/long,后缀s表示float,后缀t表示long double<br>■ 12. CSAPP:第3章第4节:访问信息<br> ■ ia32指令的操作数有三种寻址模式(addressing mode):(1)立即数寻址(immediate)(2)寄存器寻址(register)(3)存储器寻址。所有存储器寻址方式都只该模式的特例,Imm(Eb, Ei, s),即立即数+基址+索引+scale,其中scale可以使1,2,4,8。诸如Imm(,Ei, s), (, Ei, s)都是合法的<br> ■ GAS的源和目的操作数是和MASM及intel的规则顺序相反的。三操作数时也全反<br> ■ 数据传输指令:movl, movw, movb,movsbl, movzbl。后者是将字节扩展成字,左边追加符号位或0。另外还有pushl和popl<br> ■ lea非常强大,因为存储器寻址包括基址+索引*scale等,因此lea可以用来部分代替乘法和加法!由于lea只要一个时钟,在mul很慢的时代,lea是对乘法有效的优化。如lea 32(eax, eax, 8), ebx等价于32+9*eax。另外,形如lea 0(eax), eax这样的空操作,还可以用来代替多个nop<br>■ 12. CSAPP:第3章第6节:控制<br> ■ cmp a, b执行a-b; test a, b执行a & b。它们都影响控制字。包括cmpb, cmpw, cmpl等。访问条件码的方法包括用sete、setz、setge等直接提取位,或者用条件转移je、jz、jge等。<br> ■ 编译器实现switch时,如果case较多,且case的值比较密集(跨度不大),它可能会采用jump table来优化。jump table是C语言无法实现的<br>■ 12. CSAPP:第3章第7节:过程<br> ■ leave等价于movl %ebp, %esp; popl %ebp<br> ■ ia32中取得eip的唯一方法:call next; next: popl %eax<br> ■ eax, edx, ecx被划分为调用者保存(caller save);而ebx, esi, edi被划分为被调用者保存(callee save),即应该有显示的pushl和popl。另外,由于浮点寄存器%st(0)、%st(1)的相关性(load会影响多个st的值),所以浮点寄存器组也只能是caller save<br>■ 12. CSAPP:第3章第8节:数组分配和访问<br> ■ 指针比数组多一次存储器寻址。比如,栈上的指针a[1],相当于mov eax, 8(esp); mov ebx, 4[eax];而数组a[1],相当于 mov ebx, 12(esp)<br> ■ 编译器会采用stride = f'(x)的方式,来缓存数组增量,从而避免在循环内部做乘法,达到优化的效果<br> ■ 注意多维数组char a[M][N]的访问a[1][2]等价于a + 1 * N + 2,即多维数组的访问理论上会用到乘法,而维数越多,乘法越多,尤其当维数是运行时数据时,开销更大。但无论是静态数组还是动态数组,由于编译器对循环内数组的stride缓存优化,这个维度访问的乘法开销一般不影响<br> ■ 当表达式过于复杂或者循环层次过多时,编译器都可能进行register spilling(寄存器溢出)<br>■ 12. CSAPP:第3章第10节:对齐<br> ■ 某些计算机系统对数据类型的地址有一定要求,要求特定类型的地址必须是K(2, 4, 8)的整数倍。这种对齐可以简化处理器和存储器的接口设计<br> ■ 无论是否对齐,ia32都能正常工作,但intel建议对齐已提供更好的性能。对于栈、全局/静态变量,编译器往往有对齐策略,而堆内存,一般运行时也有对齐策略(malloc的返回一般被设计为4/8对齐)。linux的对齐策略是,sizeof(T)==2的类型,按2对齐,其他按4对齐;而windows全部按类型的size对齐。linux的策略现在不够优,这是早年为节省存储器的决策<br> ■ GAC汇编中可以放置形如.align 4的命令来对齐之后的数据。<br> ■ 验证struct的对齐规则:(1)struct内字段按其类型对齐(结合规则2,字段的最终地址也将按类型对齐)(2)struct的起始地址按最大类型的size对齐(当然,起始地址也将是更小地址对齐的)(3)struct的整体size按最大类型对齐,末尾增加padding(考虑可能会有struct数组,所以必须padding,以保证规则1)<br>■ 12. CSAPP:第3章第13节:存储器的越界引用和缓冲区溢出<br> ■ 由于C数组没有边界检查,而局部变量和栈帧信息(ebp,ret地址)都放在栈上,所以缓冲区溢出(buffer overflow)的影响可能很严重。典型问题函数如gets<br> ■ 缓冲区溢出的一个致命使用是注入漏洞入侵代码(exploit code),即将机器码作为输入,然后覆盖ret地址,以执行输入的机器码,push被覆盖的ret地址最后二次ret。结果就能执行意外的代码!<br> ■ 蠕虫和病毒都能在计算机中传播自己,其中一个区别是,蠕虫可以独立运行和复制,而病毒只是一段代码,必须依托于宿主程序<br>■ 12. CSAPP:第3章第14节:浮点代码<br> ■ FPU在8087第一次出现的时候,作为协处理器(coprocessor)得到了很高的赞誉,它是IEEE浮点的第一个实现。<br> ■ 函数返回浮点时,结果放在%st(0)中<br> ■ 当进行过fcmp后,需要访问状态字时,可以用fnstsw将状态字装入通用寄存器,然后用test检测对应位<br>■ 12. CSAPP:第3章第15节:在C程序中嵌入汇编代码<br> ■ 汇编之于C的优点:(1)性能优势。但由于优化编译器的存在,该优势消失(2)能够访问特殊寄存器和地址。如操作系统调度线程,在进行context switch时,需要保存各种通用寄存器和xmm寄存器,此需求非汇编不能完成。另外汇编也能访问特殊的地址,执行特殊动作,如cas。另外要访问状态寄存器也只能用汇编<br> ■ 汇编之于C的缺点:(1)没有类型检查,不能保证一致的访问(2)可移植性差。换CPU时需要重写整个程序<br> ■ 一种汇编的使用方式:按照C语言的calling convention来编写汇编函数,然后和C代码一起链接,如gcc main.cpp test.s -o main<br> ■ GCC支持基本内嵌汇编:asm(code-string)。缺点是直接访问汇编器,编译器不知道,因此无法进行寄存器使用上的沟通,也无法访问高级语言表达式<br> ■ GCC支持扩展ASM格式:asm(code-string[:output-list[:input-list[:overwrite-list]]])。编译器知道这里的汇编码的存在,可以协作完成功能<br>■ 14. CSAPP:完成Bomb_Labs(人肉反编译)<br>■ 17. CSAPP:完成Buffer_Labs<br> ■ level0: 执行目标函数。buffer overflow。写ret地址为目标函数人口,目标函数工作完毕就exit。这里的ebp备份随便覆盖一个值也无所谓,因为目标函数有自己的ebp(mov %esp, %ebp)。注意进入目标函数时,目标函数会将栈顶看做ret地址<br> ■ level1: 执行带参数目标函数。buffer overflow。写ret地址为目标函数入口,并且写ret地址+4为第一个参数值,执行完毕exit<br> ■ level2: 在执行目标函数前设置global。exploit code。写机器码,写ret地址为buf起始地址。机器码为:mov 123, global; (目标函数会用到global); push func; ret<br> ■ level3: 执行buff上一段额外的代码后,返回上级函数(作业中要求改写返回值,即eax)。expoit code。ret地址为buf起始地址,buf上的机器码为:mov 1234, eax; push ret地址; ret。另外,要求覆盖ebp时不改动其值<br> ■ level4:对于栈基址(ebp)会变的情况,也能正确执行额外代码后返回。机器码为:nop; nop; ... nop; nop; mov 0x28(%esp), %ebp(用正确的偏移修复ebp); mov 123, %eax; push ret地址; ret; 另外,buffer overflow的时候,ebp备份随便写,ret地址为buf+240,这里的240是可能的栈偏移,目的是当ebp变化的时候,始终ret到buf上的nop指令上,然后执行buf末尾的工作代码<br>■ 19. 程序员修炼之道:13. 估算<br> ■ 所有的解答都是估算,只是一些要求比其他的更准确,因此当被要求估算时,第一个问题应该是,解答问题的语境是什么?奶奶问你合适达到只是为了准备午餐或晚餐,而被困在水下氧气快用光的潜水员需要精确到秒的答案。<br>■ 19. 程序员修炼之道:13.5. 基本工具<br> ■ (1)编辑器。强力编辑器让你更有效率(2)源码控制系统。不丢失任何成果,比对变化(3)高超的调试技能(4)胶水语言。如python、awk,将各种魔术粘合在一起<br>■ 19. 程序员修炼之道:14. 纯文本的威力<br> ■ 二进制格式并不比文本更安全,尽管它更晦涩。如果需要安全性,请使用加密算法<br> ■ 纯文本的优点1:永远不过时。由于自描述的特点,哪怕解析程序已经废弃,只要文本数据本身还存在,就有办法解析。<br> ■ 纯文本的优点2:杠杆作用。编辑器、编译器、debuger、版本控制系统、过滤器,这些都是基于文本行的。在Unix系统下,文本格式更是进程间通信的标准接口<br> ■ 纯文本的优点3:可以作为公共通信的标准格式。如http协议,如unix程序<br> ■ 作为一个文本/二进制系统的比较:实现一个地址簿软件,分别使用二进制和文本作为存储格式,然后面对以下变更需求:增加一个新的变长字段“长度”,考虑存档向后兼容<br> ■ 思考文本vs二进制(1):对于数据的存储,有两种方式,一种是只存储内容,另外单独存储数据的解释(这里的解释可以是代码,或者元数据),而另一种,是将元数据嵌入到正文中。后者相比前者,需要更多的存储空间,和额外的解析开销,但由于元数据(即对数据的解释)总是和正文放在一起,因此不会有版本不一致的问题。因此,后者对版本号的要求不是硬性的,甚至可以仅依据数据格式来编写解析器,而前者总是要求和正文版本兼容的解析器。<br> ■ 思考文本vs二进制(2):另外一个正交的概念,是文本格式和二进制格式,文本格式是human readable的,一般也能精确的鉴别字段和类型,而二进制格式是字节流,完全丢失了类型信息和字段边界,丢掉了对基本数据的解释。二进制格式需要依赖于特定的解析器,对比文本格式,这个解析器相当于是将字段的类型、布局,做了一次预提取。文本格式有更好的可读性,却付出了额外的存储、解析成本。<br> ■ 思考文本vs二进制(3):由以上概念得到四种组合:正文+二进制:常见组合,最小的存储成本和最快的解析速度,需要额外的版本匹配的解析器。自解释+文本:常见组合,最好的兼容性和扩展性。xml、json中的key-value对是让元数据(key)在每份数据中重复,而excel的view那样使用元数据头+文本正文集的做法也是可取的。正文+文本:用途有限,如csv。用[17, 'f', 'marry']这样的文本来描述一个人,比用同样的二进制流,优势不大(尽管可以单独编辑某些字段),都需要分离的、版本匹配的解析器<br> ■ 思考文本vs二进制(4):自解释+二进制:常见的专用软件,如数据库、excel。因为二进制不可读,重复二进制的key意义不大,像xml那样通过冗余的元数据提供更好的可读性的做法就没有意义了,但是,通过唯一的元数据头描述更大的二进制正文数据集的做法,却有意义,这里的元数据头可以是文本或二进制。尽管由于元数据头的存在,提供了向后兼容行,但是由于二进制正文紧凑的存储方式,要扩展字段仍然比较困难,需要转储或者trunk。<br>■ 30. CSAPP:第4章第0节<br> ■ 一个处理器支持的指令和指令在字节级的编码称为它的ISA(instruction-set architecture)<br> ■ Intel的IA32、IBM/Motorola的PowerPC、Sun的SPARC,都有不同的ISA,不兼容,程序编译成一种ISA后不能在另一种机器上运行。IA32保持了向后兼容。<br> ■ ISA在编译器作者(以及汇编程序员)和处理器设计者之间提供了一个抽象概念层,前者只需要有哪些指令及其编码,后者设计执行这些指令的处理器<br> ■ 处理器的实际执行方式可能和ISA的隐含模型大相径庭,比如流水线、超标量、乱序(out-of-order),甚至通用寄存器并不一定是真的时钟寄存器(比如Y86中的PC)<br>■ 30. CSAPP:第4章第1节:Y86指令集体系结构<br> ■ IA32中的hlt也是停机,但在用户代码中使用会抛出异常<br> ■ CISC:读作sisc。(1)指令数很多(2)部分指令latency很大,包括寄存器和存储器间的数据传输(3)变长编码(4)寻址方式多样(5)允许对寄存器、存储器进行逻辑、算数运算(6)机器级细节对程序不可见,ISA提供抽象层(7)条件码(8)栈密集的过程调用<br> ■ 早期RISC:读作risc。(1)指令数少,<100(2)latency都很小(3)定长编码,常为4字节(4)寻址方式简单,常为基址+位移(5)只能对寄存器进行逻辑、算数运算,另外通过load、store在寄存器、存储器间传递数据(6)机器细节对程序可见(短视),甚至禁止特殊的机器码序列(7)没有条件码,需要明确的测试,结果会被放到特定寄存器(8)寄存器密集的过程调用<br> ■ RISC非常适合流水线化<br> ■ RISC和CISC之争:80年代争论。RISC优点在于简约式指令集设计、高级编译器优化技术、流水线化的处理器实现;而CISC实现特定功能只需要较少的指令,以得到更高的总体性能。争论在90年代平息。RISC引入更多的指令,暴露机器细节的做法被证明是短视,不适合新的硬件优化技术;CISC引入类似RISC的微指令,一条指令被动态翻译成多个微指令,如mov [ptr], eax被翻译为load, mov。<br> ■ PowerPC和SPARC都是RISC的。<br> ■ 实现ISA的时候,push esp; pop esp;其行为都要仔细考虑<br>■ 30. CSAPP:第4章第2节:逻辑设计和硬件控制语言HCL<br> ■ 这里提到的HCL是hardware control language,注意和HDL,hardware description language的区别。HDL被硬件设计者用于现代逻辑电路图的设计。HDL是一种高级语言,常用的是Verilog和VHDL。80年代研究者开发出了逻辑综合(logic synthesis)程序,可以根据HDL的描述生成有效的电路设计。<br> ■ 注意与、或、非门的电路图<br> ■ 时钟寄存器(clocked register):总是输出;当上升沿的时钟信号来的时候,加载输入。随机访问存储器(ram):输入读/写地址码,时钟信号来的时候,写入或读出。存储器和寄存器堆(register file)都是ram。<br>■ 30. CSAPP:第4章第3节:Y86的顺序实现<br> ■ 将处理拆成阶段,使得,即使指令的动作差异很大,但都遵循统一的序列,这样有效的降低了复杂度,让不同的指令尽量共享了最多的硬件。硬件上复制逻辑块成本很大,而且处理特殊情况的成本也很大。<br> ■ Y86的指令处理被拆成以下阶段:取指(fetch)、译码(decode)、执行(execute,访问ALU)、访存(meomry)、写回(write back,将结果写回register file)、更新PC(pc update)。这些阶段循环执行,直到遇到hlt<br> ■ 处理器的主要硬件块包括:组合逻辑、时钟寄存器、ram。其中,组合逻辑的处理有一定的时延,但不需要输入时钟,因此,只有时钟寄存器和ram需要时序。<br> ■ 各个阶段即使同时发生,仍然能得到依次执行6个阶段一样的等价效果,是因为Y86的指令设计有以下性质:处理器从来不需要为了完成一条指令的执行而去读由该指令更新的状态。故可以在一个时钟内完成所有阶段<br> ■ 书中的SEQ处理器太慢了,clock cycle等于latency,一个指令执行完毕前,不能执行下一个指令。其实一个latency内,该指令只会用到部分硬件,如果同时把其他硬件让给后面的指令用的话,这就需要引入流水线寄存器(pipeline register),提高时钟频率,最终演化成流水线化处理器(pipelined processor)。<br> ■ 书中的SEQ+将pc update阶段的硬件实现提前,彻底去掉了对eip的需求,这反映了ISA这个抽象层两侧可能完全不同的事实。SEQ+是向流水线化的PIPE的一个演化。<br>■ 30: CSAPP:第4章第4节:流水线的通用原理<br> ■ 典型的流水线:自助餐厅,自动洗车。它们都允许多个顾客同时经过系统,而不是等到某个人从头到尾走完流程再让下一个人进入。单位时间内流水线上可以服务更多的顾客,但是单个顾客的处理时间可能增加(如,虽然你不需要饮料,但你得排队等前面的顾客)。<br> ■ 流水线增加了系统的吞吐量(throughput,单位时间处理数),但也会轻微增加执行时间(latency)<br> ■ 流水线化,需要引入流水线寄存器,增加时钟频率。每个指令的执行时间中,增加了几个流水线寄存器的时间,因此latency增大了。每个时钟周期上,硬件的不同部分可以同时执行,因此,每个时钟内能同时执行N(N等于流水线寄存器数,即流水线深度)个指令。假设旧的latency为L,旧的throughput为1/L,流水线寄存器的处理时间为PT,那么,新的throughput为1/(L/N+PT),显然,由于PT的存在,将时钟频率提高N倍,得到的throughput小于N倍<br> ■ 由于指令处理不能被均匀的分割,因此时钟周期等于最大的阶段,于是其他阶段存在很多的空闲时间。例如,如果ALU需要100 ns,流水线寄存器需要10 ns,而且ALU是组合逻辑种最费事的阶段,那么,throughput不会超过1/110 ns。<br> ■ 流水线深度越深,latency中的流水线寄存器开销越大,收益会下降。现代处理器的流水线深度很大(>15,P4甚至到31),处理器设计者的职责是将处理流程拆细,得到最大的深度,提高吞吐量;而电路设计者则需要精心设计流水线寄存器,减少时延<br> ■ 由于相邻指令之间存在数据相关性(data dependency)和控制相关性(control dependency),所以流水线化的时候需要利用反馈(feedback)来处理这些流水线冒险(pipeline hazard)<br>■ 30. CSAPP:第4章第5节:Y86的流水线实现<br> ■ 分支预测(branch prediction):影响执行序列的指令有,定向跳转的jmp、call,条件跳转cjmp,以及访存跳转的ret和间接寻址的jmp、cjmp、call。其中立即寻址的jmp、call,处理器直接就能取出下一条pc,而立即寻址的条件跳转cjmp只能做分支预测,最后,访存跳转指令需要stalling整个流水线。<br> ■ 关于cjmp的立即数寻址模式,分支预测分为静态预测和动态分支预测。其中静态预测的策略包括总是选择(always taken,即取指令中的目标地址作为下一条pc,60%成功率),以及反向选择、正向不选择(backward taken, forward not taken,BTFNT,65%成功率,即总是取较小的那个候选目标)。后者依赖于这么个事实:循环跳转回loop头一般是反向跳转,而循环在执行流中比例最大。<br> ■ 由于指令间存在数据相关性(data dependency)和控制相关性(control dependency),所以流水线处理器也就相应的存在数据冒险(data hazard)和控制冒险(control hazard)。<br> ■ 控制冒险主要是因为下一个pc的不确定,对策是分支预测(包括预测失败的处理,mispredict)和暂停(stalling,当遇到ret或间接寻址时)。<br> ■ 数据冒险是因为,后一条指令可能引用前一条指令的副作用,如果引用的是状态寄存器或寄存器堆,可以用前递(forwarding)逻辑电路来将前面指令的结果直接交给后面的指令,如果引用的是存储器,则必须要暂停<br> ■ 加载/使用数据冒险(load/use data hazard),又叫load interlocked,是指,“mov mem, %eax; mov %eax, %ebx”,即前一条读了存储器,下一条要访问其结果,只能在两条之间插入bubble,从而stalling<br> ■ Y86的流水线实现有3类冒险需要特殊处理:ret,对策-stalling直到取出下一个pc;load interlocked,对策-stalling直到读完存储器;mispredict,对策-预测失败时废掉正在执行的错误指令及其副作用,在Y86中,由于流水线短、副作用要到更晚的时候才会生效所以更容易处理。<br> ■ 由于ret、load interlocked、mispredict的存在,Y86流水线的CPI(cycles per instruction)是大于1的,因为为了stalling会插入bubble<br> ■ Y86流水线没有考虑的问题1:硬件异常,当遇到hlt、地址不对齐、无效指令时都应该引发异常。应该:(1)流水线中一次只触发一个异常(尽管一个周期处理了N个指令)(2)分支预测错误时被多执行的指令不应该触发异常(3)异常指令后面的流水线上指令不应该产生副作用。具体方案是,一旦发生异常,就在流水线寄存器上产生标记并传播,到写回阶段时才调用OS的异常处理程序,这就解决了问题(1)(2);另外,将靠后阶段的流水线寄存器的异常标记反馈回来,就能屏蔽后面指令的副作用<br> ■ Y86流水线没有考虑的问题2:多周期指令。乘法、除法以及浮点运算需要较多的时钟周期,如果仍然当做一个普通流水线阶段来处理,则会增大指令的latency,因此,复杂的ALU计算和浮点运算一般被作为外部电路实现,然后和精简的流水线电路进行反馈协作。<br> ■ Y86流水线没有考虑的问题3:PIPE设计中,忽略了访存开销的多样性,尽管由于IL1(instruction level 1 cache)、DL1以及TLB(translation look-aside buffer,翻译备用缓冲,用于虚地址到物理地址的翻译)的存在,大量的访存动作的确可以一个周期完成,但cache miss会要求处理器stalling几十个周期,而page fault异常会触发OS处理程序访问磁盘从而耗费数以万计的周期<br> ■ 注意cache miss和page fault的一些区别:cache miss是硬件行为,处理器被stalling,OS不知道;而page fault是异常,会触发OS异常调度程序来将数据从磁盘加载到主存,这当中也会发生一些cache miss。一个典型的例子就是,windows任务管理器中只能看见页面错误数<br> ■ Y86的五阶段流水线,是80年代水平,今天的处理器已经达到31+的流水线深度<br> ■ 流水线每个周期只能完成一条指令,但由于超标量(superscalar,即多条流水线并行执行)的存在,IPC是能够大于1的。<br> ■ 乱序执行技术(out-of-order)能够以不确定顺序并行的执行多条指令,结果却和ISA定义的一样。<br> ■ 芯片一旦设计出来,就不能修改,因此,设计、排错和验证应该更加仔细<br>■ 31. 英文单词:conservative-保守的<br></div>