深入理解计算机系统_第五章_优化程序性能

检讨

在公司就有同事给我指出过:“大段贴代码的技术博客都是垃圾”,如今反复的体味到这句话很对。技术博客确实应该保持篇幅适中,思路流畅简洁,我最近在看自己写的深入理解计算机系统系列文章,真是又长又臭,第三章和第四张我自己都看不下去,基本是在摘抄原文中的话,那人家干嘛看我这博客,人家自己买本书不好的多?在我这里转一手,除了排版更垃圾没其他改变了,真是让人失望透顶了。接下来我写博客,一定要多加自己的理解,多画结构图,帮助自己和别人理清脉络。

章前导读

写程序一定要在保证运行正确的基础上尽可能运行的快,并且代码要清晰简洁,这样在日后需要修改代码时,其他人能够读懂和理解代码。
编写高效程序需要做到三点:1.必须选择一组适当的算法和数据结构;2.必须编写出编译器能够有效优化以转换成高效可执行代码的源代码(对于这一点,理解优化编译器的能力和局限性很重要);3.将一个任务分成多个部分,这些部分可以在多核和多处理器的某种组合上并行计算。对于优化程序的一个挑战就是尽管做了大量的变化,但还是要尽量维护代码的简洁和可读性。
程序优化的第一步就是消除不必要的工作,让代码尽可能有效地执行所期望的任务。包括不必要的函数调用、条件测试和内存引用。
为了使程序性能最大化,程序员和编译器都需要知道一个模型,用来指明目标机器如何处理指令,以及各个操作的时序特性(比如知道了时序信息,就能确定是用一条乘法还是用移位和加法的组合)。文中给出了一种图形数据流表示法(基于Intel和AMD处理器),可以使处理器对指令的执行形象化,可以利用它预测程序的性能。
了解处理器运作后,可以进行程序优化的第二步,利用处理器提供的指令级并行能力,同时执行多条指令。比如降低一个程序不同部分的数据相关程度,增加并行度,这样就可以同时执行这些部分了。
最后本章还介绍了代码剖析程序,该程序是测量程序各个部分性能的工具。可以帮助找到代码中低效率的地方,进而确定应该着重优化的部分。
研究程序的汇编代码表示是理解编译器以及产生的代码会如何运行的最有效手段之一。我们常常通过确认关键路径来确定执行一个循环所需要的时间。关键路径就是在循环中形成的数据相关链,然后回过头来修改源代码。

优化编译器的能力和局限性

GCC可以通过“-o1”、“-o2”、“-o3”来控制优化级别,更大的数字代表更大量的优化,这样可以更好的提高性能,但也会让程序规模变大,也更难以用标准的调试工具进行调试。

如果一个程序中有可能出现内存别名使用的情况(内存别名使用是指两个指针可能指向同一个内存位置),编译器对其优化的能力就大大减少了,因为这两个指针如果指向同一个内存,其结果与指向不同内存的结果大不相同,这就是一个妨碍优化的因素。如果编译器不能确定两个指针是够指向同一位置,就必须假设什么情况都可能,这就限制了可能的优化策略。

第二个妨碍优化的因素是函数调用,比如func1返回了f()+f()+f()+f(),func2返回4*f(),看起来返回的结果应该是一样,但func1调用了4次,func2只调用了1次,看起来func2的效率更高。
但如果f()中改变了某一个全局数据,例如f()的函数主体是 return counter++(这个counter被定义为全局变量,初始化为0),那调用func1之后,counter = 0 + 1 + 2 + 3 = 6,而调用func2后,counter = 0。
也就是说,如果被调用的函数有一个副作用——修改了全局程序状态的一部分(这个例子中是一个全局变量),那改变调用它的次数就会改变程序的行为。
编译器不会去判断一个函数有没有上述的副作用,而是默认最糟糕的情况,保持所有的函数调用不变。
tips:有时编译器会利用内联函数替换对包含函数调用的代码进行优化(内联函数替换就是将函数主体替换到调用处),这样就不存在上述的问题了,因为函数主体被直接替换了过来,不过要对函数进行追踪或者设置断点,或者用代码剖析的方式评估程序性能的话,就不应该使用内联函数替换了。

表示程序性能

在这里插入图片描述
上述的两个函数psum1和psum2最终的效果是一样的,当把两个函数需要的时间和n的取值范围做一个最小二乘拟合后,发现前者近似于368 + 9n,后者近似于368 + 6n。式子中n的系数成为每元素周期数(CPE)的有效值,所以我们在优化程序时,会集中精力于减小CPE。根据这种量度标准,psum2的CPE为6,优于CPE为9.0的pusm1。

