2011-03-17 51 views
1

不久前,我創建了一堆用於定點值操作的C宏。受到SO上幾個問題和答案的鼓舞,我希望在計算密集型計劃中獲得性能提升。雖然代碼看起來能產生正確的結果,但我想知道它是不是太天真/過於簡化了,因爲它實際上比我的例程的常規浮點版本(我在Wintel上進行雙三次圖像插值)運行速度慢。您能否看看這段包含我的宏的代碼並提出一些改進建議,特別是在性能方面?謝謝。我的定點算術實現是否正確?

// these are architecture-dependent 
typedef short int fixed16; 
typedef int fixed32; 
typedef __int64 fixed64; 

// value of 2^n 
#define POW2(n) (1 << n) 

// create 16bit integer-based fixed point value from a floating point value, n is the number of bits reserved for the fractional part 
#define FP_MAKE16(x, n) ((x) > 0.0 ? static_cast<fixed16>(floor((x) * POW2(n) + 0.5)) : static_cast<fixed16>(ceil((x) * POW2(n) - 0.5))) 
// the same, 32bit 
#define FP_MAKE32(x, n) ((x) > 0.0 ? static_cast<fixed32>(floor((x) * POW2(n) + 0.5)) : static_cast<fixed32>(ceil((x) * POW2(n) - 0.5))) 
// and 64bit 
#define FP_MAKE64(x, n) ((x) > 0.0 ? static_cast<fixed64>(floor((x) * POW2(n) + 0.5)) : static_cast<fixed64>(ceil((x) * POW2(n) - 0.5))) 

// convert a fixed-point integer from one (n) format to another (m) assuming n < m 
#define FP_CONVERT_UP(x, n, m) ((x) << (m-n)) 
// same for n > m 
#define FP_CONVERT_DOWN(x, n, m) ((x) >> (n-m)) 

// convert floating-point value back to float 
#define FP_FLOAT(x, n) (static_cast<float>(x)/POW2(n)) 
// same for double 
#define FP_DOUBLE(x, n) (static_cast<double>(x)/POW2(n)) 
// and for int. fractional part will be discarded! 
#define FP_INT(x, n) ((x) >> n) 

// arithmetic operations for same-format numbers 
#define FP_NEG(a) ((~a)+1) 
#define FP_ADD(a, b) ((a) + (b)) 
#define FP_SUB(a, b) ((a) + FP_NEG(b)) 
#define FP_MUL(a, b, n) (((a) * (b)) >> n) 
#define FP_DIV(a, b, n) (((a) << n)/(b)) 
#define FP_POW2(a, n) (((a) * (a)) >> n) 
#define FP_POW3(a, n) (((((a) * (a)) >> n)*(a)) >> n) 

// arithmetic for different-format numbers, assuming n is the target (result) format and n > m 
#define FP_ADD_UP(a, b, n, m) ((a) + ((b) << (n-m))) 
#define FP_SUB_UP(a, b, n, m) ((a) + FP_NEG((b) << (n-m))) 
#define FP_MUL_UP(a, b, n, m) (((a) * (b)) >> m) 
#define FP_DIV_UP(a, b, n, m) (((a) << m)/(b)) 

// same for n < m 
#define FP_ADD_DOWN(a, b, n, m) ((a) + ((b) >> (m-n))) 
#define FP_SUB_DOWN(a, b, n, m) ((a) + FP_NEG((b) >> (m-n))) 
#define FP_MUL_DOWN(a, b, n, m) (((a) * (b)) >> m) 
#define FP_DIV_DOWN(a, b, n, m) (((a) << m)/(b)) 

編輯:基本上,答案和註釋轉向這兩點:

  • 的代碼是可怕的,很難使用和維護:我wholehartedly同意並會花時間把它封裝在一個類中。我對一些表現出色的問題表示了一些擔憂,並且我確信它們是毫無根據的,我毫不知情地付出了所有的麻煩。 :)
  • 無論如何,固定點並不值得麻煩:儘管在我的平臺上這可能是正確的,但我創建這個來提高我的代碼在沒有浮點硬件的平臺上的執行速度。在那裏,浮點操作耗時過長,固定是要走的路。我只在英特爾測試了這個,因爲目前我無法訪問目標硬件

雖然我非常感謝迄今爲止提供的洞察力,但我希望聽到某個實際做了一些計算的人定點告訴我這些算術運算是否確實是要走的路。也許還有一些我沒有意識到的額外的點點滴滴,這會對性能或精度產生影響嗎?換句話說,如果我要封裝這些代碼,我可以在內聯運算符函數中保留基本相同的算術指令,還是應該以某種方式更改它們?

+0

刪除'C'標籤,因爲這不是'C'代碼。 – Puppy 2011-03-17 15:58:31

+5

使用這些宏編寫的代碼將是殘酷的。你爲什麼想做這個?說實話,如果我看到'#define FP_ADD(a,b)((a)+(b))'代碼被強制維護,我會深表擔憂。 – meagar 2011-03-17 15:58:32

+0

我想我會避免一些開銷,如果我拒絕整個面向對象的東西。我可以將整個東西包裝在一個類和重載算術運算符中,但是由於AFAIK它們實際上被實現爲具有調用開銷的函數,所以我希望通過這種方式獲得一些性能。這些天我有點迷戀。另外,我想也許編譯器會更容易優化它。 – neuviemeporte 2011-03-17 16:02:52

回答

5

