浮點數是一種可以帶有小數部分的數字,例如-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 位元整數。