程序示例

文中设计了一个程序,用来比较优化后与优化前的的CPE。程序如下:
在这里插入图片描述
其中当OP为+时,IDENT初始化为0;OP为*时,IDENT初始化为1,vec_lenget函数计算出数据结构中数据部分的长度,get_vec_element函数最终会把结构体v中的第i个数据地址赋值给val。整个函数通过宏定义,实现了对一个数据结构中所有数据的加和或乘积。
在这里插入图片描述
图中分别针对整数加法、乘法和浮点数加法、乘法比较了优化前后的CPE。

消除循环的低效率

上面的combine1函数,在for循环中调用了vec_length函数,每循环一次都需要调用该函数一次,但是调用结果又不会随着循环进行而改变,因此只需要计算一次长度,此后直接用这个长度就可以了。修改如下:
在这里插入图片描述
测试结果如下:
在这里插入图片描述这种优化称为代码移动。这类优化包括识别要执行多次但计算结果不会改变的计算。代码移动前,往往程序会在小数据集上测试,这时候看不出什么端倪,可当数据集变成100万后,这段代码就完完全全变成了危险炸弹。所以一定要避免引入这样的渐近低效率。

减少过程调用

过程调用会有很大开销,而且妨碍优化。过程调用就像是combine2代码中,每次循环迭代都会调用get_vec_element来获取下一个向量元素,而该函数中,每次都要把向量索引i与循环边界作比较,这就很明显的造成了低效率。
为了减少过程调用,直接用结构体的起始地址操作。一方面这种改变严重损害了程序的模块性和安全性,从原则上说,该结构体的使用者甚至不应该知道结构体的内容是作为数据来存储的,而不是作为诸如链表之类的某种其他数据结构来存储的;另一方面这种变化是获得高性能结果的必要步骤。
在这里插入图片描述
性能没有明显提升,显然还有其他限制性更大的操作在起作用。

消除不必要的内存引用

回顾combine2函数的代码,在每次循环中都用到了语句 *dest = *dest OP data[i],这就意味着每次循环,都要从&dest中读出数值,再写回&dest,这样的读写很浪费,因为每一次循环从&dest读出的值就是上次循环最后写入的值。
在这里插入图片描述
为了消除这种不必要的内存读写,如上更改代码,用一个临时变量acc循环积累计算出来的值,循环结束后再放入&dest中。
在这里插入图片描述
可以看到程序性能有了显著提高。
那么为什么编译器不自动的把combine3函数转换成combine4函数呢?其实由于内存别名使用,两个函数可能会有不一样的行为。
例如,设v = [2,3,5]是一个3元素组成的数据结构,考虑下面两种函数调用:
在这里插入图片描述
也就是在向量最后一个元素和存放结果的目标之间创建一个别名,函数的执行如下:
在这里插入图片描述
正因为编译器有如上的考虑,所以在编译combine3的时候,不会优化为形如combine4的代码。

理解现代处理器

之前的优化都是降低了过程调用、和一些妨碍优化的因素。想再进一步提高性能,必须考虑利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。
为了理解改进性能的方法,我们需要理解现代处理器的微体系结构。现在微处理器采用复杂而奇异的微处理器结构,其中,多条指令可以并行地执行(指令级并行),同时又呈现出一种简单的顺序执行指令的表象。
对于微处理器系统的原则有一般性了解就能够理解如何实现指令级并行了,其中有两种下届描述了程序的最大性能。当一系列操作必须按照严格顺序执行时,就会遇到延迟界限,因为下一条指令开始之前,这条指令必须结束。当代码中的数据相关限制了处理器利用指令级并行的能力时,延迟界限能够限制程序性能。吞吐量界限刻画了处理器功能单元的原始计算能力。这个界限是程序性能的终极限制。

整体操作

