轨道
/
x86-64 Assembly
x86-64 Assembly
/
练习
/
自由职业者费率
自由职业者费率

自由职业者费率

学习练习

简介

浮点数

浮点数是一种可以带小数部分的数字,例如 -1.23 或 3.1415。

为了把这样的数字表示成一串字节,x86-64 使用IEEE-754标准。 该标准中最常见的两种格式是单精度浮点数和双精度浮点数。 x86-64 两种都支持。

单精度浮点数宽 32 位,而双精度浮点数宽 64 位。

XMM 寄存器

在 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

说明

在这个练习中,你将编写代码,帮助一位自由职业者与项目经理沟通。 你的任务是提供几个实用工具,用来快速计算日费率和月费率,并且可以选择应用给定的折扣。

我们首先在自由职业者和项目经理之间约定几条规则:

  • 日费率是小时费率的 8 倍。
  • 一个月有 22 个可计费日。

有时,自由职业者会主动在自己的日费率上提供折扣(例如针对最忠诚的客户或非营利客户)。

折扣用表示百分比的小数来建模,例如 25.0(25%)。

Note

这些是浮点指令的后缀:

后缀 操作数大小
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 截断

1. 根据小时费率计算日费率

实现一个名为daily_rate的函数,根据作为参数传入的小时费率来计算日费率,该费率为 64 位浮点数。 合约规定一天有 8 个可计费小时。

daily_rate(60)
// => 480.0

返回的日费率应该是一个 64 位浮点数。

2. 计算折扣后的价格

实现一个apply_discount函数,计算折扣后的价格。 它应该接受两个参数:原价和以百分比表示的折扣率,两者都是 64 位浮点数。

apply_discount(150.0, 10.0)
// => 135.0

返回值应该始终是 64 位浮点数。

3. 根据小时费率和折扣计算月费率

实现一个monthly_rate函数,计算折扣后的月费率。 它接受两个参数,小时费率和以百分比表示的折扣,两者都是 64 位浮点数。

monthly_rate(77.0, 10.5)
// => 12130

返回的月费率应该向上取整(即取上限),成为最接近的 64 位整数。

4. 根据预算、小时费率和折扣计算完整工作日的数量

实现一个days_in_budget函数,接受以下参数:

  1. 预算,一个 64 位无符号整数。
  2. 小时费率,一个 64 位浮点数。
  3. 以百分比表示的折扣,一个 64 位浮点数。

这个函数应该返回预算所能覆盖的完整工作天数。

days_in_budget(20000, 80, 11.0)
// => 35

返回的天数应该向下取整(即取下限),成为最接近的 32 位整数。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
x86-64 Assembly Exercism

准备好开始 自由职业者费率 了吗?

注册 Exercism,借助 22 个概念130 个练习 和真人导师指导,学习并掌握 x86-64 Assembly,全部免费。