內聯函數。使用它們。不是宏。使用一個類,超載它的操作符。並且static_cast在C中不存在。如果您使用它發佈代碼示例,那麼這個代碼非常糟糕,這將是完全不可讀的。

請記住,浮點操作是在硬件中實現的,而您實現的定點操作是用軟件實現的。對於這種變化將會有一定的懲罰,而且很可能你的代碼在算法層面上不夠快,無法克服這種變化。

+0

我實際上編寫了代碼來使用它,所以我知道你是對的,而且我幾乎確信爲了可讀性和可維護性將它包裝在類中。儘管如此,表現仍然存在一些疑問,但我想我只需要測量一下是否有進一步的打擊。我只是不明白硬件/軟件二分法的含義。硬件當然也會執行整數操作。而且我認爲CPU需要更多的時間來乘以兩個浮點數,而不是乘以兩個整數並轉換結果? – neuviemeporte 2011-03-17 16:22:23

+2

@neuviwmeporte:浮點乘法是一個硬件操作。 int-int乘法,然後int-shift是更多寄存器(三個操作數,而不是兩個),並且您正在調用硬件兩次。如果int-int mul比float-float mul快,在許多情況下毫無價值,因爲您正在等待額外內存,或者在CPU等待下一個週期執行下一個操作時浪費時間。你也在吹更多的指令緩存。永遠不要「繪製」任何東西,*在描述代碼之前,先對它進行描述並證明你的假設。 – Puppy 2011-03-17 16:32:13

2

你可以通過編寫一個unit test找到你的實現。直到你確信你的代碼

assert(FP_ADD(7, 5) == 12) 
assert(FP_SUB(7, 5) == 2) 

覆蓋足夠用例:一個簡單的實現可以連續assertations來實現。此外,不要忘記比較double s和float s在一個小的epsilon。由於位表示限制,平等可能無法按預期工作。

+0

我基本上就是這樣做的,我相信它會給出正確的結果。我只是想知道它是否儘可能快。 – neuviemeporte 2011-03-17 20:29:25

4

這是對@ neuviemeporte關於表現的評論的迴應。我正在做這個答案,而不是評論,所以我可以更容易地格式化代碼。你說,「AFAIK它們實際上被實現爲具有調用開銷的函數」,並且「我猜結構成員也必須被某種指針引用;你不能從'this'中逃脫。這些擔憂都是有效的,但讓我們進一步調查。

我在Linux/x86上使用gcc。考慮這個程序:

typedef int fixed32; 
#define FP_ADD(a,b) ((a)+(b)) 
#define FP_NEG(a) ((~a)+1) 
#define FP_SUB(a,b) ((a)+FP_NEG(b)) 
#define FP_INT(x,n) ((x)>>n) 
#define FP_MUL(a,b,n) (((a)*(b))>>n) 
#define FP_DIV(a,b,n) (((a)<<n)/(b)) 

template<class T, unsigned n> 
struct Fixed { 
private: 
    T theBits; 

public: 
    Fixed(T t = T()) : theBits(t) {} 

    Fixed operator+(const Fixed& rhs) const { 
     return Fixed(theBits + rhs.theBits); 
    } 

    Fixed operator-(const Fixed& rhs) const { 
     return Fixed(theBits - rhs.theBits); 
    } 

    Fixed operator~() const { 
     return Fixed(~theBits); 
    } 

    Fixed operator*(const Fixed& rhs) const { 
     return Fixed((theBits*rhs.theBits)>>n); 
    } 

    Fixed operator/(const Fixed& rhs) const { 
     return Fixed((theBits<<n)/rhs.theBits); 
    } 

    operator T() const { 
     return theBits >> n; 
    } 
}; 

int DoFpAdd(const fixed32 a, const fixed32 b) { 
    fixed32 result = FP_ADD(a, b); 
    return FP_INT(result, 16); 
} 

int DoFixedAdd(const Fixed<int, 16> a, const Fixed<int, 16> b) { 
    return a+b; 
} 


int DoFpSub(const fixed32 a, const fixed32 b) { 
    fixed32 result = FP_SUB(a, b); 
    return FP_INT(result, 16); 
} 

int DoFixedSub(const Fixed<int, 16> a, const Fixed<int, 16> b) { 
    return a-b; 
} 

int DoFpMul(const fixed32 a, const fixed32 b) { 
    fixed32 result = FP_MUL(a, b, 16); 
    return FP_INT(result, 16); 
} 

int DoFixedMul(const Fixed<int, 16> a, const Fixed<int, 16> b) { 
    return a*b; 
} 

int DoFpDiv(const fixed32 a, const fixed32 b) { 
    fixed32 result = FP_DIV(a, b, 16); 
    return FP_INT(result, 16); 
} 
int DoFixedDiv(const Fixed<int, 16> a, const Fixed<int, 16> b) { 
    return a/b; 
} 

我編譯它與此命令行g++ -O4 -Wall -pedantic -ansi -S x.cc && c++filt <x.s > x.S

您可能會驚訝地發現,類似命名的函數產生相同的彙編語言。是的,FP_ADD()和Fixed <> :: operator +是相同的。沒有函數調用(全部內聯)no this指針,只是指令指令相同的彙編語言。

執行速度沒有區別。可用性,可維護性和可讀性存在巨大差異。我建議你在你使用的任何平臺上進行類似的實驗,然後切換到類接口。

+0

感謝您接受我的麻煩並提供此洞見。 :) – neuviemeporte 2011-03-17 20:14:51

+0

如果可以的話,我會贊成這一百次。 – meagar 2011-03-19 22:36:00