在这里插入图片描述
这是一个现代微处理器的简化示意图,这些处理器成为超标量,即可以在每个时钟周期执行多个操作,而且是乱序的,即指令执行顺序不一定与它们在机器级程序中的顺序一致。
整个设计有两部分:指令控制单元(ICU),和执行单元(EU)。前者负责从内存中读出指令序列,并根据这些指令序列生成一组针对程序数据的基本操作;后者执行这些操作。(乱序处理器比按序处理器相比更复杂,但也能更好的进行指令级并行)
ICU从指令高速缓冲中读取指令(一个特殊告诉存储器,包含最近访问的指令)。ICU把指令在执行之前就取出进行译码,在遇到分支时,会利用分支预测技术猜测选择哪个分支及其目标地址,再利用投机执行技术取出预测的目标地址指令,在还不确定分支预测是否正确之前就开始执行这些指令,如果之后确定分支预测是错误的,再回到之前的状态,取另一个方向的指令执行,标记为取指控制的块包括分支预测,以完成确定哪些指令的任务。
指令译码逻辑接受实际的程序指令,并将它们转换成基本操作,比如两个数相加、向内存写数据等。对于x86机器,一条指令可能会译码为多个操作。
EU接受来自取指单元的操作。通常每个时钟周期会接受多个操作。这些操作会被分派到功能单元中,它们会执行实际操作。
读写内存是由加载和存储单元实现的。加载单元和存储单元都分别有一个加法器来完成地址计算。图中加载和存储单元通过数据告诉缓存来访问内存。数据高速缓存是一个高速存储器,存放着最近访问的数据值。
使用投机执行记住对操作求值,但是结果不会存放在程序寄存器或数据内存中,除非处理器能确定应该执行这些指令。分支操作送给EU,不是确定分支该往哪里去,而是确定分支预测是否正确,如果预测错误,EU就丢弃之前计算的结果,还告知分支单元预测错了,指出正确的分支目的,然后分支单元开始新的取指。这种预测错误会导致很大的性能开销。
在ICU中,退役单元记录正在进行的处理,并确保它遵守机器级程序的顺序语义。指令译码时,关于指令的信息被放置在一个先进先出的队列中,这个信息会一直保持在队列,直到发生以下两个结果之一:1.一旦一条指令的操作完成了,而且所有引起这条指令的分支点也都被确认预测正确了,那这条指令就可以退役了,所有读程序寄存器的更新都可以被实际执行了。2.如果引起该指令的某个分支点预测错误,这条指令会被清空,丢弃计算结果。
控制操作数在执行单元间传送的最常见的机制称为寄存器重命名。当一条更新寄存器r的指令译码时,产生标记t,得到一个指向该操作结果的唯一的标识符。条目(r,t)被加入到一张表中,该表维护着每个程序寄存器r与会更新该寄存器的操作的标记t之间的关联。当随后以寄存器r作为操作数的指令译码时,发送到执行单元的操作会包含t作为操作数源的值。当某个执行单元完成第一个操作时,会生成一个结果(v,t),指明标记为t的操作产生值v。所有等待t作为源的操作都能使用v作为源值,这就是一种形式的数据转发。通过这种机制,值可以从一个操作直接转发到另一个操作,而不是写到寄存器文件再读出来,使得第二个操作能够在第一个操作完成后尽快开始。

功能单元的性能

在描述性能时一以下数值来刻画:延迟:完成运算所需要的总时间;发射时间:表示两个连续的同类型的运算之间需要的最小时钟周期数;容量:表示能够执行该运算的功能单元的数量。
发射时间为1的功能单元被称为完全流水线化的:每个时钟周期可以开始一个新的运算。表达发射时间的一种更常见的方法是指明这个功能单元的最大吞吐量,定义为发射时间的倒数。一个完全流水线化的功能单元有最大的吞吐量,每个时钟周期一个运算,而发射时间较大的功能单元的最大吞吐量比较小。
这些算术运算的延迟、发射时间和容量会影响合并函数的性能。我们用CPE值的几个基本界限来描述这种影响:
在这里插入图片描述
延迟界限给出了任何必须严格顺序完成合并运算的函数所需要的最小CPE值。根据功能单元产生结果的最大速率,吞吐量界限给出了CPE的最小界限。
例如:因为只有一个整数乘法器,它的发射时间为1个时钟周期,处理器不可能支持每个时钟周期大于1条乘法的速度。另一方面,四个功能单元都可以执行整数加法,处理器就可能持续每个周期执行4个操作的速率。但是因为需要从内存读数据,这造成了另一个吞吐量界限。两个加载单元限制了处理器每个时钟周期最多只能读取两个数据值,从而使得吞吐量界限为0.5。

处理器操作的抽象模型

