[英]Is it better to perform n additions of a floating-point number or one integer multiplication?
考虑以下两种情况:
// Case 1
double val { initial_value };
for (int i { 0 }; i < n; ++i) {
val += step;
foo(val);
}
// Case 2
for (int i { 0 }; i < n; ++i) {
double val = initial_value + i * step;
foo(val);
}
其中n
是步数, initial_value
是类型的一些给定值double
, step
是类型的某些预定值double
和val
在后续呼叫的功能中使用的变量foo
。 哪种情况会产生较少的浮点错误? 我的猜测是第二个,因为只有一个加法和乘法,而第一种情况会导致所有n
加法的浮点表示错误。 我问这个问题是因为我不知道要搜索什么。 对于此类案例,是否存在一些好的参考资料?
实际上,变量val
将用于这两种情况的循环中。 我没有包含任何示例,因为我只对浮点错误感兴趣。
选项 2 的误差明显较低。
多少? 好吧,为了简单起见,让我们首先假设initial_value
为0
。 您有 53 个有效位,您看到舍入错误的速度取决于我们在加法过程中设法将它们移出远端的速度。
因此,让我们选择step
,使有效位理想地全为 1: 0.999999999999999999999999
。
现在舍入误差是每次加法过程中距离step
远端的log2(val/step)
位。 在第一次迭代期间并不多,但错误很快就会变得明显。
选择一个巨大的initial_value
并且错误会变得非常极端。 对于initial_value >= pow(2, 53) * step
,您的第一个循环甚至在迭代之间根本无法更改val
。
您的第二个循环仍然可以正确处理。
关键是在许多情况下,人们可能需要在指定的起点和终点之间均匀间隔的一系列值。 使用第二种方法将产生的值在起点和接近所需值的结束值之间尽可能均匀地间隔,但可能不完全匹配。
还有一个由Bathsheba 写的:
两者都有缺陷。 您应该计算开始和结束,然后将每个值计算为这些值的函数。 第二种方法的问题是你一步一步地乘以错误。 前者累积错误。
我建议几个选择。
从 C++20 开始,标准库提供std::lerp ,其中std::lerp(a, b, t)
返回“参数 t 的 a 和 b 之间的线性插值(或外推,当 t 超出范围时 [ 0,1])”。
像value = (a * (n - i) + b * i) / n;
这样的公式value = (a * (n - i) + b * i) / n;
可能会导致更均匀的中间值1分布。
(1)在这里,我尝试针对不同的极端情况和样本点数量测试所有这些方法。 该程序比较每个算法在以相反方向(首先从左到右,然后从右到左)应用时生成的值。 它显示了中间点值之间绝对差之和的平均值和方差。
其他指标可能会产生不同的结果。
考虑一个极端情况。 假设initial_value
远大于step
。 很多很多。 由于浮点表示的限制, initial_value + step == initial_value
如此之大。 但是,我们不希望这种“极端”情况变得过于极端。 给initial_value
一个上限,比如说让它足够小,以便有initial_value + (2*step) != initial_value
。 (有些人可能将这个放置step
称为介于某个 epsilon 和该 epsilon 的一半之间,但我会将术语混淆。)现在运行您的代码。
在第一个循环中, val
每次迭代都将等于initial_value
因为没有执行会更改其值的操作。 相反,如果有足够的迭代,第二个循环最终将具有不同的val
值。 因此,在这种极端情况下,第二个选项,即计算initial_value + i * step
的选项更准确。
我们还应该看看相反的极端。 假设initial_value
相对于step
小到initial_value + step == step
。 在这种情况下, initial_value
也可能为零,问题简化为询问是否有比将i
和step
相乘更准确的方法来计算i*step
step
。 (如果有,我可能想要一个新的编译器。)因此,在这种极端情况下,第二个选项并不比第一个更差。
极端案例分析不是结论性的,但它往往能揭示趋势。 我把计算推到了相反的极端,第二个选项从绝对更好到绝对不差。 我愿意得出结论,第二个选项产生的错误更少。
警告:可能是错误的大小可以忽略不计,不值得编码。 此外,该问题的范围有限,忽略了其他考虑因素(例如step
从何而来;如果是除以n
的结果,可能还有更好的选择)。 尽管如此,在问题提出的狭窄场景中,每次迭代计算initial_value + i*step
看起来像是获得最小数值误差的方法。
包括<cmath>
并使用std::fma(i, step, initial_value)
将始终产生最佳结果,假设i
不是太大以至于将其转换为浮点类型会出现舍入错误。 这是因为fma
被指定为产生一个结果,相当于计算i
• step
+ initial_value
的实数,然后将其四舍五入到最接近的可表示值。 它在乘法之后和加法之前没有内部舍入,因此它产生了可在浮点类型中表示的最佳结果。
在乘法和加法之间,一般优选乘法。 加法可以产生更好的结果。 假设 IEEE-754 双精度二进制,一个例子很容易构造为initial_value = -1./3
, i = 3
和step = 1./3
。 然后在initial_value + step + step + step
, initial_value + step
产生恰好为零(因此没有舍入误差),添加step
没有错误,第二个 add 只是将step
加倍,这也没有错误。 所以加法会产生一个没有错误的最终结果。 相比之下,在initial_value + 3*step
, 3*step
有一个舍入误差,它在加法过程中一直存在。
然而,除了故意构造的例子,乘法通常会产生比加法更好的结果,因为它使用的运算更少,在大多数情况下更少。 通常,重复添加中的舍入误差会像随机游走一样,有时会增加累积误差,有时会减少累积误差。 随机游走有时可以返回原点,但很少这样做。 因此,与具有一次乘法和一次加法的表达式相比,具有许多加法的序列具有更接近原点的累积误差(零误差)是很少见的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.