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))

编辑:基本上,答案和评论转向这两点:

  • 代码是可怕的,难以使用和维护:我完全同意并将花时间将它封装在一个类中。我对性能有一些担忧,这些担忧得到了解决,我一直相信它们是没有根据的,我费尽心思,一无所获。:)
  • 无论如何,定点不值得麻烦:虽然这在我的平台上可能是正确的,但我创建它是为了提高我的代码在没有浮点硬件的平台上的执行速度。在那里,浮点运算花费了太长时间,固定是要走的路。我只是在英特尔上测试这个,因为我目前无法访问目标硬件

虽然我非常感谢迄今为止提供的洞察力,但我希望听到有人实际上在定点进行了一些计算,告诉我这些算术运算是否确实是要走的路。也许还有一些我不知道的额外的花哨的小玩意儿,这在性能或精度方面会有所不同?换句话说,如果我要封装这段代码,我是否可以在内联运算符函数中保持与现在基本相同的算术指令,还是应该以某种方式更改它们?

4

4 回答 4

5

Inline functions. Use them. Not macros. Use a class, overload it's operators. And static_cast does not exist in C. This code is so abysmally terrible, if you posted a code sample using it, it would be totally unreadable.

Remember that floating point operations are implemented in hardware, and the fixed-point operations you've implemented are in software. There's going to be a penalty for that change and it could easily be that your code simply isn't fast enough at the algorithmic level to overcome that change.

于 2011-03-17T15:59:47.857 回答
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+ 是一样的。没有函数调用(都是内联的)没有this指针,只是指令对指令相同的汇编语言。

执行速度没有区别。在可用性、可维护性和可读性方面存在巨大差异。我建议您在使用的任何平台上执行类似的实验,然后切换到类接口。

于 2011-03-17T17:02:48.630 回答
2

您可以通过为您的实现编写单元测试来找出答案。一个简单的实现可以通过连续的断言来实现:

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

等等

涵盖足够多的用例,直到您对自己的代码充满信心。另外,不要忘记在一个小 epsilon 内比较doubles 和s。float由于其位表示限制,平等可能无法按预期工作。

于 2011-03-17T16:03:15.617 回答
2

您可能应该阅读 C++ 委员会的这篇论文——“关于 C++ 性能的技术报告”。

http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf

它有效地扼杀了一些神话。

于 2011-03-17T16:50:03.923 回答