我们可以用数据流表示处理器上执行的机器级程序性能,这是一种图形,展现不同操作之间的数据相关是如何限制它们的执行顺序的。这些限制形成了图中的关键路径,这是执行一组机器指令所需时钟周期数的下界。
在这里插入图片描述
以乘法为例子,数据操作流有两条,分别是mul和add两个操作对于acc和data[i]的修改组成,如下:
在这里插入图片描述
可见,如果mul需要5个周期,add需要1个周期,那么mul将成为关键路径,循环结束需要5n的周期。
另外,从上面的CPE图可以看到,对于整数加法,CPE为1.27,而根据关键路径图预测的CPE应该为1.0,实际值比预测值慢,说明关键路径提供的只是下界。还有其他因素限制性能,比如可用的功能单元数量和任何一步中功能单元之间能够传递数值的数量等。
总结一下,根据抽象数据流表示说明,combine4的关键路径长L* n是由对acc的连续更新造成的,这条路径将CPE限制为最多L。对于整数加法,测量出的CPE大于关键路径所期望的CPE。

循环展开

循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。首先,这样可以减少不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支;第二,它可以减少整个计算中关键路径上的操作数量。
在这里插入图片描述
上述代码,第一个循环中,让limit = length - 1,防止循环访问越界;本例中选择步长k = 2,即每次data[i] + data[i+1] ,在这个基础上,如果length为偶数,第一个循环就可以直接计算完全,如果length为奇数,第一个循环就会剩余一个,剩余的用第二个循环计算,注意第二个循环继承了第一个循环结束时的i。
归纳:如果像设置步长为k的循环,第一个循环上限为 length - k + 1,这种做法下称为k * 1。
在这里插入图片描述

提高并行性

加法和乘法的功能单元是完全流水线化的,这说明它们可以每个时钟周期开始一个新操作,并且有些操作可以被多个功能单元执行。硬件有这种能力,但是代码不能利用这种能力,即使是循环展开也不行,这是因为我们将积累值放在一个单独的变量acc中。在前面的计算完成之前,都不能计算acc的新值。现在我们打破这种顺序,得到比延迟界限更好性能的方法。

多个积累变量

类似整数加法或乘法这种可结合可交换的运算,可以把一组合并运算分割成多个部分,最后合并结果。在这里插入图片描述
上述代码的第一个循环中,就分别用了acc0和acc1来计算偶数位和奇数位,最后把acc0和acc1合并。这种做法称为22。
在这里插入图片描述
可以看到已经打破了延迟界限设下的界限了。
这种将循环展开k次,每次并行积累k个值,得到k * k循环展开。当k足够大时,程序在所有情况下几乎都能达到吞吐量界限。
只有保持能够执行该操作的所有功能单元流水线都是满的,程序才能达到这个操作的吞吐量界限。对于延迟为L,容量为C的机器,要求k>=C
L。
在执行这种k * k循环展开变化时,必须要看看最后能够保留原函数功能,补码运算是可交换和结合的,溢出时也是如此。所以对于整数数据类型,在所有情况下,k * k循环得到的结果与原结果相同。而浮点乘法和加法不可结合,因此可能会得到不同结果。

重新结合变换

在这里插入图片描述
这里只是改变了第一个循环中acc等式中括号的位置,称为重新结合变换,括号改变了向量元素与积累值acc的合并顺序,产生了我们称为“2 * 1a”的循环展开形式。
在这里插入图片描述
重新结合变换能够减少计算中关键路径上操作的数量,更好地利用功能单元的流水线能力得更好的性能。大多数编译器不会对浮点数运算做重新结合,因为不能保证浮点数运算可结合。

优化合并代码的结果小结

在这里插入图片描述
分别使用了消除循环条件中的低效,循环体中的过程调用,不必要的内存引用(中间变量存储),循环展开,提高并行后,CPE提升了10~20倍。

一些限制因素

上文介绍了数据流图表示中,关键路径指明了执行该程序所需时间的一个基本的下界。也就是说,如果程序中有某条数据相关链,这条链上的所有延迟之和等于T,那么这个程序至少需要T个周期才能执行完。
功能单元的吞吐量界限也是程序执行时间的一个下界。也就是所,假设一个程序一共需要N个某种运算的计算,而微处理器只有C个能执行这个操作的功能单元,并且这些单元的发射时间为I。那么这个程序至少需要N*I/C个周期。

寄存器溢出

如果并行度超过了可用的寄存器数量,那么编译器就会将某些临时值存放到内存中,通常是在运行时堆栈上分配空间。
在这里插入图片描述
现代X86-64处理器有16个寄存器,并可以使用16个保存浮点数的寄存器,一旦循环变量的数量超过了可用寄存器的数量,CPE就变差了。

