2011-03-17 49 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