第9章:优化浮点数和整数运算
# 第9章:优化浮点数和整数运算
# 概述
在本章中,我们将探索处理浮点和整数运算的高级技术,重点关注精度、性能和控制。我们将首先了解任意精度算术(arbitrary precision arithmetic),它使我们能够进行超出内置类型精度限制的计算。这对于密码学或高精度模拟等应用至关重要。然后,我们将研究优化算术运算(arithmetic operations)的方法,通过利用先进的CPU特性和现代编译器优化来提高性能。
接下来,我们将把重点转移到定点算术(fixed-point arithmetic),这是一种在避免浮点不精确性陷阱的同时,能更好地控制小数精度的方法。最后,我们将探索数学常数和舍入,学习如何处理像π和e这样的常数,以及如何精确管理舍入策略,以满足各种数学应用的要求。通过掌握这些技术,我们将有能力精确且高效地处理数值计算。
# 任意精度算术
# 概念及其用途
任意精度算术是指一种计算技术,它允许对超出标准数据类型(如float
和double
)精度限制的数字进行运算。在传统的浮点算术中,精度由用于表示数字的位数固定,在处理非常大或非常小的数字时,这可能会导致舍入误差、溢出或下溢。任意精度消除了这些限制,允许根据手头任务的需要,以尽可能高的精度表示和处理数字 。
这种方法在科学计算、密码学和高精度金融计算等领域特别有用,在这些领域中,float
和double
等内置类型的局限性不足以维持问题所需的精度。任意精度通常通过专门的库来实现,例如GNU多精度算术库(GNU Multiple Precision Arithmetic Library,GMP),它允许程序员表示具有数千甚至数百万位数字的数字。
# 任意精度算术的优点
在我们的行星轨道模拟中使用任意精度算术,使我们能够克服标准浮点类型的精度限制,确保即使在很长一段时间内,模拟也能保持准确。这在科学计算中至关重要,因为小的误差会随着时间累积,导致结果出现显著偏差。
主要优点包括:
- 避免舍入误差(Rounding Errors):使用标准浮点类型时,舍入误差会随着时间累积,特别是在涉及许多次迭代或数值变化非常小的模拟中。任意精度通过为计算提供所需的精度,消除了这个问题。
- 提高准确性:通过使用任意精度,我们可以模拟需要极高精度的场景,例如精确计算天体的轨道或量子力学系统。
- 灵活的精度:任意精度算术的主要优点之一是能够根据问题的要求调整精度。在某些情况下,我们可能只需要额外几位精度,而在其他情况下,可能需要数千位精度。
我们将考虑一个科学计算场景,在这个场景中任意精度算术至关重要:计算行星在很长一段时间内的轨道。
# 示例程序:行星轨道模拟
假设我们正在进行一个模拟,以预测行星在数千甚至数百万年中的轨道。行星与其他天体之间的引力相互作用对位置和速度的微小变化极为敏感。在这些计算中使用标准双精度可能会导致累积舍入误差,随着时间的推移,这些误差会导致预测轨道出现显著偏差。
在这种情况下,需要使用任意精度算术,以确保即使是位置或速度上的最小差异也能被精确捕捉,防止误差累积并扭曲结果。我们将演示如何使用任意精度算术来处理这种情况。
在这个例子中,我们将使用GMP库,它为任意精度整数、有理数和浮点数提供支持。要使用GMP,你需要安装它并将其与你的C++项目链接。在我们的演示中,我们将专注于任意精度浮点数。
以下是在C++程序中设置GMP以进行高精度计算的方法:
#include <iostream>
#include <gmpxx.h> // Include the GMP C++ interface
int main() {
// Initialize arbitrary precision floating-point numbers
mpf_class planet_position(0.0, 256); // 256 bits of precision
mpf_class planet_velocity(0.0, 256); // 256 bits of precision
// Set initial conditions for the simulation
planet_position = "1.496e11"; // Initial position of 1.496 x 10^11 meters (approx distance from Earth to Sun)
planet_velocity = "30000"; // Initial velocity of 30,000 meters per second
// Perform some calculations using arbitrary precision arithmetic
mpf_class time_step("1e5", 256); // Time step of 100,000 seconds
mpf_class gravitational_constant("6.67430e-11", 256); // Gravitational constant in m^3 kg^-1 s^-2
mpf_class mass_of_sun("1.989e30", 256); // Mass of the sun in kilograms
// Calculate the gravitational force and update the planet's position
mpf_class force = (gravitational_constant * mass_of_sun) /
(planet_position * planet_position);
planet_velocity += force * time_step;
planet_position += planet_velocity * time_step;
// Output the updated position and velocity with high precision
std::cout << "Updated position: " <<
planet_position.get_str(10) << " meters\n";
std::cout << "Updated velocity: " <<
planet_velocity.get_str(10) << " meters/second\n";
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
在这里,我们使用GMP提供的mpf_class
类型声明了两个变量planet_position
和planet_velocity
。这些变量被初始化为256位精度,远远超过了double
的精度。
初始位置设定为大约地球到太阳的距离(1.496e11米),初始速度设定为每秒30000米。这些是行星轨道模拟的典型值。
引力是根据牛顿万有引力定律计算的,两个物体之间的引力与它们之间距离的平方成反比。模拟的时间步长设置为100000秒,以模拟行星随时间的运动。
更新后的位置和速度以高精度打印输出,使用get_str()
以完整精度输出这些值。
# 示例程序:金融计算中的任意精度
虽然上面的示例侧重于科学计算,但任意精度算术在对准确性要求极高的金融应用中也至关重要。例如,在高频交易系统或大规模金融模拟中,即使是很小的舍入误差也可能导致重大的财务损失。以下是一个使用任意精度算术以极高精度计算长期复利的示例:
#include <iostream>
#include <gmpxx.h>
int main() {
// Initialize arbitrary precision floating-point numbers for financial calculations
mpf_class principal("10000.0", 256); // Initial principal of $10,000
mpf_class rate("0.05", 256); // Annual interest rate of 5%
mpf_class years("50", 256); // Investment period of 50 years
// Calculate compound interest: A = P * (1 + r)^t
mpf_class multiplier = 1.0 + rate;
mpf_class final_amount = principal * pow(multiplier, years.get_d());
// Output the final amount after 50 years with high precision
std::cout << "Final amount after 50 years: $" << final_amount.get_str(10) << "\n";
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这里,本金设定为10000美元,年利率为5%,投资期为50年。这些值使用GMP库存储为任意精度浮点数。复利公式A = P * (1 + r)^t用于计算50年后的最终金额。由于计算涉及对一个数进行幂运算,额外的精度确保了即使在很长的投资期内,结果也是准确的。
最终金额以高精度打印输出,确保即使是最小的小数部分也能被准确表示。总之,GMP库的使用消除了标准浮点类型的精度限制,确保了即使在高风险模拟中,我们的计算也能保持准确。
# 加速算术运算
数学运算的标准实现可能并不总是针对这些情况下所需的性能和速度进行优化。通过设计和使用高效算法(efficient algorithms),我们可以在保持准确性的同时高速处理复杂的数学运算。在本节中,重点将放在实现能够高效处理这些运算的算法上,从而加速这些运算。
假设我们正在进行一个实时物理模拟,计算3D空间中物体的轨迹。该模拟涉及求解牛顿运动方程,这需要频繁进行三角函数计算(如正弦、余弦和正切),以及用于变换物体位置和方向的向量和矩阵运算。由于模拟是实时运行的,这些运算必须快速执行,这意味着任何计算延迟都可能导致明显的卡顿或模拟结果错误。
为了满足实时要求,我们需要确保模拟中使用的数学运算在不牺牲准确性的前提下进行速度优化。
# 数学运算的高效算法
优化数学运算的关键在于利用高效算法,减少不必要的计算,并利用现代CPU的功能,如并行处理和向量化。在本节中,我们将演示如何实现一些常见数学运算的高效版本,包括矩阵乘法和三角函数计算。
# 优化矩阵乘法
矩阵乘法(Matrix multiplication)是物理模拟中常见的运算,在处理三维空间中的变换和旋转时尤其如此。
朴素的矩阵乘法实现可能会非常慢,特别是对于大型矩阵。然而,通过优化算法并使用高效的计算技术,我们可以显著加快运算速度。
我们将首先实现一个标准的矩阵乘法算法,然后对其进行优化。
#include <iostream>
#include <vector>
// Function to perform matrix multiplication
void matrix_multiply(const std::vector<std::vector<double>>& A, const std::vector<std::vector<double>>& B,
std::vector<std::vector<double>>& C) {
int n = A.size();
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
C[i][j] = 0;
for (int k = 0; k < n; ++k) {
C[i][j] += A[i][k] * B[k][j];
}
}
}
}
int main() {
// Initialize two 3x3 matrices
std::vector<std::vector<double>> A = { {1.0, 2.0, 3.0},
{4.0, 5.0, 6.0},
{7.0, 8.0, 9.0} };
std::vector<std::vector<double>> B = { {9.0, 8.0, 7.0},
{6.0, 5.0, 4.0},
{3.0, 2.0, 1.0} };
std::vector<std::vector<double>> C(3, std::vector<double> (3));
// Perform matrix multiplication
matrix_multiply(A, B, C);
// Output the result
std::cout << "Resulting matrix:\n";
for (const auto& row : C) {
for (double val : row) {
std::cout << val << " ";
}
std::cout << "\n";
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
在上面的示例中,我们实现了朴素的矩阵乘法算法。矩阵乘法运算通过三个嵌套循环来执行:
- 外层循环遍历矩阵A的行。
- 第二个循环遍历矩阵B的列。
- 最内层循环对矩阵A和B的对应元素进行相乘并求和。
这个基本算法是可行的,但在性能方面没有进行优化,特别是在处理较大矩阵时。该算法的时间复杂度为O(n³),对于大型矩阵来说,这个速度可能会慢得让人难以接受。
# 优化矩阵乘法算法
为了加快矩阵乘法的速度,我们可以应用多种优化技术,例如循环展开(loop unrolling)、分块(blocking,也称为平铺tiling),以及利用现代处理器中可用的单指令多数据(SIMD,Single Instruction, Multiple Data)指令。
- 分块(平铺):我们不再一次处理一个元素,而是将矩阵划分为较小的块,并分块进行处理。这种技术通过确保数据在从缓存中被逐出之前得到重用,从而提高了缓存利用率,进而减少了内存访问时间。
- SIMD指令:现代CPU支持SIMD指令,它允许一条指令同时对多个数据点进行操作。通过利用SIMD,我们可以并行执行多个算术运算,显著加快矩阵乘法的速度。
我们将使用分块技术实现一个更优化的矩阵乘法算法:
#include <iostream>
#include <vector>
#include <cmath>
// Function to perform matrix multiplication with blocking optimization
void matrix_multiply_blocked(const
std::vector<std::vector<double>>& A, const std::vector<std::vector<double>>& B,
std::vector<std::vector<double>>& C, int block_size) {
int n = A.size();
for (int i = 0; i < n; i += block_size) {
for (int j = 0; j < n; j += block_size) {
for (int k = 0; k < n; k += block_size) {
// Perform matrix multiplication for the current block
for (int ii = i; ii < std::min(i + block_size, n); ++ii) {
for (int jj = j; jj < std::min(j + block_size, n); ++jj) {
for (int kk = k; kk < std::min(k + block_size, n); ++kk) {
C[ii][jj] += A[ii][kk] * B[kk][jj];
}
}
}
}
}
}
}
int main() {
// Initialize two 3x3 matrices
std::vector<std::vector<double>> A = { {1.0, 2.0, 3.0},
{4.0, 5.0, 6.0},
{7.0, 8.0, 9.0} };
std::vector<std::vector<double>> B = { {9.0, 8.0, 7.0},
{6.0, 5.0, 4.0},
{3.0, 2.0, 1.0} };
std::vector<std::vector<double>> C(3, std::vector<double> (3));
int block_size = 2; // Set block size for blocking optimization
// Perform matrix multiplication with blocking
matrix_multiply_blocked(A, B,C, block_size);
// Output the result
std::cout << "Resulting matrix with blocking optimization:\n";
for (const auto& row : C) {
for (double val : row) {
std::cout << val << " ";
}
std::cout << "\n";
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
在上述代码中,矩阵不再是逐个元素进行处理,而是被划分为大小为block_size
的较小块。通过处理这些块,我们提高了CPU缓存的利用率,因为块会被加载到缓存中,并在被逐出之前得到重用。较小的块更适合CPU缓存,减少了缓存未命中的次数,提高了内存访问速度。分块实现使用三个外层循环来遍历矩阵A、B和C的块,使用三个内层循环来对每个块执行实际的矩阵乘法。
通过使用这种优化方法,我们可以获得更好的性能,特别是对于大型矩阵。具体的性能提升取决于矩阵的大小和所选的块大小,但一般来说,分块可以显著提高缓存利用率和处理速度。
# 优化三角函数运算
在实时物理模拟中,正弦(sine)、余弦(cosine)和正切(tangent)等三角函数经常用于旋转和变换计算。虽然这些函数的标准库实现很精确,但对于每秒需要进行数千或数百万次此类计算的实时应用来说,它们可能不够快。
加快三角函数计算的一种方法是使用预先计算好的查找表。通过预先计算一系列角度的正弦和余弦值,我们可以减少运行时执行这些计算所需的时间。以下是我们实现正弦和余弦查找表的方法:
#include <iostream>
#include <vector>
#include <cmath>
// 函数:生成正弦和余弦的查找表
void generate_trig_tables(std::vector<double>& sin_table, std::vector<double>& cos_table, int num_angles) {
for (int i = 0; i < num_angles; ++i) {
double angle = (i * 2 * M_PI) / num_angles; // 将索引转换为弧度制的角度
sin_table[i] = std::sin(angle);
cos_table[i] = std::cos(angle);
}
}
// 函数:使用预先计算好的正弦查找表
double fast_sin(const std::vector<double>& sin_table, int index) {
return sin_table[index];
}
// 函数:使用预先计算好的余弦查找表
double fast_cos(const std::vector<double>& cos_table, int index) {
return cos_table[index];
}
int main() {
int num_angles = 360; // 要预先计算的角度数量(1度分辨率)
std::vector<double> sin_table(num_angles);
std::vector<double> cos_table(num_angles);
// 生成正弦和余弦查找表
generate_trig_tables(sin_table, cos_table, num_angles);
// 使用预先计算的值进行快速正弦和余弦计算
int angle_index = 90; // 示例:90度(索引90)
std::cout << "Fast sine of 90 degrees: " << fast_sin(sin_table, angle_index) << "\n";
std::cout << "Fast cosine of 90 degrees: " << fast_cos(cos_table, angle_index) << "\n";
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
在这里:
- 我们预先计算了一系列角度(在这种情况下,是0到360度)的正弦和余弦值,并将它们存储在查找表中。这使我们能够在运行时即时检索这些值,无需反复计算正弦和余弦函数。
- 并且,在模拟过程中,我们不再调用
std::sin()
和std::cos()
,而是直接在表中查找预先计算的值,从而显著加快了计算速度。
通过使用分块和查找表等技术,我们可以显著加快这些运算,在计算要求较高的应用中实现实时性能。这些优化确保我们的数学运算既快速又准确,满足高性能计算任务的需求。
# 用定点运算(Fixed-Point Arithmetic)进行精确控制
# 定点运算基础
定点运算为浮点运算(floating-point arithmetic)提供了一种强大的替代方案,尤其适用于对数字表示需要精确控制的性能关键型应用。与浮点型数字不同,浮点型数字提供动态范围,但可能会引入舍入误差,而定点数在小数点前后用固定数量的数字表示值。这使得定点数具有更高的精度和可预测性,特别是在硬件或性能限制使浮点运算太慢或不够精确的系统中。
定点运算在嵌入式系统(embedded systems)、实时应用(real-time applications)和数字信号处理(Digital Signal Processing,DSP)中特别有价值,在这些系统中,确定性的行为和较低的计算开销至关重要。在这类系统中,精度要求是预先确定的,而且许多嵌入式处理器中没有浮点单元(Floating-Point Unit,FPU),这使得浮点运算在时间和能耗方面成本较高。
定点数用固定数量的比特位来表示整数部分和小数部分。例如,一个16比特的定点数可能会给整数部分分配8比特,给小数部分分配8比特,这样实际上我们就可以表示范围在-128.0到127.99609375之间的数字,精度为1/256。这种固定的分配方式简化了加法、减法和乘法等算术运算,因为这些运算可以使用整数运算来执行,而整数运算比浮点运算更快且更具可预测性。
# 定点运算的性能优势
定点运算相比浮点运算有几个优点,尤其在像嵌入式系统和实时控制系统这样的性能关键型应用中:
- 速度:定点运算通常比浮点运算快,特别是在没有浮点单元(FPU)的硬件上。许多嵌入式处理器没有FPU,这意味着浮点运算必须在软件中模拟,而这比整数运算慢得多。
- 可预测性:在实时系统中,可预测的执行时间至关重要。定点运算具有确定性的性能,这意味着执行运算所需的时间是恒定的,而浮点运算的性能可能会根据所处理的值而变化。
- 内存效率:定点数使用更少的比特位来表示与浮点型数字相同范围的值。这在内存有限的系统(如微控制器)中尤为重要。
- 精度控制:使用定点运算,我们可以通过调整分配给小数部分的比特数来明确控制精度。这使我们能够根据应用的需求,在精度和范围之间进行微调。
# 实现定点运算
我们将创建一个简单的类来表示定点数,并演示如何对它们进行算术运算。在我们的示例中,我们将使用16比特来表示数字,其中8比特用于整数部分,8比特用于小数部分。
#include <iostream>
#include <cstdint>
class FixedPoint {
public:
FixedPoint(int32_t value = 0) : raw_value(value) {}
// 从double转换为定点数
FixedPoint(double value) {
raw_value = static_cast<int32_t>(value * scaling_factor);
}
// 转换为double以便输出
double to_double() const {
return static_cast<double>(raw_value) / scaling_factor;
}
// 加法
FixedPoint operator+(const FixedPoint& other) const {
return FixedPoint(raw_value + other.raw_value);
}
// 减法
FixedPoint operator-(const FixedPoint& other) const {
return FixedPoint(raw_value - other.raw_value);
}
// 乘法
FixedPoint operator*(const FixedPoint& other) const {
// 两个定点数相乘需要进行缩放调整
return FixedPoint((raw_value * other.raw_value) / scaling_factor);
}
// 除法
FixedPoint operator/(const FixedPoint& other) const {
// 除法需要进行缩放调整以保持精度
return FixedPoint((raw_value * scaling_factor) / other.raw_value);
}
// 输出值
void print() const {
std::cout << to_double() << std::endl;
}
private:
int32_t raw_value; // 以整数形式存储定点值
static constexpr int32_t scaling_factor = 256; // 8个小数位(2^8 = 256)
};
int main() {
// 初始化表示温度的定点数
FixedPoint temp1(22.75); // 22.75摄氏度
FixedPoint temp2(18.5); // 18.5摄氏度
// 进行算术运算
FixedPoint sum = temp1 + temp2;
FixedPoint diff = temp1 - temp2;
FixedPoint product = temp1 * FixedPoint(2.0); // 乘以2(自动处理缩放调整)
FixedPoint quotient = temp1 / FixedPoint(1.5); // 除以1.5
// 输出结果
std::cout << "Sum: "; sum.print();
std::cout << "Difference: "; diff.print();
std::cout << "Product: "; product.print();
std::cout << "Quotient: "; quotient.print();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
在这里,FixedPoint
类将数字存储为整数(raw_value
),同时表示整数部分和小数部分。使用256作为缩放因子,对应8个小数位。这使我们能够以1/256的精度表示小数。构造函数允许我们从double
类型初始化一个定点数。该值会乘以缩放因子,然后转换为整数,作为定点值存储。
在main()
函数中,我们使用定点运算表示两个温度读数(22.75摄氏度和18.5摄氏度)。我们执行基本的算术运算(加法、减法、乘法和除法)并输出结果。
# 示例程序:用于温度控制的定点运算
我们将扩展之前的温度控制系统,以模拟一个实时反馈回路。在这个回路中,系统会根据当前温度与所需设定点之间的差异来调节加热元件。该模拟需要快速、精确的计算,这使得定点运算成为理想之选。
#include <iostream>
#include <cstdint>
class FixedPoint {
public:
FixedPoint(int32_t value = 0) : raw_value(value) {}
FixedPoint(double value) {
raw_value = static_cast<int32_t>(value * scaling_factor);
}
double to_double() const {
return static_cast<double>(raw_value) / scaling_factor;
}
FixedPoint operator+(const FixedPoint& other) const {
return FixedPoint(raw_value + other.raw_value);
}
FixedPoint operator-(const FixedPoint& other) const {
return FixedPoint(raw_value - other.raw_value);
}
void print() const {
std::cout << to_double() << std::endl;
}
private:
int32_t raw_value;
static constexpr int32_t scaling_factor = 256;
};
void control_temperature(const FixedPoint& current_temp, const FixedPoint& target_temp) {
FixedPoint error = target_temp - current_temp;
FixedPoint adjustment = error * FixedPoint(0.1);
std::cout << "Adjustment needed: ";
adjustment.print();
}
int main() {
FixedPoint current_temp(21.5);
FixedPoint target_temp(23.0);
control_temperature(current_temp, target_temp);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
在上述示例脚本中,我们模拟了一个简单的反馈回路。在这个回路中,使用定点运算来计算当前温度与目标温度之间的差异,并据此调节加热元件。定点运算的使用确保了计算既快速又精确,使其成为嵌入式设备中实时控制系统的理想选择。
# 掌握数学常量和舍入
在C++23中,数学常量的处理和舍入运算有了显著改进,开发者可以编写更精确、更高效的代码。像π(圆周率,pi)、e(自然常数,Euler’s number)等数学常量已被引入标准库,简化了对这些常用值的访问,开发者无需依赖自定义定义或外部库。此外,舍入技术的改进让开发者能更精细地控制值的舍入方式,确保在对性能要求苛刻的应用中保持数值精度。
在本节中,我们将回顾之前展示过的一个程序——使用定点运算的温度控制系统,并展示C++23中引入的新数学常量和舍入技术如何提升数值计算。
# 数学常量
在C++23之前,开发者必须手动定义常用的数学常量,或者依赖外部库。现在,C++23引入了标准化常量,可直接从std::numbers
命名空间中使用,确保在不同的编译器和平台上具有一致的精度。
引入的一些重要常量包括:
- π(
std::numbers::pi
):代表数学常数圆周率,约等于3.141592653589793。 - e(
std::numbers::e
):代表自然常数,约等于2.718281828459045。 - 黄金分割比(
std::numbers::phi
):代表黄金分割比,约等于1.618033988749895。
这些常量可直接用于计算,无需手动定义,确保了常量提供的精度一致,并针对平台进行了优化。
# 在温度控制系统中应用数学常量
我们以温度控制系统为例,在该系统中,当前温度和目标温度会基于反馈回路进行调整。在实际系统中,可能需要对温度值进行一些转换,比如将调整值乘以π,以反映系统的几何形状(例如,在圆形加热系统中调节温度)。
下面,我们将展示如何将这些数学常量集成到系统中。
#include <iostream>
#include <cstdint>
#include <numbers>
class FixedPoint {
public:
FixedPoint(int32_t value = 0) : raw_value(value) {}
FixedPoint(double value) {
raw_value = static_cast<int32_t>(value * scaling_factor);
}
double to_double() const {
return static_cast<double>(raw_value) / scaling_factor;
}
FixedPoint operator+(const FixedPoint& other) const {
return FixedPoint(raw_value + other.raw_value);
}
FixedPoint operator-(const FixedPoint& other) const {
return FixedPoint(raw_value - other.raw_value);
}
FixedPoint operator*(const FixedPoint& other) const {
return FixedPoint((raw_value * other.raw_value) / scaling_factor);
}
void print() const {
std::cout << to_double() << std::endl;
}
private:
int32_t raw_value;
static constexpr int32_t scaling_factor = 256;
};
void control_temperature_with_pi(const FixedPoint& current_temp, const FixedPoint& target_temp) {
FixedPoint error = target_temp - current_temp;
FixedPoint pi_factor(std::numbers::pi);
FixedPoint adjustment = error * pi_factor;
std::cout << "Adjustment needed (scaled by pi): ";
adjustment.print();
}
int main() {
FixedPoint current_temp(21.5);
FixedPoint target_temp(23.0);
control_temperature_with_pi(current_temp, target_temp);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
在这里,我们使用数学常量π(std::numbers::pi
)来缩放温度控制系统所需的调整值。这展示了如何将std::numbers
命名空间中的常量无缝集成到现有程序中。这样做可以避免手动定义常量可能出现的问题,并确保最大精度。
在control_temperature_with_pi()
函数中,温度调整值通过π进行缩放。这可以代表一个实际场景,比如在圆形加热元件周围调节温度,系统的几何形状要求调整值根据与π相关的系数进行缩放。
通过使用预定义的数学常量,我们确保了整个计算过程中的精度。std::numbers
中的常量针对性能和精度进行了优化,可将舍入误差和截断误差降至最低。
# 改进的舍入技术
在C++23中,引入了几种新的舍入模式,使开发者能更精细地控制舍入方式。这些舍入模式在金融系统、科学计算和嵌入式系统等应用中至关重要,因为即使是很小的舍入误差,随着时间的推移也可能导致显著偏差。
C++23中引入的一些新舍入技术包括:
std::midpoint()
:计算两个数的精确中点,防止溢出。std::lerp()
:在两个值之间进行线性插值,并将结果舍入为最接近的可表示值。std::round()
、std::floor()
、std::ceil()
:这些函数经过改进,能更有效地处理边界情况,并提供更可预测的舍入行为。
我们将展示如何应用这些改进的舍入技术,优化温度控制系统中的数值计算。
# 应用改进的舍入技术
在我们的温度控制系统中,假设为了显示目的,我们希望确保温度调整值始终舍入到最接近的0.1度。这就需要精确控制值的舍入方式,确保显示的值既准确又便于阅读。
我们将使用std::round()
函数将温度值舍入到最接近的0.1度,并展示如何针对性能和精度优化舍入操作。
#include <iostream>
#include <cmath>
class FixedPoint {
public:
FixedPoint(int32_t value = 0) : raw_value(value) {}
FixedPoint(double value) {
raw_value = static_cast<int32_t>(value * scaling_factor);
}
double to_double() const {
return static_cast<double>(raw_value) / scaling_factor;
}
FixedPoint operator+(const FixedPoint& other) const {
return FixedPoint(raw_value + other.raw_value);
}
FixedPoint operator-(const FixedPoint& other) const {
return FixedPoint(raw_value - other.raw_value);
}
void print() const {
std::cout << std::round(to_double() * 10) / 10 << std::endl;
}
private:
int32_t raw_value;
static constexpr int32_t scaling_factor = 256;
};
int main() {
FixedPoint current_temp(21.57);
FixedPoint target_temp(23.06);
std::cout << "Current temperature (rounded to nearest 0.1): ";
current_temp.print();
std::cout << "Target temperature (rounded to nearest 0.1): ";
target_temp.print();
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
在print()
函数中,我们使用std::round()
将温度值舍入到最接近的0.1度。具体做法是将值乘以10,舍入到最接近的整数,然后再除以10得到最终结果。
通过将温度值舍入到最接近的0.1度,我们确保系统在显示用户友好的值的同时,保持控制系统计算所需的精度。在一些系统中,小的舍入误差可能导致不稳定或错误结果,这种处理方式在这类系统中尤为重要。
通过结合新的数学常量和改进的舍入技术,我们可以在嵌入式系统、实时模拟和科学计算等对性能要求苛刻的应用中优化数值计算。使用标准化常量可确保跨平台的精度一致性,而新的舍入模式让开发者能更精细地控制值的截断或舍入方式,降低累积误差的风险。
# 总结
最终目标是通过探索更精确、高效地管理整数和浮点运算的方法来增强数值计算。本章从任意精度运算开始,它借助像GMP这样的库克服了标准浮点类型的限制。接着,我们专注于使用高效算法优化算术运算。我们研究了矩阵乘法和三角函数运算,展示了分块和查找表等技术如何显著提升性能。
随后重点转向定点运算,事实证明它在对性能要求苛刻的应用中很有用,特别是在嵌入式系统和实时系统中。本章最后介绍了新的数学常量和改进的舍入方法。我们通过使用π
和自然常数(Euler's number)等标准库常量简化了复杂计算,并确保了精度一致性。此外,先进的舍入技术实现了更精确的截断和舍入,减少了数值计算中的误差。这些改进为在现代C++脚本中优化数值运算提供了一套完整的工具。