浮點數取整討論
這一篇是從巴哈上面的一篇 https://forum.gamer.com.tw/C.php?bsn=60292&snA=7344 所提到的取整來討論,我對於浮點數加法在硬體上怎麼運作以及加法時的有效位數等不是很明瞭,這裡也不考慮 subnormal 等比較特殊的情形,如果有任何地方有講錯或者有東西可以補充的,再麻煩留言指正或補充了,謝謝
float custom_round(float x) {
return (x + 12582912.0f) - 12582912.0f;
}
主要想法
是利用浮點數 (參考 wiki - Floating-point arithmetic) 其實只有有限位數來儲存 significand/mantissa (有效數字,尾數),加上一個數,使之小數部分無法被浮點數儲存,再把該數減掉,即可得到取整後的結果。
討論(十進位)
這裡用十進位來進行討論,比較方便表示。假設我們的浮點數 mantissa 是 4 位 (存儲 3 位) ,即 \( 1.abc \times 10^x \)
- 例如要將 1.234 四捨五入,可以先加 1100 (\( 1.1 \times 10^3 \)),再把他減掉1.234 + 1100 = 1101.234 (實際值) -> \( 1.101 \times 10^3 \) (只能儲存 3 位) = 1101
1101 - 1100 = 1 (結果) - 又或者 0.987
0.987 + 1100 = 1101.987 (實際值) -> \( 1.101 \times 10^3 \) (只能儲存 3 位,至於是 1101 還是 1100 有可能要看硬體了) = 1101
1101 - 1100 = 1 (結果)
設定的數其實不一定要固定,只要讓加法後結果只能表示到整數部分即可
二進位及誤差
那二進位跟十進位的想法是一樣的。
誤差可能會根據硬體將加法結果變回浮點數儲存時,怎麼處理有關了
以下是在 Compiler Explorer 的範例 (介紹可以看之前寫的這篇)
#include <cmath>
#include <iomanip>
#include <iostream>
float custom_round_1(float x) {
return (x + 8388608.0f) - 8388608.0f;
}
float custom_round_2(float x) {
return (x + 12582912.0f) - 12582912.0f;
}
int main() {
float x = 10.5;
std::cout << std::setprecision(9);
std::cout << "float: " << x
<< " custom_round_1: " << custom_round_1(x)
<< " custom_round_2: " << custom_round_2(x)
<< " std::roundf : " << std::roundf(x)
<< std::endl;
float y = x + 12582912.0f;
float z = y - 12582912.0f;
std::cout << "custom_round_2: " << x
<< " -> " << y
<< " -> " << z
<< std::endl;
return 0;
}
例如 10.5 + 12582912 會得到 12582922 而非 12582923
這邊是使用 rounding half down, 當 xxx.5 時會變成 xxx;而 round 是用正常的四捨五入 - rounding half up,當 xxx.5 時會變成 xxx+1 (更多不同取整數的方法詳見 wiki - Rounding)
當使用 10.5000009537 這些方法就給出一樣的結果了
另外一種可能的誤差則是,當加完之後,浮點數能儲存的部分只到整數部分倒數第二位 (\(2^0\) 無法表示),導致在扣掉時就也無法弄回來了。
12582912?
那為何選擇 12582912 這我就不是很清楚了,但如果拿這個數字去查,會出現使用該數字來將浮點數轉換成整數,那他是用 1.5 * (1 << #stored_mantissa_bits),在 float 的話就是 1.5 * (1 << 23),而將 12582912 放入 Floating Point Converter 也可以看到同樣的結果
如果有任何錯誤或缺漏,再麻煩留言指正及補充了