26、实时编程中的数值计算:浮点与定点数学

实时编程中的数值计算:浮点与定点数学

在实时编程领域,数值计算是许多应用的核心。然而,不同的计算需求和硬件环境会影响我们选择合适的数值表示和计算方法。本文将探讨浮点数学和定点数学的相关知识,包括它们的特点、应用场景以及如何在编程中使用。

1. 浮点数学与贝塞尔函数计算

在实时 C++ 编程中,浮点计算是常见的需求。以计算圆柱贝塞尔函数为例,我们可以使用 cyl_bessel_j() 模板来计算单精度浮点数下 $J_2(1.23)$ 的近似值。

const float j2 = cyl_bessel_j(UINT8_C(2), 1.23F);
// 计算结果: 0.1663694
// 已知值: 0.1663693837...

计算得到的结果 j2 为 0.1663694,与已知值 $J_2(1.23) \approx 0.1663693837…$ 在单精度浮点数约七位小数的精度范围内是一致的。

从 C++17 开始,我们也可以使用 <cmath> 库中的 std::cyl_bessel_j() 来验证这个圆柱贝塞尔函数的值。

#include <cmath>
const float j2 = std::cyl_bessel_j(2, 1.23F);
// 结果: 0.166369

泛型数值编程在实时 C++ 中非常有用,因为它具有灵活性和可扩展性。由于泛型数值编程利用模板方法,编译器可以对结果进行高度优化,从而实现高效的算法。

2. 定点数学的必要性

许多嵌入式系统应用需要进行浮点计算,但小型微控制器可能没有硬件浮点单元(FPU)来支持浮点计算。为了避免使用可能较慢的浮点仿真库来处理 32 位单精度浮点数或 64 位双精度浮点数,许多开发者选择使用基于整数的定点算术。

3. 定点数据类型

定点数是一种基于整数的数据类型,用于表示分数,可带符号,小数点左边有固定数量的整数位,右边有固定数量的小数位。定点数据类型通常用于保存实数值,也可以作为复数类的实部和虚部。

定点数据类型常见于以 2 或 10 为基数的实现中。在微控制器编程中,定点计算非常高效,因为它们使用接近整数的表示方式。

以一个 4 位二进制整数表示的定点系统为例,小数点左边有 2 位整数,右边有 2 位小数。分数 1.5 可以表示为二进制值 0b0110(即十进制值 6),这里分数值左移了 2 位(乘以 4)以适应整数表示。

现代微控制器的高性能定点数实现通常使用 2 为基数。例如,常见的有符号 16 位定点表示,有 1 位符号位,小数点左边有 7 位二进制整数,右边有 8 位二进制小数,这就是 Q7.8 定点类型。

在 Q 表示法中,整个定点数表示为一个带隐式符号位的二进制补码有符号整数。例如,Q15.16 描述了一个有 1 位符号位、15 位整数位和 16 位小数位的定点类型,其表示可以存储在 32 位有符号整数中。

定点数通常没有指数域,因此具有接近整数的表示方式。这使得定点数的加法、减法、乘法和除法等操作可以使用整数算法,比传统的浮点表示更简单且可能更高效。

Q7.8 表示可以存储的实数范围约为 $\pm [0.004 … 127.996]$,其精度在小数点左右略超过 2 位小数。由于小数点在底层整数数据类型中的位置固定,较小的数字精度会降低。

与浮点表示相比,定点类型的范围和精度通常较小,但这正是它们性能提升的原因。定点计算通过牺牲范围和精度来换取使用更简单整数算法的潜在效率提升。

在定义定点类型时,可以改变基本整数大小和/或小数点分割特性,以获得不同的性能或数值范围。例如,对于 32 位架构,可以使用有符号 32 位 Q15.16 表示;对于 8 位平台,考虑存储大小或性能时,可以使用无符号 8 位 Q0.8 表示。

Q0.8 表示可以存储小于 1 的正数定点数,精度约为 2 位小数,适用于只需要实现一些三角函数计算(如正弦和余弦函数)且精度要求不高的应用。

无论使用哪种定点表示,都必须了解其范围,并在进行定点计算时始终注意保持在数值限制范围内。

4. 可扩展的定点模板类

在 C++ 中,专门数值类型的类表示应尽可能像内置类型一样工作。为了实现这一点,专门数值类的开发者通常需要实现以下一些或全部特性:
- 构造函数 :实现自类型的复制构造函数以及从其他内置类型的构造函数。
- 赋值运算符 :实现自类型和其他内置类型的赋值运算符。
- 算术复合赋值运算符 :重载赋值运算符和算术复合赋值运算符,如 operator+= operator-= 等。
- 一元运算符 :实现全局一元运算符 operator+ operator- ,以及前缀和后缀形式的递增和递减运算符 operator++ operator--
- 二元算术运算符 :实现标准的全局二元算术运算符,如 operator+ operator- 等。
- 比较运算符 :编写专门类型和其他内置类型的全局比较运算符,如 operator< operator<= 等。
- 可选特性 :为数值类型实现 std::numeric_limits 的模板特化。