分支预测和预测错误处罚

分支预测不准的时候回招致预测错误处罚,现代处理器的工作远远的超前于当前正在执行的指令,只要指令遵循一种简单的顺序,那么这种指令流水线化就能很好的工作。而出现分支预测时,使用投机执行的处理器会执行预测的目标处的指令,如果之后验证预测准确,就更新寄存器,反之撤销。一个C语言程序员怎么能够保证分支预测处罚不会阻碍程序的效率呢?
1.不要过分关心可预测的分支
我们已经看到错误的分支预测的影响可能非常大,但是这并不意味着所有的程序分支都会减缓程序的执行。
2.书写合适用条件传送实现的代码
分支预测只对有规律的模式可行,对于完全不可预测的,依赖于数据的任意特性的程序,预测会一塌糊涂。如果编译器能够产生使用条件数据传送,而不是使用条件控制转移的代码,可以极大地提高程序的性能。
条件控制转移是指用条件控制程序执行的方向,然后去执行那个方向的运算,如下图:在这里插入图片描述
条件数据传送是先把两个方向上的运算都事先做好,然后用条件控制最终的赋值,如下图:
在这里插入图片描述
有些表达条件行为的方法能够更直接地被翻译成条件数据传送。
在这里插入图片描述
上述的代码在随机数据上测试,CPE为13.50,对于可预测数据,CPE为2.5~3.5。
在这里插入图片描述
这个代码无论数据是任意,还是可预测,CPE都是4.0。
通过这个例子我们知道,在合适的情况下,可以通过修改代码帮助编译器实现条件数据传送。

理解内存性能

这一小节专注一下加载(从内存到寄存器)和存储(从寄存器到内存)操作的程序性能。

加载的性能

加载操作程序既依赖于流水线的能力,也依赖于加载单元的延迟。上面的CPE从来没到过0.5以下,因为所有的示例都需要从内存读一个值,对于两个加载单元来说,每一个时钟周期只能启动一条加载操作,所以CPE不可能小于0.5。对于每个必须加载k个值的程序,CPE不可能小于k/2。
想要看一看加载操作的延迟产生的影响,需要让一条加载操作的结果决定下一条操作的地址。
在这里插入图片描述
这个例子中,每次循环都依赖于指针 ls->next 读出的值。测试出CPE为4.0,这就是加载操作的延迟所决定的。

存储的性能

在这里插入图片描述
在这个例子中,示例A的CPE为1.3,示例B的CPE为7.3。这是因为,示例A中加载和存储的位置分别是a[0] 和 a[1],没有数据相关;而示例B的加载和存储都是a[0],这就产生了很大的数据相关,在微处理器执行加载和存储的时候,如果两个指令的地址是不同的,两个操作就可以独立进行;如果地址相同,就必须等到存放完成后,再加载。

应用:性能提高技术

1) 高级设计。为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用会渐进地产生糟糕性能的算法或编码技术。
2) 基本编码原则。

  • 消除连续的函数调用:可能的话,将计算移到循环外
  • 消除不必要的内存引用:引入临时变量来保存中间结果,最后将结果存到数组或全局变量中
    3) 低级优化。
  • 展开循环,降低开销
  • 通过使用例如多个积累变量和重新结合等技术,提高指令并行
  • 用功能性风格重写条件操作,使得编译采用条件数据传送

确认和消除性能瓶颈

