浮点数是一种可以带小数部分的数字,例如 -1.23 或 3.1415。
为了把这样的数字表示成一串字节,x86-64 使用IEEE-754标准。 该标准中最常见的两种格式是单精度浮点数和双精度浮点数。 x86-64 两种都支持。
单精度浮点数宽 32 位,而双精度浮点数宽 64 位。
在 x86-64 中,有 16 个专用于浮点运算的寄存器,名为xmm0到xmm15。
这些寄存器至少有 128 位宽,但较新的处理器把这一宽度提高到 256 位或 512 位。 不过在实践中,大多数浮点数只用到最低的 32 位或 64 位,具体取决于所需的精度。
有一系列指令用于在不同数字格式之间转换:cvtxx2yy。
xx和yy都不是字面量,而是占位符:
xx是源操作数类型的占位符。yy是目标操作数类型的占位符。这个占位符有多种形式,其中包括:
| 占位符 | 含义 |
|---|---|
si |
有符号整数 |
ss |
32 位浮点数 |
sd |
64 位浮点数 |
例如:
cvtsi2ss xmm0, eax ; converts a 32-bit signed integer to a single-precision floating-point
cvtsd2si rax, xmm1 ; converts a double-precision floating-point to a 64-bit signed integer
注意,可以把 32 位整数转换成 64 位浮点数,也可以把 64 位整数转换成 32 位浮点数。
同样,也可以把 32 位浮点数转换成 64 位整数,或把 64 位浮点数转换成 32 位整数。
换句话说,cvtxx2yy的两个操作数的大小_不必相同_:
cvtsi2sd xmm0, eax ; converts a 32-bit signed integer to a double-precision floating-point
cvtsi2ss xmm0, rax ; converts a 64-bit signed integer to a single-precision floating-point
cvtss2si rax, xmm0 ; converts a single-precision floating-point to a 64-bit signed integer
cvtsd2si eax, xmm0 ; converts a double-precision floating-point to a 32-bit signed integer
当浮点数无法被精确表示成整数时,cvtxx2si会根据一个名为mxcsr的特殊寄存器中的舍入控制对该数字进行舍入。
这可能是就近舍入、向下舍入、向上舍入或截断。
函数入口处没有保证的默认值。
注意,精确的结果不会被舍入,因此也不受舍入控制的影响。
例如,42.0_总是_会被转换成42。
还有一系列格式为cvttxx2si的类似指令(多了一个t),它们总是截断结果。
roundxx系列指令把源操作数中的浮点数转换成舍入后的浮点数,存放到目标操作数中。
这里,xx照例是操作数类型的占位符:ss表示单精度浮点数,sd表示双精度浮点数。
这些指令的第三个操作数是一个常数。 这个数字表示舍入模式:
| 数字 | 模式 |
|---|---|
| 0 | 就近 |
| 1 | 向下取整 |
| 2 | 向上取整 |
| 3 | 截断 |
例如:
roundss xmm0, xmm1, 1 ; this rounds down (takes the floor) a 32-bit floating-point in xmm1 and stores it in xmm0
roundsd xmm2, xmm2, 2 ; this rounds up (takes the ceil) a 64-bit floating-point in xmm2 and stores it in xmm2
由于这些指令产生的是精确结果,它们可以用来为cvtxx2si做准备。
许多浮点数运算所用的指令,其名字与对应的整数指令相似,只是在末尾加上必要的占位符(ss或sd)。
这些指令都是双操作数的,语义与通常一样,即使是执行乘法或除法的指令也是如此:
addss xmm1, xmm2 ; xmm1 = xmm1 + xmm2 (all 32-bit floating-point)
mulsd xmm4, xmm5 ; xmm4 = xmm4 * xmm5 (all 64-bit floating-point)
divss xmm0, xmm3 ; xmm0 = xmm0 / xmm3 (all 32-bit floating-point)
把一个 XMM 寄存器中的值移动到另一个寄存器,用的是movss或movsd指令:
movss xmm1, xmm0 ; this copies a 32-bit floating-point from xmm0 to xmm1
movsd xmm6, xmm4 ; this copies a 64-bit floating-point from xmm4 to xmm6
不可以把立即数(一个常量有符号整数)直接移动到 xmm 寄存器。
这个数字需要先移动到通用寄存器,再用cvtsi2xx转换:
mov rax, 10 ; rax is a 64-bit integer with value 10
cvtsi2sd xmm0, rax ; xmm0 is a 64-bit floating-point with value 10.0
有两组指令用于比较两个浮点数。
它们的格式是comixx和ucomixx,其中xx是ss或sd。
两者都像通常那样设置rflags中的标志位,语义与cmp相同。
两者的区别在于:如果两个浮点数中有一个的值是NaN,ucomixx会设置parity flag (PF),而comixx在这种情况下会抛出异常。
浮点数之间的比较是_无符号_的:
ucomisd xmm0, xmm1
ja .greater ; 'ja' must be used for "greater", not 'jg'
jb .lesser ; 'jb' must be used for "lesser", not 'jl'
在 System V ABI 中,前八个浮点实参按顺序通过xmm0到xmm7传给函数。
浮点值通过xmm0返回,必要时也通过xmm1。
所有xmm寄存器都可自由使用,没有一个是保留给调用方的。
浮点值可以按照与同尺寸整数相同的语义在内存中定义。 NASM(The Netwide Assembler,本 track 使用的汇编器)也允许用科学计数法声明浮点值。
section .data
pi dq 3.141592653589793 ; this is a double-precision floating-point value
exp_num dd 1.e5 ; this is a single precision floating-point value equal to 1 * 10⁵
exp_num2 dd 1.e+5 ; this is a single precision floating-point value equal to 1 * 10⁵
frac dq 1.e-10 ; this is a double precision floating-point value equal to 1 * 10⁻¹⁰, i.e., 0.0000000001
在这个练习中,你将编写代码,帮助一位自由职业者与项目经理沟通。 你的任务是提供几个实用工具,用来快速计算日费率和月费率,并且可以选择应用给定的折扣。
我们首先在自由职业者和项目经理之间约定几条规则:
有时,自由职业者会主动在自己的日费率上提供折扣(例如针对最忠诚的客户或非营利客户)。
折扣用表示百分比的小数来建模,例如 25.0(25%)。
这些是浮点指令的后缀:
| 后缀 | 操作数大小 |
|---|---|
ss |
32 位 |
sd |
64 位 |
以下是本概念中提到的一些指令:
| 指令 | 说明 |
|---|---|
| movss/movsd a, b | 把 b 复制到 a |
| addss/addsd a, b | a = a + b |
| subss/subsd a, b | a = a - b |
| mulss/mulsd a, b | a = a * b |
| divss/divsd a, b | a = a / b |
| cvtsi2ss/cvtsi2sd a, b | 把整数 b 转换为浮点数 a |
| roundss/roundsd a, b, c | 根据 c 对浮点数 b 进行舍入,并把结果存入 a |
| cvtss2si/cvtsd2si a, b | 把浮点数 b 转换为整数 a |
| comiss/comisd a, b | 在 a 和 b 之间进行比较,如果是NaN则抛出异常 |
| ucomiss/ucomisd a, b | 在 a 和 b 之间进行比较,如果是NaN则设置PF
|
以下是 roundss 和 roundsd 中的舍入控制数字:
| 编号 | 模式 |
|---|---|
| 0 | 最近 |
| 1 | 向下取整 |
| 2 | 向上取整 |
| 3 | 截断 |
实现一个名为daily_rate的函数,根据作为参数传入的小时费率来计算日费率,该费率为 64 位浮点数。
合约规定一天有 8 个可计费小时。
daily_rate(60)
// => 480.0
返回的日费率应该是一个 64 位浮点数。
实现一个apply_discount函数,计算折扣后的价格。
它应该接受两个参数:原价和以百分比表示的折扣率,两者都是 64 位浮点数。
apply_discount(150.0, 10.0)
// => 135.0
返回值应该始终是 64 位浮点数。
实现一个monthly_rate函数,计算折扣后的月费率。
它接受两个参数,小时费率和以百分比表示的折扣,两者都是 64 位浮点数。
monthly_rate(77.0, 10.5)
// => 12130
返回的月费率应该向上取整(即取上限),成为最接近的 64 位整数。
实现一个days_in_budget函数,接受以下参数:
这个函数应该返回预算所能覆盖的完整工作天数。
days_in_budget(20000, 80, 11.0)
// => 35
返回的天数应该向下取整(即取下限),成为最接近的 32 位整数。