第2章
point1
练习题2.49:对于一种具有n位小数的浮点格式,给出不能准确描述的最小正整数的公式(因为想要准确表示它可能需要n+1位小数)。假设阶码字段长度k足够大,可以表示的阶码范围不会限制这个问题。
思路:整数的二进制表示的低位,和浮点表示的小数部分的高位是匹配的,可以通过移位来得到浮点表示的小数部分。
因为要求的是正整数的表示,因此可以排除非规格化的数值范围,因为这些值全部都小于1。在考虑规格化的数值范围里,倘若需要n+1位小数表示,并且是最小的小数的话,则应该是由n个0和最低位的1个1组成。也就是此时的尾数M = 1 + f = 1 + 2-n-1,由公式V=2E*M,使用阶码抵消掉小数位,则取阶码为2n+1,因此得到值为2n+1+1,即为n位浮点格式不能准确描述的最小正整数。
结合例子理解一下
实际上尾数决定了浮点数的精度,尾数只有23位,加上省略的小数点前的那位就是24位。如果一个int类型的值小于224,那么float是完全可以表示的。如果int类型大于224就不一定能表示了。如一个int数值的二进制表示形式是1000 0000 0000 0000 0000 0000,表示成指数形式是1.0000 0000 0000 0000 0000 000 * 223,对应的float的类型,尾数位全部为0,指数位是23+127=150,这样完全没有问题。假如一个int数值的二进制表示形式是10000 0000 0000 0000 0000 0001,表示成指数形式是1.0000 0000 0000 0000 0000 0001*224,对应的float的类型尾数位是0000 0000 0000 0000 0000 0001一共24位,这样就完全超出了float最多容纳23位尾数的能力。所以就不能正确表达这个int值了。由此也可以得出不能被float准确表达的最小int值是224+1。我们再将1 0000 0000 0000 0000 0000 0001的值加1,变成了1 0000 0000 0000 0000 0000 0010,这样变换为指数形式可以看出尾数又变为了23位,也就是说25位的二进制整数最后一位是0才能被float准确表示,每2个数就有一个不能被准确表示。如果是26位的二进制整数最后两位都是0才可以被float准确表达,每4个数就有3个不能被准确表示,以此类推。
第3章
point1
练习题3.3里有题是这样的
movl %eax,%rdx
这个代码是错 答案上写是因为 “destination operand incorrect size” 目的操作数大小错误
刚开始理解的是这个代码中 movl 的作用不就是把一个32位寄存器%eax的数据移动到64位寄存器%rdx上吗,然后高4位全变成0 这对一个64位的寄存器有什么影响?
后来对照了书上其他的习题和代码,发现汇编中,数据传送指令的最后一个字符必须与寄存器的大小匹配。
书上123页有源和目的的类型的五种可能的组合,当数据从源寄存器传送到目的寄存器时,两个寄存器的大小应该一致。
point2
(转自:https://blog.csdn.net/ciqingloveless/article/details/84325685 )
练习题3.4
假设变量sp和dp被声明为类型
src_t *sp;
dest_t *dp;
这里src_t和dest_t是用typedef声明的数据类型。我们想使用适当的数据传送指令来实现下面的操作
*dp = (dest_t) *sp;
假设sp和dp的值分别存储在寄存器%rdi和%rsi中。对于表中的每个表项,给出实现指令数据传送的两条指令。其中第一条指令应该从内存中读数,做适当的转换,并设置寄存器%rax的适当部分。然后,第二条指令要把%rax的适当部分写到内存。在这两种情况中,寄存器的部分可以使%rax、%eax、%ax或%al,两者可以互不相同。
记住,当执行强制类型转换既涉及大小变化又涉及C语言中符号变化时,操作应先改变大小。
src_t
dest_t
指令
long
long
movq (%rdi),%rax
movq %rax,(%rsi)
char
int
char
unsigned
unsigned
long
int
char
unsigned
unsigned char
char
short
解答
这个题目本身不难,但是有一点很绕,什么时候用符号扩展传送指令,什么时候用零扩展传送指令
我们一行一行解答
char -> int
首先char是有符号类型,int也是有符号类型,所以可以肯定需要符号扩展传送指令,由于char是1字节,而int是是双字,所以结果应该为
movsbl (%rdi),%eax
movl %eax,(%rsi)
char -> unsigned
由于char是有符号类型,unsigned是无符号类型,那么一个有符号一个无符号,我怎么选择扩展指令呢,这是题目的那句,记住,当执行强制类型转换既涉及大小变化又涉及C语言中符号变化时,操作应先改变大小。,所以我们应该先改变大小,改变有符号数的大小还是应该用movs指令,所以结果与上面相同,
movsbl (%rdi),%eax
movl %eax,(%rsi)
unsigned char -> long
现在又来了,第一个是无符号数,第二个是有符号数,第一个是一字节,第二个是4字,根据上面的原则应该先改变大小,
movzbq (%rdi),%rax
movq %rax,(%rsi)
等等为什么答案不对,为什么答案是
movzbl(%rdi),%eax
movq %rax,(%rsi)
这个问题网上基本上是没有答案了,我咨询了一下做CPU的同学,movzbl movzbq的效率不同,所以为了优化cpu效率,选择了下面的答案
int -> char
这个基本上就没有什么要讲的了,int有符号,char有符号,也不需要扩展,
movl (%rdi),%eax
movb %al,(%rsi)
unsigned -> unsigned char
这个答案也很简单,
movl (%rdi),%eax
movb %al,(%rsi)
char -> short
这个也不难了,char是有符号一个字节,short是有符号的一个字,所以答案是
movsbw (%rdi),%ax
movw %ax,(%rsi)
根据这个例子,我推断,所有强制转换操作,多会优化为效率最优的mov指令,就是说能转成32的肯定不会转成64,这样是为了提高CPU效率。
point3
条件控制 和 条件传送
条件控制:使用控制的条件转移,当条件满足时,程序沿着一条执行路径执行,而当条件不满足时,就走另一条路径。(效率低)
条件传送:使用数据的条件转移,这种方法计算一个条件操作的两种结果,然后再根据条件是否满足从中选一个。(只有在一些受限制的情况中,这种策略才可行,效率高)
现代处理器通过使用流水线(pipelining)来获得高性能,也就是重叠连续指令的步骤,例如,在取一条指令的同时,执行它前面的一条指令的算数运算。要做到这一点要求能够事先确定要执行的指令序列,这样才能保证流水线中充满了待执行的指令。
当机器遇到条件分支时,如果使用条件控制,只有当分支条件求值完成后,才能决定分支往哪边走。处理器会采用非常精密的分支预测逻辑来猜测每条跳转指令是否会执行,如果预测错误,会招致很严重的惩罚。对于包含条件跳转的x86-64代码,当分支行为模式很容易预测时,每次调用函数需要大约8个时钟周期;而分支行为模式是随机的时候,每次调用需要大约17.5个周期。分支预测错误的处罚是大约19个时钟周期。
如果使用条件传送,无论测试的数据是什么,编译出来的代码所需要的时间都是大约8个时钟周期,控制流不依赖于数据,这使得处理器更容易保持流水线是满的。
因此,基于条件数据传送的代码会比基于条件控制转移的代码性能要好。
不是所有的条件表达式都可以用条件传送来编译。因为无论测试结果如何,我们给出的抽象代码会对then-expr和else-expr都求值,如果这两个表达式中的任意一个可能产生错误条件或者副作用,就会导致非法的行为。
使用条件传送也不总是会提高代码的效率。例如,如果then-expr和else-expr的求值需要大量的计算,那么相对应的条件不满足时,这些工作就白费了。编译器必须考虑浪费的计算和由于分支预测错误所造成的性能处罚之间的相对性能。实验表明,只有当两个表达式都很容易计算时,它才会使用条件传送,而即使许多分支预测错误的开销会超过更复杂的计算,GCC还是会使用条件控制转移。
point4
switch语句
C代码中开关变量可以是任意整数,但是编译器会做一些变换,将取值范围移到0-m(视具体情况而定)之间,创建一个新的程序变量,在C版本中称为index,补码表示的复数会映射成无符号表示的大正数,利用这一事实,将index看作无符号值,从而进一步简化了分支的可能性。可以通过测试index是否大于m来判定index是否在0-m的范围之外。
如书上例,switch_eg中开关变量为100-106,故汇编代码中出现了以下三句:
subq $100, %rsi
cmpq $6, %rsi
ja .L8
用开关变量减去100,变为0-6,再进行判断。
在switch2中开关变量为-1~7,汇编代码如下:
addq $1, %rdi
cmpq $8, %rdi
ja .L2
用开关变量加上了1,变为0-8,再进行判断。
在switcher中开关变量为0-7,汇编代码如下:
cmpq $7, %rdi
ja .L2
没有进行变换,直接判断即可。
point5
lea指令
加载有效地址指令lea(load effective address),其指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。它的第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。
另外,它还可以简洁的描述普通的算术操作,例如,如果寄存器%rdx的值为x,那么指令 leaq 7(%rdx,%rdx,4),%rax 将设置寄存器%rax的值为5x+7。将x看作一个地址,源操作数 7(%rdx,%rdx,4)的地址为5x+7,但是leaq并不引用5x+7地址里存的值,而是直接将5x+7这个有效地址赋值给%rax,可以看作实现了算术操作。
如果是指令 mov 7(%rdx,%rdx,4),%rax,则是将内存地址为5x+7的内存区域的值赋给%rax寄存器。
point6
数据对齐
对于包含结构的代码,编译器可能需要在字段的分配中插入间隙,以保证每个结构元素都满足它的对齐要求。为了最小化浪费的空间(插入的间隙),当所有的数据元素的长度都是2 的幂时,一种行之有效的策略是按照大小的降序排列结构的元素。