以下是一个可扩展的 fixed_point 模板类的部分代码:

// 可扩展的 fixed_point 模板类
template<typename integer_type>
class fixed_point
{
public:
    // fixed_point 类型的有符号表示
    typedef integer_type signed_value_type;
    // 默认构造函数
    fixed_point();
    // 从 POD 类型的构造函数
    fixed_point(const char);
    fixed_point(const signed char);
    fixed_point(const unsigned char);
    fixed_point(const signed short);
    fixed_point(const unsigned short);
    fixed_point(const signed int);
    fixed_point(const unsigned int);
    fixed_point(const signed long);
    fixed_point(const unsigned long);
    fixed_point(const float&);
    fixed_point(const double&);
    // 复制构造函数
    fixed_point(const fixed_point&);
    // 从另一个定点类型的复制构造
    template<typename other_type>
    fixed_point(const fixed_point<other>&);
    // 从 POD 类型的复制赋值运算符
    fixed_point& operator=(const char);
    fixed_point& operator=(const signed char);
    fixed_point& operator=(const unsigned char);
    fixed_point& operator=(const signed short);
    fixed_point& operator=(const unsigned short);
    fixed_point& operator=(const signed int);
    fixed_point& operator=(const unsigned int);
    fixed_point& operator=(const signed long);
    fixed_point& operator=(const unsigned long);
    fixed_point& operator=(const float&);
    fixed_point& operator=(const double&);
    // 复制赋值运算符
    fixed_point& operator=(const fixed_point&);
    // 从另一个定点类型的复制赋值
    template<typename other>
    fixed_point& operator=(const fixed_point<other>&);
    // 取反
    void negate();
    // 前缀递增和递减
    fixed_point& operator++();
    fixed_point& operator--();
    // 复合赋值操作
    fixed_point& operator+=(const fixed_point&);
    fixed_point& operator-=(const fixed_point&);
    fixed_point& operator*=(const fixed_point&);
    fixed_point& operator/=(const fixed_point&);
    // 转换操作
    float to_float() const;
    double to_double() const;
    signed_value_type to_int() const;
    std::int8_t to_int8() const;
    std::int16_t to_int16() const;
    std::int32_t to_int32() const;
private:
    // 内部数据表示
    signed_value_type data;
    // 特殊构造函数的内部结构
    typedef nothing internal;
    // 从数据表示的特殊构造函数
    fixed_point(const internal&, const signed_value_type&);
    // 比较函数
    // ...
    // 其他私有实现细节
    // ...
};
// 全局后缀递增和递减
// 全局二元数学运算
// 全局二元比较运算
// 全局数学函数和超越函数
// ...

fixed_point 类中,小数点分割总是在底层整数表示的中间。模板参数 integer_type 的大小决定了 fixed_point 类的规模。例如,如果 integer_type std::int16_t ,则 fixed_point 类表示 Q7.8 定点数;如果是 std::int32_t ,则表示 Q15.16 定点数。

我们还定义了四种可扩展的 fixed_point 类型:

// 定义四种可扩展的 fixed_point 类型
typedef fixed_point<std::int8_t> fixed_point_3pt4;
typedef fixed_point<std::int16_t> fixed_point_7pt8;
typedef fixed_point<std::int32_t> fixed_point_15pt16;
typedef fixed_point<std::int64_t> fixed_point_31pt32;

对于 8 位微控制器目标,前三种类型可以有效使用。在 8 位平台上,Q31.32 表示所需的有符号 64 位整数操作成本过高,应避免使用。而在 32 位微控制器上,Q31.32 表示可以非常高效。在为系统选择合适的定点类型时,分析运行时间和汇编列表以找到性能、范围和精度之间的最佳平衡是有益的。

5. 使用 fixed_point

使用 fixed_point 类非常简单。例如,我们可以将 Q7.8 定点变量 r 的值设置为约 1.23。

// r 约为 1.23
const fixed_point_7pt8 r(1.23F);

不过,使用纯整数构造定点值可能更高效。

// r 约为 1.23
const fixed_point_7pt8 r(fixed_point_7pt8(123) / 100);

在这种情况下, r 使用从整数 123 创建的中间定点对象,然后除以整数 100。一般来说,这种定点构造方式应该提供最佳性能,甚至在后续整数除法时也是如此。但需要注意的是,我们必须小心基准测试结果,以验证在特定架构上的特定定点类型是否确实如此。

同时,了解定点类型的范围限制也至关重要。例如,在上述构造函数中将中间值设置为 123 时,我们已经接近 Q7.8 表示整数部分的最大值 127。如果初始值为 234,则会导致 Q7.8 表示的整数部分溢出。

使用 fixed_point 类编写函数也很容易。例如,我们可以编写一个模板函数来计算圆的定点面积。

template<typename fixed_point_type>
fixed_point_type area_of_a_circle(const fixed_point_type& r)
{
    return (fixed_point_type::value_pi() * r) * r;
}

我们可以使用 Q7.8 定点类型来计算半径为 1.23 的圆的近似面积。

// r 约为 1.23
const fixed_point_7pt8 r(fixed_point_7pt8(123) / 100);
// a 约为 4.723
const fixed_point_7pt8 a = area_of_a_circle(r);

计算得到的面积 a 约为 4.723,与实际值 4.75291… 相差仅 0.6%。

fixed_point 类可以与其他内置整数和浮点类型无缝混合在数学表达式中。例如,我们可以实现一个简单的模板子程序来计算具有有符号整数多项式系数的三次方程的左边。

template<typename fixed_point_type,
         const int_fast8_t c0,
         const int_fast8_t c1,
         const int_fast8_t c2,
         const int_fast8_t c3>
fixed_point_type cubic(const fixed_point_type& x)
{
    return (((c3 * x + c2) * x + c1) * x) + c0;
}

此外, fixed_point 类还可以用于实现三角函数的多项式逼近。例如,对于正弦函数的 5 阶多项式逼近:

$\sin x = 1.5704128 \chi - 0.6425639 \chi^3 + 0.0722739 \chi^5 + \epsilon (x)$

其中 $\chi = x \frac{2}{\pi}$。

这个多项式在 $-\frac{\pi}{2} \leq x \leq \frac{\pi}{2}$(即 $-1 \leq \chi \leq 1$)范围内以相对误差 $|\epsilon (x)| \lesssim 0.0002$ 逼近 $\sin x$。我们可以使用 fixed_point 类实现这个多项式逼近。

// 这里是一个简单但不完整的定点正弦函数实现
// 它使用 float 会损失性能,并且缺少范围缩减和反射
// 后续会展示更高效和完整的实现

综上所述,在实时编程中,浮点数学和定点数学各有其优势和适用场景。浮点计算适用于需要高精度和大范围的场景,但可能在硬件资源有限的情况下效率较低。定点数学则通过牺牲一定的范围和精度来换取更高效的整数计算,适用于对性能要求较高的嵌入式系统。在实际应用中,我们需要根据具体的需求和硬件环境选择合适的数值计算方法。

实时编程中的数值计算:浮点与定点数学

6. 更高效的定点正弦函数实现

上文中提到的使用 fixed_point 类实现的正弦函数多项式逼近是一个较为简单和不完整的实现,它存在使用 float 导致性能损失以及缺少范围缩减和反射等问题。接下来,我们将探讨更高效和完整的定点正弦函数实现。

范围缩减是提高定点正弦函数计算效率和精度的重要步骤。由于正弦函数是周期函数,其周期为 $2\pi$,因此我们可以将输入值 $x$ 缩减到一个较小的范围内,例如 $[-\frac{\pi}{2}, \frac{\pi}{2}]$。这样可以减少多项式逼近的计算量,同时提高精度。

反射是另一个优化技巧。正弦函数具有对称性,即 $\sin(-x) = -\sin(x)$ 和 $\sin(\pi - x) = \sin(x)$。利用这些对称性,我们可以进一步将输入值范围缩减到 $[0, \frac{\pi}{2}]$。

以下是一个更高效的定点正弦函数实现的示例代码:

#include <cmath>

template<typename fixed_point_type>
fixed_point_type sin_fixed(const fixed_point_type& x)
{
    // 范围缩减到 [0, 2π]
    fixed_point_type reduced_x = x % (fixed_point_type::value_2_pi());

    // 利用对称性将范围缩减到 [0, π]
    if (reduced_x > fixed_point_type::value_pi())
    {
        reduced_x -= fixed_point_type::value_pi();
        reduced_x = -reduced_x;
    }

    // 利用对称性将范围缩减到 [0, π/2]
    if (reduced_x > fixed_point_type::value_pi_half())
    {
        reduced_x = fixed_point_type::value_pi() - reduced_x;
    }

    // 计算 χ = x * (2 / π)
    fixed_point_type chi = reduced_x * (fixed_point_type::value_2_over_pi());

    // 5 阶多项式逼近
    fixed_point_type chi_squared = chi * chi;
    fixed_point_type chi_cubed = chi_squared * chi;
    fixed_point_type chi_fifth = chi_cubed * chi_squared;

    fixed_point_type sin_value = fixed_point_type(1.5704128) * chi
                               - fixed_point_type(0.6425639) * chi_cubed
                               + fixed_point_type(0.0722739) * chi_fifth;

    return sin_value;
}

我们可以使用这个函数来计算正弦值:

// r 约为 1.23
const fixed_point_7pt8 r(fixed_point_7pt8(123) / 100);
const fixed_point_7pt8 sin_r = sin_fixed(r);
7. 定点数学的性能分析

定点数学的主要优势在于其潜在的高效性。由于定点数使用接近整数的表示方式,其加法、减法、乘法和除法等操作可以使用整数算法,这些算法通常比浮点算法更简单且执行速度更快。

然而,定点数学也有其局限性。定点数的范围和精度通常比浮点数小,这意味着在处理大范围或高精度的数值时可能会出现溢出或精度损失的问题。因此,在使用定点数学时,我们需要仔细考虑数值的范围和精度要求,并选择合适的定点类型。

为了评估定点数学的性能,我们可以进行基准测试。以下是一个简单的基准测试示例,比较使用 fixed_point 类和 float 类型计算圆面积的性能:

#include <chrono>
#include <iostream>

// 假设已经定义了 fixed_point 类和 area_of_a_circle 函数

template<typename T>
void benchmark_area_calculation(const T& r, int iterations)
{
    auto start = std::chrono::high_resolution_clock::now();

    for (int i = 0; i < iterations; ++i)
    {
        area_of_a_circle(r);
    }

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

    std::cout << "Time taken for " << iterations << " iterations: " << duration << " microseconds" << std::endl;
}

int main()
{
    const fixed_point_7pt8 r_fixed(fixed_point_7pt8(123) / 100);
    const float r_float = 1.23f;

    int iterations = 100000;

    std::cout << "Benchmarking fixed-point area calculation:" << std::endl;
    benchmark_area_calculation(r_fixed, iterations);

    std::cout << "Benchmarking floating-point area calculation:" << std::endl;
    benchmark_area_calculation(r_float, iterations);

    return 0;
}

通过运行这个基准测试,我们可以比较定点和浮点计算的性能差异。一般来说,在硬件资源有限的嵌入式系统中,定点计算可能会比浮点计算更快。

8. 选择合适的定点类型

在实际应用中,选择合适的定点类型至关重要。我们需要考虑以下几个因素:

  • 范围 :定点类型的范围决定了它可以表示的数值大小。例如,Q7.8 定点类型的范围约为 $\pm [0.004, 127.996]$,如果我们需要处理更大的数值,就需要选择范围更大的定点类型,如 Q15.16。
  • 精度 :定点类型的精度决定了它可以表示的小数位数。精度越高,计算结果越准确,但同时也会增加计算复杂度和存储需求。
  • 性能 :不同的定点类型在不同的硬件平台上可能有不同的性能表现。例如,在 8 位微控制器上,处理 64 位整数的操作可能会非常缓慢,因此应避免使用 Q31.32 定点类型。

为了选择合适的定点类型,我们可以按照以下步骤进行:

  1. 确定应用的数值范围 :分析应用中需要处理的最小和最大数值,以此来确定定点类型的范围要求。
  2. 评估精度需求 :根据应用的精度要求,选择具有足够小数位数的定点类型。
  3. 考虑硬件平台 :了解目标硬件平台的特点,如处理器位数、内存大小等,选择在该平台上性能最优的定点类型。

以下是一个选择定点类型的决策树:

graph TD;
    A[确定数值范围] --> B{范围是否小于 127.996?};
    B -- 是 --> C{精度要求是否较低?};
    C -- 是 --> D[选择 Q7.8];
    C -- 否 --> E{是否在 32 位平台?};
    E -- 是 --> F[选择 Q15.16];
    E -- 否 --> D;
    B -- 否 --> G{是否在 32 位平台?};
    G -- 是 --> F;
    G -- 否 --> H[重新评估需求或优化算法];
9. 总结与展望

在实时编程中,浮点数学和定点数学是两种重要的数值计算方法。浮点数学提供了高精度和大范围的数值表示,但在硬件资源有限的情况下可能效率较低。定点数学通过牺牲一定的范围和精度,换取了更高效的整数计算,适用于对性能要求较高的嵌入式系统。

在实际应用中,我们需要根据具体的需求和硬件环境选择合适的数值计算方法。同时,我们还可以通过优化算法,如范围缩减和反射等技巧,进一步提高定点数学的性能和精度。

未来,随着硬件技术的不断发展,定点数学和浮点数学的性能差距可能会逐渐缩小。同时,新的数值计算方法和技术也可能会不断涌现,为实时编程带来更多的选择和可能性。我们需要不断学习和探索,以适应不断变化的技术环境,为实时编程提供更高效、更精确的数值计算解决方案。

通过对浮点数学和定点数学的深入理解和应用,我们可以更好地满足实时编程的需求,开发出性能更优、稳定性更高的实时系统。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值