之前给出的都是小程序的优化,对于大程序的优化没那么简单,连知道应该优化什么地方都很难,这一节介绍了一种Unix系统下代码剖析程序:GPROF,用来分析某程序中每个函数花费的CPU时间,和每个函数被调用的次数,不多介绍了。

  • 6
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《深入理解计算机系统》是一本经典的计算机科学教材,第二版是对第一版的全面升级和扩展。本书的作者是布莱恩·卡尼汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie),他们是C语言的创始人之一,有着丰富的实践经验和深厚的理论功底。 这本书的主要目的是帮助读者深入了解计算机系统的底层原理和工作机制,以及如何利用这些知识进行系统性能优化和问题调试。书中从编程语言、汇编语言、程序执行、内存管理、文件系统等多个角度,详细介绍了计算机系统的各个组成部分和其相互作用的方式。 第二版在内容和理念上都有了一些新的改进。首先,本书增加了关于网络编程和并发编程的新章节,以适应当今计算机系统日益复杂的发展趋势。其次,为了保持与迅速变化的技术形势的同步,第二版对许多章节进行了修订和更新,包括对新的计算机体系结构、处理器技术、存储介质等的介绍,并针对一些近期出现的漏洞和攻击做了相应的解释。 本书的特点是理论与实践相结合,既深入剖析计算机系统的原理,又通过大量实例和案例帮助读者将理论应用到实际问题中。读者在阅读过程中,不仅可以学到计算机系统的原理和工作机制,还能够锻炼自己的系统设计和调试能力。 总的来说,《深入理解计算机系统》第二版是一本非常重要的计算机科学教材,适合对计算机系统工作原理感兴趣的学生、教师和从业人员阅读。通过阅读本书,读者可以深入理解计算机系统的底层原理,提高系统性能,解决实际问题。 ### 回答2: 《深入理解计算机系统》第二版是一本经典的计算机系统原理教材,由美国卡内基梅隆大学的教授布莱恩·卡尼汉(Brian Kernighan)和罗伯特·戴维森(Robert Davidson)合著。本书的中文翻译版本在CSDN上非常受欢迎。 该书主要通过深入讲解计算机底层硬件、操作系统以及编译原理等知识,帮助读者全面理解计算机系统的工作原理和设计思想。书中内容围绕计算机系统的核心概念展开,包括进程、内存管理、文件系统、虚拟内存等。 《深入理解计算机系统》第二版与第一版相比,进行了全面的更新和改进。作者引入了最新的计算机体系结构和技术,在保留经典内容的基础上,增加了对多核处理器、并行计算等新技术的讲解。此外,书中后期的内容还涉及了网络编程、安全和性能优化等实际应用方面的知识。 这本书的优点在于,作者以清晰简洁的语言,结合大量实例和案例,将复杂的计算机系统理论概念讲解得容易理解和易于实际运用。读者通过学习本书,可以更好地理解和分析计算机系统的性能瓶颈,并通过优化和改进提升系统的效率。 此外,《深入理解计算机系统》还鼓励读者通过自主实践,使用常见的工具和技术,动手实践并深入理解计算机系统设计和性能调优的方法。这种实践性的学习方式,使得读者能够通过实际操作加深对书中知识的理解和掌握。 总之,《深入理解计算机系统》第二版通过全面深入的讲解,帮助读者建立起系统化的计算机系统知识框架。对于计算机科学相关专业的学生和从事软件开发、系统管理等工作的人员来说,本书都是一本非常有价值的参考资料,有助于他们理解计算机系统的内在原理和工作机制,进一步提升技术水平。 ### 回答3: 《深入理解计算机系统(第二版)》是由美国卡内基梅隆大学的教授Randal E. Bryant和David R. O'Hallaron合著的一本计算机系统相关的教材。该书是计算机科学与工程领域的经典教材之一,旨在帮助读者深入理解计算机系统的底层原理和工作机制。 这本书主要分为10个章节,从CPU的组成部分开始,逐步向上层的内存和I/O系统扩展。第一章介绍了计算机系统的基本概念和层次结构,为后续章节奠定了基础。接着,第二章到第五章讲解了整数和浮点数的表示与运算,同时介绍了汇编语言和数据表示的相关概念。 在第六章和第七章中,书籍聚焦于理解计算机系统中内存层次结构和缓存一致性。这些章节解释了为什么程序中有些内存操作会比其他操作更快,并介绍了各种优化技术。在第八章中,书籍介绍了虚拟内存的概念与实现方式,深入讲解了操作系统如何使用虚拟内存机制提高程序的执行效率。 接下来的两个章节,第九章和第十章,介绍了动态内存分配和链接。这些章节探讨了程序运行时如何管理内存和使用动态分配的技术,如何生成可执行文件并将其与其他对象文件链接。 《深入理解计算机系统(第二版)》通过系统性的讲解,帮助读者逐步深入理解计算机系统的底层原理。书中的例子和实践问题,能帮助读者巩固所学知识并应用于实际问题。此外,书的附录还提供了一些计算机系统方面的背景知识,供读者参考。 通过阅读这本书,读者可以全面掌握计算机系统的基本原理,并具备实际解决问题的能力。无论是对于计算机科学与工程专业的学生,还是对于从事计算机系统相关领域的从业者来说,《深入理解计算机系统(第二版)》都是一本必不可少的参考书籍。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值