《自己動手打造“超高精度浮點數類”》源代碼簡要導讀

                 《自己動手打造“超高精度浮點數類”》源代碼簡要導讀
                           
[email protected]

tag: PI,超高精度浮點數,TLargeFloat,FFT乘法,二分乘法,牛頓迭代法,borwein四次迭代,AGM二次迭代,源代碼導讀

摘要: 很多人可能都想自己寫一個能夠執行任意精度計算的浮點數;:D我寫的第一個程序就是用qbasic計算自然數e到100萬位(後來計算PI);  我的blog文章《自己動手打造“超高精度浮點數類”》裏有一個C++類的實現TLargeFloat,它能夠執行高精度的浮點數運算;演示代碼裏面有一個計算PI的Borwein四次迭代式和一個AGM二次迭代式(我用它計算出了上億位的PI小數位:)  本文章是對其源代碼的進一步解讀;

  本系列文章的由來: 源於一次在abp論壇(現在的www.cpper.com)的帖子,那是2004年的時候,有初學者詢問高精度計算的原理;我那時比較激動(哈哈),因爲這不就是我剛學編程的時候做的嗎?! 就計劃"重操舊業"用C++給初學者寫一個高精度計算的Demo程序(決定實際動手寫要歸功於abp的codinggirl);第一個版本實現前後花了10小時;但實際上完成的代碼沒有真正發佈,也缺少源代碼的分析和介紹;後來放在了csdn的blog上;由於實在太慢,擺在我的blog裏“礙眼”:D , 最近對它做了一些速度改進;本文章是它的簡要導讀;
  超高精度浮點數類TLargeFloat的完整源代碼參見:
http://blog.csdn.net/housisong/archive/2005/11/08/525215.aspx ;在導讀文章中列出的代碼(僅作示例)很可能和實際的代碼有區別;導讀文章會更關注於算法和原理,而更多的細節需要讀者去閱讀源代碼;

ps:題外話(因爲是blog文章,說點題外話纔是正常狀態),回想自己上大學的時候,不務正業的整天跑(泡)圖書館,對編寫計算機程序產生了濃厚的興趣;我的第一個程序是想計算出自然數e(2.718281828...)的上百萬位;由於沒有電腦,就把所有代碼都寫在紙上(用的qbasic語言),覺得差不多了的時候就到學校的計算機房去把代碼敲到電腦裏;那時候對電腦上編寫程序一點概念都沒有(高中學習接觸的根本不能算寫程序);折騰了一陣子後旁邊的一個玩遊戲中(好像)的學長實在看不下去了,轉過頭來告訴我怎麼打開qbasic環境!  我笨拙的把紙上寫好的代碼敲入計算機,在修改了幾處打字錯誤後運行成功,運行第一次就正確的輸出了e的上千位小數,哈哈,當時很有成就感; 在紙上寫程序和修改,並在腦袋中"運行"多遍後再讓電腦來實際運行的那段記憶只能是美好的回憶了 (記得有一個程序我甚至寫了幾十頁的) :D

a.高精度浮點數的表示方法(數據結構)

超高精度浮點數類TLargeFloat的數據結構定義:
class TLargeFloat 
{
    long               m_Sign;     //符號位
    long               m_Exponent; //保存10爲底的指數
    std::vector<long>  m_Digits;   //小數部分
};
  其中:  m_Sign用來保存該浮點數的正負號(正1和負1);我用0來代表浮點數0,這樣對於某些判斷比較方便;
    m_Exponent保存的是該浮點數的指數部分,其值代表的是10進製爲底的指數 (指數部分可以使用int64整數)
    m_Digits數組保存的是該浮點數的小數尾數部分,每個元素保存4位10進制數,也就是取值[0--9999]; 對於m_Digits[0],正常情況下的值取值範圍爲[1000,9999],否則該浮點數就是未規格化的(未規格化小數只存在於運算過程中的臨時值);
    比如當m_Sign=-1,m_Exponent=-100,m_Digits長度爲3,m_Digits[0]==1234,m_Digits[1]=5678,m_Digits[2]=258; 則這個TLargeFloat代表的是浮點數:-0.123456780258E-100

  源代碼中,爲了捕獲指數運算超出值域的異常情況我定義了一個TCatchIntError類用來包裝m_Exponent所使用的整數類型;TCatchIntError實現了“+=” 、“-=” 、“*=”3個運算符,並處理可能的值域超界,如果發生值域超界將拋出TLargeFloatException類型的異常。

  小數部分數據結構的選擇,將決定很多算法的具體實現細節;爲了容易編碼和閱讀我捨棄了一些更節約內存的設計也捨棄了一些複雜的優化(比如彙編等);選擇4位10進制數將簡化很多代碼的實現(後面會看到); 對於更專業的實現,建議使用8(或9)位10進制數或者以2進製爲基礎來實現;

b.其他數到超高精度浮點數類TLargeFloat的轉換
  TLargeFloat實現了long double到TLargeFloat的多個轉換和相互運算,這樣那些能夠自動隱式轉換到long double的類型(比如int、float等)也能自動的參與TLargeFloat的運算;程序也提供了TLargeFloat與字符串類型之間的轉換方法:AsString()和StrToLargeFloat(); 這對於超大和超高精度的數傳遞給TLargeFloat就比較有用,而且通過字符串的方式轉換到TLargeFloat的數也不會產生任何誤差!

  數值轉換成TLargeFloat的構造函數,一般的聲明是 TLargeFloat(數值,高精度浮點數的有效精度);但這樣寫容易混浠,比如 TLargeFloat(200,300); 所以我定義了一個類TDigits,要求這樣寫代碼:TLargeFloat(200,TLargeFloat::TDigits(300)); 

c.高精度浮點數的加減法的實現
  加減法可以通過絕對值加和絕對值減來實現(Abs_Add(),Abs_Sub_Abs()函數),先考慮參與運算的TLargeFloat浮點數的正負號然後調用絕對值加(符號相同)或絕對值減(符號不同)就可以了;
  要實現兩個高精度數相加減,需要首先調整到指數相同(就如手工計算時的小數點對齊);
  比如: (-0.1234E2) + (-0.5678E-1) == -(0.1234E2 + 0.5678E-1)== -( 0.1234 +0.0005678 )*1E2 == -0.1239678E2
   (-0.1234E2) + (0.5678E-1) == -(0.1234E2 - 0.5678E-1)== -( 0.1234 -0.0005678 )*1E2 == -0.1228322E2

  核心實現函數:
  小數部分的多位數對齊加法: void TLargeFloat_ArrayAdd(TInt32bit* result,long rsize,const TInt32bit* y,long ysize);
      它實現將y加到result上;
  小數部分的多位數對齊減法: void TLargeFloat_ArraySub(TInt32bit* result,long rsize,const TInt32bit* y,long ysize);
      它實現從result中減去y;
 

d.高精度浮點數的乘法的簡單實現
  回想一下我們是怎麼手工計算多位數乘法的,那高精度浮點數的乘法也可以這樣實現;

  核心實現函數:
  小數部分的多位數乘法: TLargeFloat_ArrayMUL_ExE(TInt32bit* result,long rminsize,const TInt32bit* x,long xsize,const TInt32bit* y,long ysize)
  簡要過程如下:
    for (long i=0;i<xsize;++i)
    {
        for (long j=0;j<ysize;++j)
        {
            result[j+i+1]+=(x[i]*y[j]);
            ...//進位
        }
    }
  實際的代碼做了一些優化;爲了減少進位的次數,當result快超出值域的時候,纔會執行進位;
  該函數的時間複雜度爲O(N*N);在進行高精度運算過程中(N很大的時候),絕大部分時間都在做乘法運算;後面會對該實現進行優化;

