1.04.2009

[C語言] Rounding 四捨五入

前陣子有公司找我去面談,結果被打槍了,心情低落了一陣子,所以一段時間沒文章。最近在實驗室BBS有學弟妹分享如何在Matlab呼叫C寫的code(因為Matlab實在跑太慢了,更不用說Mac版的),不過這不是重點,因為我的畢業論文就用過這招了,那段程式是用inline assembly做四捨五入,所以我想就來分享一點使用C語言實作Rounding(四捨五入)的各種方法與經驗。

一般人想到用C實作四捨五入,會有幾種方法?就我所知,至少有將近10種或是更多(不好意思,學藝不精),以下分享最簡單到稍難的方法,除此以外大概還有C++專屬的用法、CPU限定的用法(跟32/64-bit有關)以及使用magic number的用法,這些我就不多著墨了,有興趣的讀者可以用我前面提供的線索去找找。

1. 利用C語言中浮點數轉整數會把小數點去掉的特性,這是ANSI C裡頭制定的規格,所以大部份的平台都可以使用(沒有浮點數的就別來亂了),程式大概長這樣:

inline int myIntRound(double dInput)
{
    if(dInput >= 0.0f)
    {
        return ((int)(dInput + 0.5f));
    }
    return ((int)(dInput - 0.5f));
}

2. 使用math.h中的round()函式,唯一要特別注意的是接回傳值是用double或int,永遠別忽略type casting所帶來的effort,使用方法很簡單,把值丟進去就行了:
double dResult = round(dInput);

3. GCC的math.h中可以找到nearbyint()這個函式,用法跟round()一樣:
double dResult = nearbyint(dInput);

4. 接下來這個方法其實有點脫褲子放屁,如果你的math.h沒有提供round()可以派上用場。一樣include math.h,我們改用floor()及ceil()來實作,程式碼如下:

inline double myFloorRound(double dInput)
{
    if(dInput >= 0.0f)
    {
        return floor(dInput + 0.5f);
    }
    return ceil(dInput - 0.5f);
}

5. 接下來要介紹的方法就比較不那麼跨平台了,我們要使用frndint這個FPU指令,並且以inline assembly實作,在使用x86 CPU的Win32上的實作上大概長得像這樣:
inline double myDoubleRound(double dInput)
{
    double dResult;
    __asm__ (
        "frndint;": "=t" (dResult) : "0" (dInput)
    );

    return dResult;
}

順道一提的是,某些math.h中的round就是這樣寫的。

6. 一樣用inline assembly,我們用到了fld及fistp這兩個指令,程式碼如下:

inline int double2int(double dInput)
{
    int nResult = 0;
    __asm {
        fld dInput
        fistp nResult
    }
    return nResult;
}
其實在使用fistp之前應該要先用fldcw設定rounding的方式,可以設定為最近的int,或是floor/ceil等。

=========== 我是分隔線 ===========
現在讓我們來測試一下各種方法的效能如何,筆者使用的環境是Mac OS X 10.5.6(Intel)、GCC 4.0.1,compile參數為-O2 -fasm-blocks,每個方法呼叫100000000次,以-3.5做為input,測試結果如下:
Math.round:     0.048509 sec, result = -4
Math.nearbyint: 0.045757 sec, result = -4
myIntRound:     0.045828 sec, result = -4
myDoubleRound:  0.045824 sec, result = -4
myFloorRound:   0.045940 sec, result = -4
double2int:     0.222163 sec, result = -4

前面的5種方法速度都在誤差範圍內,只有double2int的速度特別慢,這件事告訴我們:拔獅子的鬃毛不一定會長出頭髮,就算用inline assembly也不一定會比較快!另外不知道是不是筆者的compiler特別愛作怪,inline assembly加上volatile甚至還會拖慢,以myDoubleRound來說好了,我在__asm__後面加上__volatile__,測試結果竟然要0.457702 sec,足足慢了10倍!

小小做個總結好了,其實C語言有很多不起眼卻可以探討的主題,翻一翻GCC的code也可以挖到不少寶。以四捨五入來說,每種方法都各有優缺點,使用math.h的方法最容易實作,卻也會讓program image變大;使用myIntRound的方法如果回傳後是塞到double就需要cast的effort;採用myDoubleRound的方法需要FPU指令。要使用何種方法就見人見智囉。

6 Comment:

匿名 提到...

版主注重程式語言的基礎,程式功力應該很不錯,不大可能找不到工作吧,除非是薪水問題..^^"

我也是寫軟體的,打算創業,目前已經辭職,版主有創業的打算嗎?

PS:你的blog好像沒有追蹤回覆的選項耶,我留一下mail,希望有機會跟你聊。^_^
mrbbstartup@gmail.com

Michael 提到...

@BB
你說的沒錯,我放棄了幾個工作機會,不過不是因為薪水問題 :)
對老闆來說,對他們有用的才叫功力,所以我的功力好不好就看遇到什麼老闆啦
至於遇不遇得到伯樂我是不在意啦,畢竟伯樂沒看過Porsche...
創業一直都在我的選項內,不過不限於軟體方面開發,事實上我也想嘗試Marketing的工作呢,當老闆的好處跟壞處就是都要自己來啊 XD
另外如果你有用Google帳號登入,留言的右下角應該可以看到訂閱回覆的功能

匿名 提到...

inline int myIntRound(double dInput)
{
if(dInput >= 0.0f)
{
return ((int)(dInput + 0.5f));
}
return ((int)(dInput - 0.5f));
}
怎麼會是這樣?
幹嘛減0.5
錯了啦~

匿名 提到...

原來版主是處理負號喔~
SORRY,我沒看清楚就亂發言,是我不對!
SORRY喔!

Michael 提到...

其實我第一時間寫的時候也是忘了處理負號
我寫東西是漸進式的,這點對面試很不利啊 orz

匿名 提到...

frndint 並不適合用來轉換 (CPU設計上問題)
請用 fld / fistp m64int 方式

張貼留言