当我使用带有整数的 C++ 数学函数时,我应该担心精度吗?
Should I worry about precision when I use C++ mathematical functions with integers?
例如,由于浮点数的精度,下面的代码将产生不希望的结果。
double a = 1 / 3.0;
int b = a * 3; // b will be 0 here
不知道用数学函数会不会出现类似的问题。例如
int a = sqrt(4); // Do I have guarantee that I will always get 2 here?
int b = log2(8); // Do I have guarantee that I will always get 3 here?
如果没有,如何解决这个问题?
编辑:
其实我在为一个算法任务编程的时候遇到过这个问题。我想得到
the largest integer which is power of 2 and is less than or equal to integer N
所以round函数不能解决我的问题。我知道我可以通过一个循环来解决这个问题,但似乎不是很优雅。
我想知道是否
int a = pow(2, static_cast<int>(log2(N)));
总能给出正确的结果。例如,如果 N==8,log2(N) 是否可能给出类似 2.9999999999999 的结果,最终结果变成 4 而不是 8?
不准确的操作数与不准确的结果
I wonder whether similar problems will show up if I use mathematical functions.
实际上,基本操作(包括*
)不存在阻止log2(8)
为3的问题。但它存在于 log2
功能。
您混淆了两个不同的问题:
double a = 1 / 3.0;
int b = a * 3; // b will be 0 here
在上面的例子中,a
不正好是1/3,所以可能a*3
不会产生1.0
。乘积本可以四舍五入到 1.0
,但事实并非如此。但是,如果 a
不知何故恰好是 1/3,则 a
与 3 的乘积将恰好是 1.0
,因为 这就是 IEEE 754 [=74] =] works:基本运算的结果是对相同操作数的相同运算的数学结果最接近的可表示值。当确切的结果可以表示为 floating-point 数字时,该表示就是您得到的结果。
sqrt 和 log2 的准确度
sqrt
是“基本操作”的一部分,因此sqrt(4)
保证在IEEE 754系统中始终无一例外地是 2.0
.
log2
不是基本操作的一部分。 IEEE 754 标准不保证此函数的实现结果最接近数学结果。它可以是更远的另一个可表示的数字。因此,如果不对您使用的 log2
函数进行更多假设,就无法判断 log2(8.0)
可以是什么。
然而,大多数初等函数的合理质量实现(例如 log2
)保证实现的结果在数学结果的 1 ULP 范围内。当数学结果不可表示时,这意味着上面的可表示值或下面的值(但不一定是两者中最接近的一个)。当数学结果可以精确表示时(比如3.0
),那么这个表示仍然是唯一保证返回的
所以关于 log2(8)
,答案是“如果你有合理质量的 log2
实施,你可以预期结果是 3.0`”。
不幸的是,并非每个基本函数的每个实现都是高质量的实现。请参阅此 blog post,这是由于广泛使用的 pow
实现在计算 pow(10.0, 2.0)
时不准确超过 1 个 ULP,因此返回 99.0
而不是 100.0
.
四舍五入到最接近的整数
接下来,在每种情况下,您都通过隐式转换将 floating-point 分配给 int
。此转换在 C++ 标准中定义为 截断 floating-point 值(即向零舍入)。如果您希望 floating-point 计算的结果是一个整数,您可以在分配之前将 floating-point 值四舍五入为 最近的 整数。在误差不累积到大于 1/2 的值的所有情况下,它将有助于获得所需的答案:
int b = std::nearbyint(log2(8.0));
以对标题问题的直接回答作为结论:是的,在使用 floating-point 函数生成积分 end-result 时,您应该担心准确性。即使有基本操作的保证,这些功能也没有。
不幸的是,在 C++ 中从浮点数到整数的默认转换真的很疯狂,因为它通过删除小数部分来工作。
这很糟糕,原因有二:
一个浮点数确实非常接近正整数,但低于它会被转换为之前的整数(例如3-1×10-10 = 2.9999999999 将转换为 2)
一个浮点数确实非常接近负整数,但在它上面会被转换为下一个整数(例如-3+1×10-10 = -2.9999999999 将转换为-2)
(1) 和 (2) 的组合也意味着使用 int(x + 0.5)
将无法正常工作,因为它会将负数向上舍入。
有一个合理的round
功能,但不幸的是returns另一个浮点数,因此你需要写int(round(x))
。
使用 C99 或 C++11 时,您可以使用 lround(x)
。
请注意,唯一可以用浮点数正确表示的数字是商,其中分母是 2 的整数次方。
例如 1/65536 = 0.0000152587890625
可以正确表示,但即使只是 0.1
也不可能正确表示,因此涉及该数量的任何计算都将被近似计算。
当然,当使用 0.1 近似值时,偶尔可以抵消而留下正确的结果,但即使只是将 0.1 乘以 10,在使用 IEEE754 双精度浮点数进行计算时,也不会得到 1.0 作为结果。
更糟糕的是,允许编译器对中间结果使用更高的精度。这意味着如果编译器决定使用更高的精度并在最后四舍五入到最接近的双精度,则将 10 乘以 0.1 可能 在转换为整数时返回 1。
这是 "worse" 因为尽管精度更高,但结果取决于编译器和编译器选项,这使得对计算的推理变得更加困难,并使确切的结果在不同系统之间不可移植(即使它们使用相同的系统)精度和格式)。
大多数编译器都有特殊选项来避免这个特定问题。
例如,由于浮点数的精度,下面的代码将产生不希望的结果。
double a = 1 / 3.0;
int b = a * 3; // b will be 0 here
不知道用数学函数会不会出现类似的问题。例如
int a = sqrt(4); // Do I have guarantee that I will always get 2 here?
int b = log2(8); // Do I have guarantee that I will always get 3 here?
如果没有,如何解决这个问题?
编辑:
其实我在为一个算法任务编程的时候遇到过这个问题。我想得到
the largest integer which is power of 2 and is less than or equal to integer N
所以round函数不能解决我的问题。我知道我可以通过一个循环来解决这个问题,但似乎不是很优雅。
我想知道是否
int a = pow(2, static_cast<int>(log2(N)));
总能给出正确的结果。例如,如果 N==8,log2(N) 是否可能给出类似 2.9999999999999 的结果,最终结果变成 4 而不是 8?
不准确的操作数与不准确的结果
I wonder whether similar problems will show up if I use mathematical functions.
实际上,基本操作(包括*
)不存在阻止log2(8)
为3的问题。但它存在于 log2
功能。
您混淆了两个不同的问题:
double a = 1 / 3.0;
int b = a * 3; // b will be 0 here
在上面的例子中,a
不正好是1/3,所以可能a*3
不会产生1.0
。乘积本可以四舍五入到 1.0
,但事实并非如此。但是,如果 a
不知何故恰好是 1/3,则 a
与 3 的乘积将恰好是 1.0
,因为 这就是 IEEE 754 [=74] =] works:基本运算的结果是对相同操作数的相同运算的数学结果最接近的可表示值。当确切的结果可以表示为 floating-point 数字时,该表示就是您得到的结果。
sqrt 和 log2 的准确度
sqrt
是“基本操作”的一部分,因此sqrt(4)
保证在IEEE 754系统中始终无一例外地是 2.0
.
log2
不是基本操作的一部分。 IEEE 754 标准不保证此函数的实现结果最接近数学结果。它可以是更远的另一个可表示的数字。因此,如果不对您使用的 log2
函数进行更多假设,就无法判断 log2(8.0)
可以是什么。
然而,大多数初等函数的合理质量实现(例如 log2
)保证实现的结果在数学结果的 1 ULP 范围内。当数学结果不可表示时,这意味着上面的可表示值或下面的值(但不一定是两者中最接近的一个)。当数学结果可以精确表示时(比如3.0
),那么这个表示仍然是唯一保证返回的
所以关于 log2(8)
,答案是“如果你有合理质量的 log2
实施,你可以预期结果是 3.0`”。
不幸的是,并非每个基本函数的每个实现都是高质量的实现。请参阅此 blog post,这是由于广泛使用的 pow
实现在计算 pow(10.0, 2.0)
时不准确超过 1 个 ULP,因此返回 99.0
而不是 100.0
.
四舍五入到最接近的整数
接下来,在每种情况下,您都通过隐式转换将 floating-point 分配给 int
。此转换在 C++ 标准中定义为 截断 floating-point 值(即向零舍入)。如果您希望 floating-point 计算的结果是一个整数,您可以在分配之前将 floating-point 值四舍五入为 最近的 整数。在误差不累积到大于 1/2 的值的所有情况下,它将有助于获得所需的答案:
int b = std::nearbyint(log2(8.0));
以对标题问题的直接回答作为结论:是的,在使用 floating-point 函数生成积分 end-result 时,您应该担心准确性。即使有基本操作的保证,这些功能也没有。
不幸的是,在 C++ 中从浮点数到整数的默认转换真的很疯狂,因为它通过删除小数部分来工作。
这很糟糕,原因有二:
一个浮点数确实非常接近正整数,但低于它会被转换为之前的整数(例如3-1×10-10 = 2.9999999999 将转换为 2)
一个浮点数确实非常接近负整数,但在它上面会被转换为下一个整数(例如-3+1×10-10 = -2.9999999999 将转换为-2)
(1) 和 (2) 的组合也意味着使用 int(x + 0.5)
将无法正常工作,因为它会将负数向上舍入。
有一个合理的round
功能,但不幸的是returns另一个浮点数,因此你需要写int(round(x))
。
使用 C99 或 C++11 时,您可以使用 lround(x)
。
请注意,唯一可以用浮点数正确表示的数字是商,其中分母是 2 的整数次方。
例如 1/65536 = 0.0000152587890625
可以正确表示,但即使只是 0.1
也不可能正确表示,因此涉及该数量的任何计算都将被近似计算。
当然,当使用 0.1 近似值时,偶尔可以抵消而留下正确的结果,但即使只是将 0.1 乘以 10,在使用 IEEE754 双精度浮点数进行计算时,也不会得到 1.0 作为结果。
更糟糕的是,允许编译器对中间结果使用更高的精度。这意味着如果编译器决定使用更高的精度并在最后四舍五入到最接近的双精度,则将 10 乘以 0.1 可能 在转换为整数时返回 1。
这是 "worse" 因为尽管精度更高,但结果取决于编译器和编译器选项,这使得对计算的推理变得更加困难,并使确切的结果在不同系统之间不可移植(即使它们使用相同的系统)精度和格式)。
大多数编译器都有特殊选项来避免这个特定问题。