6

Coding4Fun - 浮點數存在的意義

 3 years ago
source link: https://blog.darkthread.net/blog/why-need-float/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Coding4Fun - 浮點數存在的意義

calendar.svg 2021-01-17 09:46 AM comment.svg 2 eye.svg 3,480

前幾天的浮點數討論再次突顯 float、double 計算結果常存在微小誤差的特性,甚至會出現以下狀況:

float a = 1/3,a 值顯示為 0.3333333,但 a 不等於 0.3333333f,a 也不等於 float.Parse(a.ToString())!

因此,在很需要精準度的場合,像是計算利息、依比例分攤費用、分贓... 等等,差一毛錢都可能引來殺機,最好事先明訂規則,並全程使用 decimal 計算。

這個結論引來一些討論:既然如此,為什麼不乾脆把 float、double 給廢了,永遠只用 decimal 就好?

其實也不行,decimal 的精準並非全無代價,它需要的儲存空間大(16 Bytes),計算速度慢。對於不需要高精準度(例如:座標軸、動畫切換時機),但對速度、儲存空間更敏感的應用(例如:遊戲),decimal 很雷,float 才是王道。

這引發一個有趣的問題:大家都知道 decimal 計算比 float、double 慢,但究竟慢多少?寫個程式試試就知道囉。

我寫了一小段程式,用亂數產生 1000 組介於 0 - 100 小數位數一位的 a、b 值,分別採用 float、double、decimal 型別,對 a, b 進行加、減、乘、除四種計算,交由 BenchmarkDotNet 實測速度:

using BenchmarkDotNet.Attributes;
using System;

namespace FloatBenchmark
{
    public class TestRunner
    {
        const int count = 1000;
        float[] fa = new float[count], fb = new float[count];
        double[] da = new double[count], db = new double[count];
        decimal[] ma = new decimal[count], mb = new decimal[count];
        public TestRunner()
        {
            var rnd = new Random(9527);
            for (int i = 0; i < count; i++)
            {
                var a = rnd.Next(1000) + 1;
                var b = rnd.Next(1000) + 1;
                fa[i] = a / 10f; fb[i] = b / 10f;
                da[i] = a / 10d; db[i] = b / 10d;
                ma[i] = a / 10m; mb[i] = b / 10m;
            }
            for (int i = 0; i < 10; i++)
                Console.WriteLine($"{fa[i],4}, {fb[i],4} | {da[i],4}, {db[i],4} | {ma[i],4}, {mb[i],4}");
        }

        [Benchmark]
        public void CalcAddWithFloat()
        {
            for (var i = 0; i < count; i++) { var a = fa[i] + fb[i]; }
        }
        [Benchmark]
        public void CalcSubWithFloat()
        {
            for (var i = 0; i < count; i++) { var a = fa[i] - fb[i]; }
        }
        [Benchmark]
        public void CalcMultWithFloat()
        {
            for (var i = 0; i < count; i++) { var a = fa[i] * fb[i]; }
        }
        [Benchmark]
        public void CalcDivWithFloat()
        {
            for (var i = 0; i < count; i++) { var a = fa[i] / fb[i]; }
        }

        [Benchmark]
        public void CalcAddWithDouble()
        {
            for (var i = 0; i < count; i++) { var a = da[i] + db[i]; }
        }
        [Benchmark]
        public void CalcSubWithDouble()
        {
            for (var i = 0; i < count; i++) { var a = da[i] - db[i]; }
        }
        [Benchmark]
        public void CalcMultWithDouble()
        {
            for (var i = 0; i < count; i++) { var a = da[i] * db[i]; }
        }
        [Benchmark]
        public void CalcDivWithDouble()
        {
            for (var i = 0; i < count; i++) { var a = da[i] / db[i]; }
        }

        [Benchmark]
        public void CalcAddWithDecimal()
        {
            for (var i = 0; i < count; i++) { var a = ma[i] + mb[i]; }
        }
        [Benchmark]
        public void CalcSubWithDecimal()
        {
            for (var i = 0; i < count; i++) { var a = ma[i] - mb[i]; }
        }
        [Benchmark]
        public void CalcMultWithDecimal()
        {
            for (var i = 0; i < count; i++) { var a = ma[i] * mb[i]; }
        }
        [Benchmark]
        public void CalcDivWithDecimal()
        {
            for (var i = 0; i < count; i++) { var a = ma[i] / mb[i]; }
        }
    }
}

偷看亂數產生的測試資料長這樣:

實測結果如下:

// * Summary *

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042
Intel Core i5-7440HQ CPU 2.80GHz (Kaby Lake), 1 CPU, 4 logical and 4 physical cores
  [Host]     : .NET Framework 4.8 (4.8.4300.0), X86 LegacyJIT
  DefaultJob : .NET Framework 4.8 (4.8.4300.0), X86 LegacyJIT


|              Method |         Mean |       Error |      StdDev |
|-------------------- |-------------:|------------:|------------:|
|    CalcAddWithFloat |     617.4 ns |     4.99 ns |     4.67 ns |
|    CalcSubWithFloat |     620.3 ns |     3.51 ns |     3.28 ns |
|   CalcMultWithFloat |     618.6 ns |     3.56 ns |     3.33 ns |
|    CalcDivWithFloat |   1,239.9 ns |     8.23 ns |     7.70 ns |
|   CalcAddWithDouble |     618.1 ns |     6.13 ns |     5.73 ns |
|   CalcSubWithDouble |     620.6 ns |     4.53 ns |     4.24 ns |
|  CalcMultWithDouble |     617.4 ns |     5.63 ns |     5.27 ns |
|   CalcDivWithDouble |   1,249.8 ns |     7.10 ns |     6.65 ns |
|  CalcAddWithDecimal |  15,419.1 ns |   171.69 ns |   160.60 ns |
|  CalcSubWithDecimal |  18,166.3 ns |   148.32 ns |   138.73 ns |
| CalcMultWithDecimal |  14,222.4 ns |    96.13 ns |    85.22 ns |
|  CalcDivWithDecimal | 164,824.1 ns | 1,138.52 ns | 1,064.97 ns |

差異非常明顯,float 與 double 計算速度相同,而靠著浮點運算器硬體加持,二者的計算速度電爆 decimal,加法快 25 倍、減法快 30 倍、乘法快 23 倍、除法則快了 132 倍。double 跟 float 計算速度相同,差別在會 double 多用一倍空間(8 Bytes vs 4 Bytes),如果數值用到的總位數小於 9 位(整數與小數部分合計),float 是最快最省空間的選擇。

題外話。這是我熱愛 Coding 的一個重要原因 - 任何時侯心中有疑問,不需要進實驗室,不用買幾千萬的設備,只要手邊有電腦,隨時可以自己動手實驗釐清。在寫程式這個領域,郭董跟我站在同一條起跑線,等等,不對! 他可以花錢請一百位資工博士幫他研究,我只能自己土砲... XD

and has 2 comments

Comments

Post a comment

Comment
Name Captcha 33 - 26 =

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK