65.9K
CodeProject 正在变化。 阅读更多。
Home

定点类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (56投票s)

2009年6月25日

BSD

23分钟阅读

viewsIcon

146072

downloadIcon

3046

一个用于定点数数学的C++模板类。

引言

我当时正在研究颜色空间转换算法,这时就需要一个实数的定点实现。之前,我偶尔也会用到定点数数学,但都是临时编码,草草了事。

因此,我决定开始着手开发一个可重用的定点数数学类。另一个动机是我想知道是否真的可以在C++中编写一个其对象行为与内置数字类型相似的类。目标是这个定点数类

  • 应该可以作为现有浮点类型的直接替代品,
  • 在不支持浮点运算的硬件上,应该比浮点仿真更快,
  • 应该能在许多情况下通用,
  • 应该天生优美,编写精良,并为将来参考而进行文档化。

在对定点数数学进行简短介绍之后,文本解释了如何使用 fixed_point 类。然后解释了该类的实现。

此文档的最新版本始终可以在这里找到。代码的最新版本始终可以在 http://fpmath.googlecode.com 找到。

定点数数学简短介绍

定点数可以分为一个可选的符号位、一个整数部分和一个小数部分。

fpmath-1.png

整数部分由 i 位组成,小数部分由 f 位组成。

定点数 V 的总值为

fpmath-2.png

如上定义的定点数被称为 i.f 格式。

无符号定点数的范围在 0 到 2^i-1 之间。有符号定点数的范围在 -2^i 到 2^i-1 之间。定点数的精度是 2^(-f)。

有关更多信息的重要参考文献是 David Goldberg 于 1991 年 3 月在 Computing Surveys 杂志上发表的论文《每个计算机科学家都应该了解的浮点算术》。这篇文本在互联网上很容易找到,有很多下载地点。

这是一个例子:假设我们有一个 16 位定点数,采用 7.8 格式,带有一个额外的符号位,位设置为 0000 0110 1010 0000(即十六进制表示为 0x06A0)。这个定点数的值是 +6.625。

如果你有这样的位模式,无论定点格式如何,你都可以将其视为一个整数并在其上执行整数数学运算。只有在进行乘法或除法时会出现一个小小的复杂情况:你需要确保小数点在正确的位置。这大致类似于你在学校学习手算乘法的方式:在乘法结束时,你需要数小数点后的位数,然后将小数点放在正确的位置。

这对机器意味着可以使用快速整数指令来执行计算。缺点是定点数的范围和精度通常小于浮点数。

项目安装

如果你只是想在代码中使用 fixed_point 类,你只需要下载项目。你会在 include/fpml 文件夹中找到 fixed_point.h

然而,该项目还包含测试代码,包括单元测试和基准测试。如果你想运行这些测试,你还需要 CMake(版本 2.6 或更高版本),可以从 http://www.cmake.org 下载。运行 CMake,并将其指向你下载项目的目录,然后使用它生成 Visual Studio 解决方案和项目。然后你可以加载 FPMATH.sln,它将包含测试项目。

fixed_point 类使用方法

你首先应该做的是包含头文件 fixed_point.h

#include <fixed_point.h>

fixed_point<B, I, F> 类定义在 fmpl 命名空间中。为了使用它,你可以在任何需要的地方添加 fpml:: 前缀,或者你可以使用 using 语句

using namespace fmpl;

fixed_point<B, I, F> 类基本上有两种不同的用例。你可以在新编写的代码中使用它,在那里你知道你需要定点数学,或者你控制行为,即确定整数位和小数位的数量。这种第一种用例的一个极其简单的代码示例可能如下所示

#include <fixed_point.h>
using namespace fpml;

main()
{
    fixed_point<int, 16> a = 256;
    fixed_point<int, 16> b = sqrt(a);
}

当然,你也可以使用所有其他运算符和函数。fixed_point<B, I, F> 类实现了你在使用浮点数时会认为理所当然的所有重要运算符和函数。

第二种用例是移植场景,当代码已经存在并且已经用 float 或 double 类型编写时。当你仔细检查了代码是否存在范围和精度问题,并决定当浮点计算被定点计算替换时代码仍然可以工作,你可以使用 #define 来移植代码,对原始代码进行少量更改

#include <fixed_point.h>
#define double fpml::fixed_point<int, 16>

… original code here, unchanged …

#undef double

不过,您应该非常小心,因为定点数的范围和精度都将与原始浮点数不同,这可能会给原始代码引入错误。特别是在使用 sqrtlogsin 等函数时,定点数的误差传播不再是线性的,可能会出现意想不到的结果。

