TW Community

浮點數取整討論

這一篇是從巴哈上面的一篇 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;
}

Compiler Explorer 範例

例如 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 也可以看到同樣的結果

如果有任何錯誤或缺漏,再麻煩留言指正及補充了

考慮加法x + a, a = 12582912 = 2^23 + 2^22

IEEE 32位元浮點數有23位有效位元,所以
a = 1.(1000…0) * 2^23
x = [±] 1.(x1,x2,…,x23) * 2^e (為了方便討論,假設x>0)
當e<23,x的整數部份有 e 位元 小數部份有 23-e 位元

在做加法時,先對齊小數點
x + a = [ 1.(x1,x2,…,x23) * 2^(e-23) + 1.1 ] * 2^23
因為只取23有效位數,這邊 x 的有效位數減少了 23-e 位元
也就是說小數部份被捨去了,只留下整數部份(實際上會取最接近的整數,並不是真的捨去)

2^23作用是對齊小數點,任何 2^23+c (c=2^i) 的數都能有同樣的效果
2^22在這邊的作用是避免干擾到加法的進位
如果a = 2^23 + 2^0,2^0有可能跟x相加之後進位,進而影響round-off的精確度

巴哈上面說 10^6 以內的數能有效到±0.1
簡單的估計一下 10^6 ~ 1024^2 = 2^20
表示誤差最多到小數點後三位(二進位)2^(-3) = 1/8 = 0.125

1個讚