e.高精度浮點數的除法和開方的實現 牛頓迭代法
  高精度浮點數的除法和開方可以用牛頓迭代法來實現,也就是利用乘法來實現;原理和推導(源代碼中也有)可以參見我的blog文章《代碼優化-之-優化除法》:
http://blog.csdn.net/housisong/archive/2006/08/25/1116423.aspx  (包括牛頓迭代法的原理、圖示 、 除法和開方的牛頓迭代公式的推導和公式的收斂速度證明)
  由於有效精度隨迭代次數成倍增加,所以可以控制當前參與運算的高精度數的精度,從而優化速度;完成該自動精度調整功能的類是TDigitsLengthAutoCtrl;(開始用較低的運算精度,隨着迭代次數的增加,增加運算的精度)這樣實現後,除法和開方的時間複雜度和乘法一樣(想當於幾次高精度乘法);

f.用二分法來優化乘法實現
  兩個高精度的數相乘x*y ; 將x,y從數組中間分成兩部分表示: x=a*E+b; y=c*E+d; 
  x*y=(a*E+b)*(c*E+d)=(E*E-E)*ac + E*(a+b)*(c+d) -(E-1)*bd;
  可見,長度爲N的數相乘,可以分解爲3次長度爲N/2的數相乘(遞推); 時間上有遞推式 f(N)=3f(N/2); 求解該方程可得該算法時間複雜度爲(N^log2(3)); 比前面的O(N^2)複雜度低了很多;
 
  二分法乘法核心實現函數:void ArrayMUL_Dichotomy(TInt32bit* result,long rminsize,const TInt32bit* x,const TInt32bit* y,long MulSize);

g.用FFT來優化乘法實現
  高精度數的乘法其實可以看作一種卷積運算,可以轉化爲傅立葉變換運算 (進階實現:轉換成數論變換!) ,而它可以有O(N*log(N))複雜度的算法--快速傅立葉變換(FFT);
  FFT核心函數: void FFT(TComplex* a,const TComplex* w,const long n);
  FFT實現的乘法核心函數:void MulFFT(const TInt32bit* ACoef,const long ASize,const TInt32bit* BCoef,const long BSize,TInt32bit* CCoef,const long CMinSize);
  這裏的實現使用的是double實現的複數算法;當乘數的長度超過一定範圍的時候,計算中產生的誤差會放大到超過0.5從而造成取整錯誤;
所以MulFFT支持的運算長度是有限制的;採用4個10進制數爲基時長度不能超過約16百萬位多10進制位;當然可以使用更短的基從而提高允許的最大的位數;但實際上這樣的內存消耗太恐怖了,所以不適合採用,而是在超過設定長度的時候轉調用ArrayMUL_Dichotomy的實現;

  這裏我們有了3個乘法的實現,它們在不同的情況下各有各的優勢,當乘法位數較短的時候,TLargeFloat_ArrayMUL_ExE更快;再大一些時使用ArrayMUL_Dichotomy更快,再大一些的話使用MulFFT,當超過MulFFT允許的最大長度時調用ArrayMUL_Dichotomy來拆開乘法運算,使乘法長度適合MulFFT;
  這個綜合函數就是:void ArrayMUL(TInt32bit* result,long rminsize,const TInt32bit* x,long xsize,const TInt32bit* y,long ysize);  它是整個高精度運算庫的核心;

h.高精度PI值計算採用的公式
   我用超高精度浮點數類TLargeFloat實現的AGM算法計算出了PI的1億位小數;
   AGM二次收斂迭代公式:
      初值:a=x=1; b=1/sqrt(2); c=1.0/4;
      重複計算:y=a; a=(a+b)/2; b=sqrt(b*y); c=c-x*sqr(a-y); x=2*x;
      最後:pi=sqr(a+b)/(4*c);

   實現它的函數是TLargeFloat GetAGMPI(unsigned long uiDigitsLength);  (參數是需要計算的有效10進制位數): 
   我也實現了Borwein四次收斂迭代式:TLargeFloat GetBorweinPI(unsigned long uiDigitsLength);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章