fixed_point 类的实现

目标之一是代码应尽可能通用。实现这一点的一种方法是使用模板。我决定使用三个模板参数

template<typename B, unsigned char I, unsigned char F
        = std::numeric_limits<B>::digits - I>
class fixed_point
{
    BOOST_CONCEPT_ASSERT((boost::Integer<B>));
    BOOST_STATIC_ASSERT(I + F == std::numeric_limits<B>::digits);
    ...
private:
    B value_;
}

B 是基本类型。这必须是整数类型。它是用于大多数计算的类型。例如 unsigned char、signed short 或 int。此类型可以根据大小、精度和性能要求进行选择。如果选择无符号类型,则 fixed_point 类的行为是无符号的;否则,它是有符号的。有符号行为更接近内置浮点类型的行为。

类体中的语句 BOOST_CONCEPT_ASSERT((boost::Integer<B>)) 确保只有整数类型可以用作基本类型。请注意,这里的基本类型不是作为基类的意义使用。实际上,fixed_point 类不从任何基类派生,而是独立存在。

I 是整数部分的位数,不包括符号位。这决定了可表示数字的范围。

F 是小数部分的位数。这决定了可表示数字的精度。在模板实例化时无需指定 F,因为它总是可以从基本类型 B 和整数位数 I 自动推断。但是,如果指定了,则需要正确。类体中的语句 BOOST_STATIC_ASSERT(I + F == std::numeric_limits<B>::digits) 确保强制执行此条件。

基本类型的位数必须满足以下公式:#(B)=S+I+F,其中

  • #(B) 是基本类型的位数,
  • S 对于有符号类型为 1,对于无符号类型为 0,
  • I 是整数位数,
  • F 是小数位数。

下表显示了可用的类型以及对 I 和 F 的要求

B

#(B)

S

F

有符号字符

8

1

7…1

0…6

unsigned char

8

0

8…1

0…7

short

16

1

15…1

0…14

unsigned short

16

0

16…1

0…15

int

32

1

31…1

0…30

无符号整数

32

0

32…1

0…31

fixed_point<B, I, F> 类型的对象与底层类型 B 的大小相同。该类经过精心设计,不会施加额外的尺寸要求。

通过将所有可用位用于整数部分,您可以实现整数行为。但是,您可以在整数上调用 sqrtlog 等已定义的函数。稍后我会详细介绍这些函数。

大于 32 位的类型尚未支持。原因之一是有些函数需要两倍大的内部结果。如果允许 64 位类型,这些内部结果将是 128 位宽。第二个原因是,如果您可以为定点类型分配 64 位,那么在许多情况下也可以使用 double 类型。

构造和转换

如果 fixed_point<B, I, F> 类型的对象要可用,它首先需要被构造。该类提供了一组构造函数。首先,是无参数的默认构造函数

fixed_point()
{ }

就像内置类型一样,不进行初始化。执行此构造函数后,值是不确定的。

其次,有一个构造函数允许从整数值构造。这是通过模板实现的

template<typename T> fixed_point(T value) : value_((B)value << F)
{
    BOOST_CONCEPT_ASSERT((boost::Integer<T>));
}

此构造函数接受类型为 T 的整数值,并将其转换为 fixed_point<B, I, F> 类型。从整数格式到定点格式的转换通过向左移动 F 位来完成,以使二进制点的位置正确。

第三,有一个构造函数允许从布尔值构造

fixed_point(bool value) : value_((B)(value * power2<F>::value))
{ }

false 构造的 fixed_point<B, I, F> 对象的值为 0.0,由 true 构造的 fixed_point<B, I, F> 对象的值为 1.0。

第四,有一些构造函数允许从浮点值构造

fixed_point(float value) : value_((B)(value * power2<F>::value))
{ }

fixed_point(double value) : value_((B)(value * power2<F>::value))
{ }

fixed_point(long double value) : value_((B)(value * power2<F>::value))
{ }

这些构造函数接受 floatdoublelong double 类型的浮点值,并将其转换为 fixed_point<B, I, F> 类型。转换通过乘以适当的 2 的幂并将结果转换为基本类型 B 来完成。适当的 2 的幂由小数部分位数 F 决定。这与整数构造函数执行的移位操作相对应。

所有到目前为止带一个参数的构造函数也用作隐式转换运算符,并将构造函数的类型转换为 fixed_point<B, I, F> 类型。因此,您可以如下使用数字值初始化 fixed_point<B, I, F> 变量

fixed_point<int, 16> a(0);
fixed_point<int, 16> b = -1.5;

最后,实现了一个拷贝构造函数

fixed_point(fixed_point<B, I, F> const& rhs) : value_(rhs.value_) 
{ }

拷贝构造函数只是简单地复制 fixed_point<B, I, F>::value_ 成员的内容。

严格来说,这个构造函数不是必需的,因为编译器会自动合成一个类似的拷贝构造函数。然而,我喜欢把事情明确化。

如前所述,带一个参数的构造函数双重地充当从数值到 fixed_point<B, I, F> 类型的转换。转换的另一个方向也是需要的,并且使用转换运算符实现,用于相同的数值类型

template<typename T> operator T() const
{
    BOOST_CONCEPT_ASSERT((boost::Integer<T>));
    return value_ >> F;
}

operator float() const
{
    return (float)value_ / power2<F>::value;
}

operator double() const
{
    return (double)value_ / power2<F>::value;
}

operator long double() const
{
    return (long double)value_ / power2<F>::value;
}

fixed_point<B, I, F> 类型的转换与构造函数对称。因此,整数转换向右移位,浮点转换除以适当的 2 的幂。

这里还有一些其他的注意事项。

遗憾的是,C++ 没有精确指定移位运算符的行为,而是将其留给实现定义。任何实现都可以自由地执行算术移位(正确处理负数的符号位)或逻辑移位(不特殊处理最左边的位)。这有可能导致代码无法按预期工作。然而,我尝试过的实现(Visual Studio 2005,Visual Studio 2008)都做了正确的事情:它们对有符号数执行算术移位,对无符号数执行逻辑移位。

浮点转换需要计算 2 的 F 次方。我本可以使用运行时函数 pow,但我不想将一个可以在编译时完成的计算推迟到运行时。不幸的是,它并非那么简单。我使用了我曾在一个地方(但不记得是哪里)看到的一种模板元编程技术,以便在编译时进行计算

template<int F> struct power2 
{
    static const long long value = 2 * power2<P-1,T>::value;
};

template <> struct power2<0> 
{
    static const long long value = 1;
};

power2 模板通过模板递归工作。例如,如果 F == 2,则执行以下步骤

  1. 实例化 power2<2>power2<2>::value 设置为 2 * power2<1>::value。由于 power2<1>::value 尚未知,编译器需要实例化 power2<1>
  2. 实例化 power2<1>power2<1>::value 设置为 2 * power2<0>::value。由于 power2<0>::value 尚未知,编译器需要实例化 power2<0>
  3. power2<0> 被实例化,power2<0>::value 为 1。现在,递归可以完全返回,有效地计算 2 * 2 * 1,即 2^2,等于 4。

运算符

当然,为了能够对 fixed_point<B, I, F> 类型的对象做一些有用的事情,需要一些运算符。

赋值与转换

有一个简单的赋值运算符,它是通过复制构造函数和交换操作实现的。

fixed_point<B, I, F> & operator =(fixed_point<B, I, F> const& rhs)
{
    fixed_point<B, I, F> temp(rhs);
    swap(temp);
    return *this;
}

交换操作本身委托给 C++ 标准库的交换函数。

void swap(fixed_point<B, I, F> & rhs)
{ 
    std::swap(value_, rhs.value_); 
}

还有一个赋值版本,可以在不同的定点格式之间进行转换,因此也需要一个转换复制构造函数。不同定点格式之间的转换可以通过根据需要将表示向左或向右移动来完成。

template<unsigned char I2, unsigned char F2>
fixed_point<B, I, F> & operator =(fixed_point<B, I2, F2> const& rhs)
{
    fixed_point<B, I, F> temp(rhs);
    swap(temp);
    return *this;
}

template<unsigned char I2, unsigned char F2>
fixed_point(fixed_point<B, I2, F2> const& rhs)
    : value_(rhs.value_)
{ 
    if (I-I2 > 0)
        value_ >>= I-I2;
    if (I2<I > 0)
        value_ <<= I2-I;
}

比较

为了比较,实现了 operator <operator ==

bool operator <(fixed_point<B, I, F> const& rhs) const
{
    return value_ < rhs.value_; 
}

bool operator ==(fixed_point<B, I, F> const& rhs) const
{
    return value_ == rhs.value_; 
}

boost::ordered_field_operators 类自动根据 operator < 实现 operator >operator >=operator <=,以及根据 operator == 实现 operator !=

在伪代码表示中,这种运算符的自动提供如下所示

boost::ordered_field_operators

operator <(a, b)

operator >(a, b)

返回 operator <(b, a);

operator >=(a, b)

返回 ! operator <(a, b);

operator <=(a, b)

返回 ! operator <(b, a);

operator ==(a, b)

operator !=(a, b)

返回 ! operator ==(a, b);

转换为布尔值

浮点类型 floatdouble 支持转换为 bool。当浮点值不等于 0 时,转换为 true,否则转换为 false。因此,我实现了转换为 bool 的功能,以及 operator !,它只返回相反的值。

operator bool() const
{
    return (bool)value_; 
}

bool operator !() const
{
    return value_ == 0; 
}

一元运算符 –

对于有符号定点类型,您可以应用一元减号运算符来获取加法逆元。对于无符号定点类型,此操作是未定义的。此外,与整数基类型 B 共享,该类型可表示的最小值无法取反,因为它会产生一个超出范围且无法表示的正值。

fixed_point<B, I, F> operator -() const
{
    fixed_point<B, I, F> result;
    result.value_ = -value_;
    return result;
}

增量和减量

浮点类型可以递增和递减 1。

fixed_point<B, I, F> & operator ++()
{
    value_ += power2<F>::value;
    return *this;
}

fixed_point<B, I, F> & operator --()
{
    value_ -= power2<F>::value;
    return *this;
}

boost::unit_steppable 类自动根据 operator ++operator -- 实现 operator ++(int)(后置递增)和 operator --(int)(后置递减)。

在伪代码表示中,这种运算符的自动提供如下所示

boost::unit_steppable

operator ++()

operator ++(int)

tmp(*this); ++tmp; return tmp;

operator --()

operator --(int)

tmp(*this); --tmp; return tmp;

加法和减法

加法和减法是为 fixed_point<B, I, F> 实现的,使用 operator +=operator -=

fixed_point<B, I, F> & operator +=(fixed_point<B, I, F> const& summand)
{
    value_ += summand.value_;
    return *this;
}

fixed_point<B, I, F> & operator -=(fixed_point<B, I, F> const& diminuend)
{
    value_ -= diminuend.value_;
    return *this;
}

boost::ordered_field_operators 类自动根据 operator +=operator -= 实现 operator +operator -

在伪代码表示中,这种运算符的自动提供如下所示

boost:: ordered_field_operators

operator +=(s)

operator +(s)

tmp(*this); tmp += s; return tmp;

operator -=(d)

operator -(d)

tmp(*this); tmp -= d; return tmp;

您应该小心加法和减法,因为——由于它们的整数实现传统——它们可能会溢出。

乘法和除法

乘法和除法是为 fixed_point<B, I, F> 实现的,使用 operator *=operator /=

fixed_point<B, I, F> & operator *=(fixed_point<B, I, F> const& factor)
{
    value_ = (static_cast<fpml::fixed_point<B, I, F>::promote_type<B>::type>
    (value_) * factor.value_) >> F;
    return *this;
}

fixed_point<B, I, F> & operator /=(fixed_point<B, I, F> const& divisor)
{
    value_ = (static_cast<fpml::fixed_point<B, I, F>::promote_type<B>::type>
    (value_) << F) / divisor.value_;
    return *this;
}

boost::ordered_field_operators 类自动根据 operator *=operator /= 实现 operator *operator /

在伪代码表示中,这种运算符的自动提供如下所示

boost:: ordered_field_operators

operator *=(s)

operator *(s)

tmp(*this); tmp *= s; return tmp;

operator /=(d)

operator /(d)

tmp(*this); tmp /= d; return tmp;

乘法和除法的实现带来了一点挑战。你可能还记得:fixed_point<B, I, F> 类有一个类型为 Bvalue_ 成员,这是一个整数类型。

现在考虑当你将两个整数相乘时会发生什么,比如说每个长度为 8 位。你将得到一个需要 16 位的结果。极端情况是当你将最大可表示值平方时,所以让我们以我们的例子为例

a = 255 = 0xFF
a*a = 65025 = 0xFE01

你清楚地看到,为了能够保留乘法的每个可能结果,你需要 16 位,这是原始因子位数的两倍。换句话说:两个 位数的因子相乘会产生一个 位数的结果。

整数的位数是其类型的一个属性,即无符号字符的长度为 8 位,无符号短整型的长度为 16 位等。幸运的是,我们可以使用模板进行一些类型操作,以便为我们的乘法结果找到正确的位数和类型。

对于每个可用作基类 B(也称为小类型)的类型,我们必须找到一个具有两倍位数的可用于结果的对应类型(也称为大类型)。

小类型

大类型

有符号字符

有符号短整型

unsigned char

unsigned short

有符号短整型

有符号整型

unsigned short

无符号整数

有符号整型

有符号长长整型

无符号整数

无符号长长整型

我提供了一组私有模板结构,它们在编译时提供必要的信息

template<>
struct promote_type<signed char>
{
    typedef signed short type;
};

template<>
struct promote_type<unsigned char>
{
    typedef unsigned short type;
};

template<>
struct promote_type<signed short>
{
    typedef signed int type;
};

template<>
struct promote_type<unsigned short>
{
    typedef unsigned int type;
};

template<>
struct promote_type<signed int>
{
    typedef signed long long type;
};

template<>
struct promote_type<unsigned int>
{
    typedef unsigned long long type;
};

这个更大的结果只被临时使用。乘法完成后,通过将结果右移恰好 F 位来固定小数点。移位后,结果被转换回原始类型。这时您必须小心,因为乘法可能会溢出,这与整数乘法溢出的方式大致相同。

除法类似,被除数有 位,除数有 位,结果有 位。

左移和右移

左移运算符 << 和右移运算符 >> 未为浮点类型定义。但是,fixed_point<B, I, F> 定义了它们,因为当 F 设置为 0 时,它也可以用于模拟整数类型。在这种情况下,fixed_point<B, I, F> 的行为类似于整数,但提供了对基本数学函数的访问(即,如果您想计算整数的平方根,则很有用)。

fixed_point<B, I, F> & operator >>=(size_t shift)
{
    value_ >>= shift;
    return *this;
}

fixed_point<B, I, F> & operator <<=(size_t shift)
{
    value_ <<= shift;
    return *this;
}

boost::shiftable 类自动根据 operator >>=operator <<= 实现 operator >>operator <<

在伪代码表示中,这种运算符的自动提供如下所示

boost::shiftable

operator >>=(n)

operator >>(n)

tmp(*this); tmp >>= n; return tmp;

operator <<=(n)

operator <<(n)

tmp(*this); tmp <<= n; return tmp;

输入和输出

为了方便,已经实现了流输入和输出运算符。我使用与 double 之间的转换来实现这些运算符。

template<typename S, typename B, unsigned char I, unsigned char F>
S & operator>>(S & s, fpml::fixed_point<B, I, F> & v)
{
    double value=0.;
    s >> value;
    if (s)
        v = value;
    return s;
}

输入流 S 是一个模板参数。这允许您使用任何流,无论是文件流还是字符串流,无论是 ANSI 还是宽字符流。

template<typename S, typename B, unsigned char I, unsigned char F>
S & operator<<(S & s, fpml::fixed_point<B, I, F> const& v)
{
    double value = v;
    s << value;
    return s;
}

同样,为输出流使用模板参数 S 允许您使用任何流,无论它是文件流还是字符串流,或者它是 ANSI 流还是宽流。

流运算符委托给 double 类型。我没有看到从头开始为 fixed_point<B, I, F> 类型实现这些运算符的巨大好处。输入和输出本质上是缓慢的,因此在这些情况下诉诸浮点数学应该是可以承受的。

函数

有了这些运算符,fixed_point<B, I, F> 类可以轻松地进行大量数学计算,例如矩阵乘法等所需的计算。然而,我感觉如果该类没有 C++ 标准库为浮点类型提供的其他函数,例如 sqrtsinlog 等,它就不完整。虽然我们可以将 fixed_point<B, I, F> 值转换为浮点类型,然后调用 C++ 标准库中的相应函数,但这在某种程度上会违背 fixed_point<B, I, F> 类最初的目的。

因此,我也为 fixed_point<B, I, F> 类实现了数学函数。这些函数很难正确实现。我花了一些功夫来选择正确的算法,但我怀疑质量是否能达到某些 C++ 标准库实现的高度。

绝对值

fabs 函数计算其参数的绝对值。

friend fixed_point<B, I, F> fabs(fixed_point<B, I, F> x)
{
    return x < fixed_point<B, I, F>(0) ? -x : x;
}

ceil

ceil 函数计算不小于其参数的最小整数值。

friend fixed_point<B, I, F> ceil(fixed_point<B, I, F> x)
{
    fixed_point<B, I, F> result;
    result.value_ = x.value_ & ~(power2<F>::value-1);
    return result + fixed_point<B, I, F>(
        x.value_ & (power2<F>::value-1) ? 1 : 0);
}

floor

floor 函数计算不大于其参数的最大整数值。

friend fixed_point<B, I, F> floor(fixed_point<B, I, F> x)
{
    fixed_point<B, I, F> result;
    result.value_ = x.value_ & ~(power2<F>::value-1);
    return result;
}

fmod

fmod 函数计算 x/y 的定点余数。

friend fixed_point<B, I, F> fmod(fixed_point<B, I, F> x, fixed_point<B, I, F> y)
{
    fixed_point<B, I, F> result;
    result.value_ = x.value_ % y.value_;
    return result;
}

modf

modf 函数将参数分解为整数部分和小数部分,每个部分与参数具有相同的符号。它将整数部分存储在 ptr 指向的对象中,并返回 x/y 的带符号小数部分。

friend fixed_point<B, I, F> modf(fixed_point<B, I, F> x, fixed_point<B, I, F> * ptr)
{
    fixed_point<B, I, F> integer;
    integer.value_ = x.value_ & ~(power2<F>::value-1);
    *ptr = x < fixed_point<B, I, F>(0) ? 
        integer + fixed_point<B, I, F>(1) : integer;
        
    fixed_point<B, I, F> fraction;
    fraction.value_ = x.value_ & (power2<F>::value-1);
    
    return x < fixed_point<B, I, F>(0) ? -fraction : fraction;
}

exp

exp 函数计算 x 的指数函数。

friend fixed_point<B, I, F> exp(fixed_point<B, I, F> x)
{
    fixed_point<B, I, F> a[] = {
        1.64872127070012814684865078781, 
        …
        1.00000000046566128741615947508 };
 
    fixed_point<B, I, F> e(2.718281828459045);

    fixed_point<B, I, F> y(1);
    for (int i=F-1; i>=0; --i)
    {
        if (!(x.value_ & 1<<i))
            y *= a[F-i-1];
    }

    int x_int = (int)(floor(x));
    if (x_int<0)
    {
        for (int i=1; i<=-x_int; ++i)
            y /= e;
    }
    else
    {
        for (int i=1; i<=x_int; ++i)
            y *= e;
    }
 
    return y;
}

cos

计算余弦。

该算法采用麦克劳林级数展开。

首先,将参数简化到 -Pi 到 Pi 的范围。然后,展开麦克劳林级数。参数简化存在问题,因为 Pi 无法精确表示。减少的轮数越多,参数的重要性就越低(每轮减少都会产生轻微的误差),以至于减少后的参数以及结果都毫无意义。

参数约简使用一次除法。级数展开使用 3 次加法和 4 次乘法。

friend fixed_point<B, I, F> cos(fixed_point<B, I, F> x)
{
    fixed_point<B, I, F> x_ = fmod(x, fixed_point<B, I, F>(M_PI * 2));
    if (x_ > fixed_point<B, I, F>(M_PI))
        x_ -= fixed_point<B, I, F>(M_PI * 2);

    fixed_point<B, I, F> xx = x_ * x_;

    fixed_point<B, I, F> y = - xx * 
        fixed_point<B, I, F>(1. / (2 * 3 * 4 * 5 * 6));
    y += fixed_point<B, I, F>(1. / (2 * 3 * 4));
    y *= xx;
    y -= fixed_point<B, I, F>(1. / (2));
    y *= xx;
    y += fixed_point<B, I, F>(1);

    return y;
}

sin

计算正弦。

该算法采用麦克劳林级数展开。

首先,将参数减少到 -Pi 到 Pi 的范围。然后展开麦克劳林级数。参数减少存在问题,因为 Pi 无法精确表示。减少的轮数越多,参数的重要性就越低(每轮减少都会产生轻微误差),以至于减少后的参数以及结果都毫无意义。

参数约简使用一次除法。级数展开使用 3 次加法和 5 次乘法。

friend fixed_point<B, I, F> sin(fixed_point<B, I, F> x)
{
    fixed_point<B, I, F> x_ = fmod(x, fixed_point<B, I, F>(M_PI * 2));
    if (x_ > fixed_point<B, I, F>(M_PI))
        x_ -= fixed_point<B, I, F>(M_PI * 2);

    fixed_point<B, I, F> xx = x_ * x_;

    fixed_point<B, I, F> y = - xx * 
        fixed_point<B, I, F>(1. / (2 * 3 * 4 * 5 * 6 * 7));
    y += fixed_point<B, I, F>(1. / (2 * 3 * 4 * 5));
    y *= xx;
    y -= fixed_point<B, I, F>(1. / (2 * 3));
    y *= xx;
    y += fixed_point<B, I, F>(1);
    y *= x_;

    return y;
}

sqrt

sqrt 函数计算其参数的非负平方根。

它使用整数算法计算平方根的近似值。该算法在维基百科中有所描述:http://en.wikipedia.org/wiki/Methods_of_computing_square_roots

该算法似乎起源于 C. Woo 先生关于编程算盘的一本书。

该函数返回参数的平方根。如果参数为负,则函数返回 0。

friend fixed_point<B, I, F> sqrt(fixed_point<B, I, F> x)
{
    if (x < fixed_point<B, I, F>(0))
    {
        errno = EDOM;
        return 0;
    }
    fixed_point<B, I, F>::promote_type<B>::type op = 
        static_cast<fixed_point<B, I, F>::promote_type<B>::type>(
            x.value_) << (I - 1);
    fixed_point<B, I, F>::promote_type<B>::type res = 0;
    fixed_point<B, I, F>::promote_type<B>::type one = 
        (fixed_point<B, I, F>::promote_type<B>::type)1 << 
            (std::numeric_limits<fixed_point<B, I, F>::promote_type<B>
                ::type>::digits - 1);
    while (one > op)
        one >>= 2;
    while (one != 0)
    {
        if (op >= res + one)
        {
            op = op - (res + one);
            res = res + (one << 1);
        }
        res >>= 1;
        one >>= 2;
    }
    fixed_point<B, I, F> root;
    root.value_ = static_cast<B>(res);
    return root;
}

特性类 std::numeric_limits<>

如果没有 std::numeric_limits<> 模板的特化,fixed_point<B, I, F> 类就不完整。std::numeric_limits<> 模板允许您查询任何数字类型的信息,例如其最小值和最大值,以及更多信息。特性对于编写真正通用的代码是不可或缺的,这种代码与类型无关,并且可以神奇地适用于不同的类型。

特性

type

value

has_denorm

const float_denorm_style

denorm_absent

has_denorm_loss

const bool

false

has_infinity

const bool

false

has_quiet_NaN

const bool

false

has_signaling_NaN

const bool

false

is_bounded

const bool

true

is_exact

const bool

true

is_iec559

const bool

false

is_integer

const bool

false

is_modulo

const bool

false

is_signed

const bool

numeric_limits<B> (1)

is_specialized

const bool

true

tinyness_before

const bool

false

traps

const bool

false

round_style

const float_round_style

round_toward_zero

digits

const int

digits10

const int

digits

max_exponent

const int

0

max_exponent10

const int

0

min_exponent

const int

0

min_exponent10

const int

0

radix

const int

0

min()

fixed_point<B, I, F>

(2)

max()

fixed_point<B, I, F>

(2)

epsilon()

fixed_point<B, I, F>

(2)

round_error()

fixed_point<B, I, F>

(2)

denorm_min()

fixed_point<B, I, F>

(3)

infinity()

fixed_point<B, I, F>

(3)

quiet_NaN()

fixed_point<B, I, F>

(3)

signaling_NaN()

fixed_point<B, I, F>

(3)

  1. 取决于基本类型。如果基本类型 B 有符号,则 fixed_point<B, I, F> 也带符号。否则,它不带符号。
  2. 这些值是根据模板参数计算的。
  3. 这些值对于定点类型来说毫无意义,并设置为零。

调试辅助工具

Visual Studio 支持调试器可视化工具,这有助于调试器漂亮地显示变量。如果没有专门的可视化工具,fixed_point<B, I, F> 类型的变量的默认显示是无用的,除非您手动进行正确的浮点转换。

fpmath-3.png

显示的值 -49152 没有任何意义,除非您碰巧知道为了获得编码值,您需要将值 -49152 除以某个数。

然而,这是一个调试器可视化工具可以为您自动完成的事情。使用此可视化工具,该值可以正确显示(它没有正确的小数位数,但这并不是一个主要问题)。

fpmath-4.png

Visual Studio 将调试器可视化工具保存在名为 autoexp.dat 的文本文件中,位于 [Visualizer] 部分。该文件可以在 %VSINSTALLDIR%\Common7\Packages\Debugger 文件夹中找到。以下是 fixed_point<B, I, F> 类型的调试器可视化工具的定义

;------------------------------------------------------------------------------
; fpml::fixed_point
;------------------------------------------------------------------------------
fpml::fixed_point<*,*,*>{
    preview (
        #if ($T3 == 32)( #( $e.value_ / 4294967296., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 31)( #( $e.value_ / 2147483648., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 30)( #( $e.value_ / 1073741824., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 29)( #( $e.value_ / 536870912., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 28)( #( $e.value_ / 268435456., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 27)( #( $e.value_ / 134217728., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 26)( #( $e.value_ / 67108864., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 25)( #( $e.value_ / 33554432., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 24)( #( $e.value_ / 16777216., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 23)( #( $e.value_ / 8388608., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 22)( #( $e.value_ / 4194304., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 21)( #( $e.value_ / 2097152., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 20)( #( $e.value_ / 1048576., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 19)( #( $e.value_ / 524288., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 18)( #( $e.value_ / 262144., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 17)( #( $e.value_ / 131072., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 16)( #( $e.value_ / 65536., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 15)( #( $e.value_ / 32768., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 14)( #( $e.value_ / 16384., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 13)( #( $e.value_ / 8192., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 12)( #( $e.value_ / 4096., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 11)( #( $e.value_ / 2048., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 10)( #( $e.value_ / 1024., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 9)( #( $e.value_ / 512., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 8)( #( $e.value_ / 256., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 7)( #( $e.value_ / 128., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 6)( #( $e.value_ / 64., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 5)( #( $e.value_ / 32., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 4)( #( $e.value_ / 16., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 3)( #( $e.value_ / 8., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 2)( #( $e.value_ / 4., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 1)( #( $e.value_ / 2., " fixed_point ", $T2,".",$T3 )) 
        #elif ($T3 == 0)( #( $e.value_ / 1., " fixed_point ", $T2,".",$T3 )) 
    )
}

如果您想了解有关原生代码调试器可视化工具的更多信息,可以在 https://svn.boost.org/trac/boost/wiki/DebuggerVisualizers 找到一份不错的文档。

使用要求

fixed_point<B, I, F> 类型以纯头文件库的形式提供。无需链接任何内容。只需在需要的地方包含 fixed_point.h,一切都应该正常工作。

源代码需要最新版本的 Boost (https://boost.ac.cn),并假定已安装且可在包含路径中找到。您可能需要确保能找到 Boost 的安装位置。Boost 用于静态断言(编译时断言)和概念检查,以确保类型和值按预期使用。此外,Boost 运算符库用于自动提供一组运算符。

所有这些 Boost 库都只是头文件,确实需要 Boost 文件,但不需要您经历漫长的构建任何 Boost 库的过程。

目前,我已使用 Visual Studio 2005 和 2008 进行测试,但其他符合标准的编译器也应该可以正常工作。

测试

除了代码,我还提供了一个测试程序,用于测试该类和所有函数。并非所有可能的类型、整数位和小数位组合都经过测试,但已测试了一个合理的参数子集。

如果您的要求有所不同,您可以轻松修改测试程序以确保您的要求得到适当测试。

基准测试

虽然该库的主要关注点是在没有浮点硬件的嵌入式系统上使用,但我还是忍不住在PC平台上进行了一些计时测量。我计时了各种操作,并将计时结果与 floatdouble 类型进行了比较。结果以每操作时钟周期数表示。基准测试程序在循环中执行大量操作并计算时钟周期。然后将总时钟周期数除以重复次数。

函数

float

double

15.16

加法

1.00367

1.00365

1.00373

乘法

1.00368

1.00371

1.00376

除法

1.00369

1.0037

1.0037

Sqrt

1.00371

1.00371

473.881

正弦

1.00364

1.00375

162.533

你可以看到加法、乘法和除法等基本操作很快,但函数仍需改进。

结论

开发这个类是一项相当大的工作,一开始我无法想象为定点数类编写基本函数会有多困难。我没有考虑 std::numeric_limits<>,也没有考虑调试器可视化工具。

我读了几本书(点击缩略图会带你到亚马逊上的图书页面),并在此过程中学到了很多。

fpmath-5.pngfpmath-6.pngfpmath-7.png

最后,经过比预期长得多的时间,我将许多东西整合在一起,总的来说,我现在对代码还算满意。

这并不意味着什么,因为最终,您也需要感到满意。我非常期待您的反馈,这希望能帮助我并给我足够的动力来进一步改进代码及其实用性。感谢您阅读到此,祝您好运。

© . All rights reserved.