我在使用小精度浮点数执行算术运算时检测到了不寻常的计算时间。以下简单代码表现出这种行为:
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
const int MAX_ITER = 100000000;
int main(int argc, char *argv[]){
double x = 1.0, y;
int i;
clock_t t1, t2;
scanf("%lf", &y);
t1 = clock();
for (i = 0; i < MAX_ITER; i++)
x *= y;
t2 = clock();
printf("x = %lf\n", x);
printf("Time: %.5lfsegs\n", ((double) (t2 - t1)) / CLOCKS_PER_SEC);
return 0;
}
以下是该程序的两个不同运行:
y = 0.5
x = 0.000000
时间:1.32000秒
y = 0.9
x = 0.000000
时间:19.99000秒
我正在使用具有以下规格的笔记本电脑来测试代码:
有人可以详细解释为什么会出现这种情况吗?我知道,当y = 0.9时,x值比y = 0.5更慢,所以我怀疑问题与此直接相关。
答案 0 :(得分:10)
非常规(或更确切地说是次正常的)数字通常会受到影响。根据您的第二个示例,慢慢收敛到0
将产生更多次正规。阅读更多here和here。要获得更严肃的阅读,请查看常被引用的(并且非常密集)What Every Computer Scientist Should Know About Floating-Point Arithmetic。
来自第二个来源:
在IEEE-754下,浮点数用二进制表示为:
Number = signbit \* mantissa \* 2exponent
有可能有多种方式来表示相同的数字, 以十进制为例,数字0.1可以表示为 1 * 10-1或0.1 * 100甚至0.01 * 10.标准规定了 数字始终与第一位一起存储。小数点 对应于1 * 10-1的例子。
现在假设可以表示的最低指数是-100。 因此,可以用正常形式表示的最小数字是 1 * 10-100。但是,如果我们放松了前导位的约束 一个,那么我们实际上可以代表相同的较小数字 空间。以十进制为例,我们可以表示0.1 * 10-100。这个 被称为次正规数。具有次正规数的目的 是为了平滑最小正常数和零之间的差距。
认识到表示次正规数是非常重要的 精度低于正常数字。事实上,他们正在交易 尺寸越小,精度越低。因此使用的计算 次正规数不会具有相同的精度 计算正常数字。这样的应用程序 对正常数字的重要计算可能是值得的 调查以确定是否重新缩放(即将数字乘以 一些比例因子)会产生更少的次正规,并且更准确 结果
我正在考虑自己解释,但上面的解释写得非常简洁。
答案 1 :(得分:3)
你得到一个可衡量的差异不是因为0.9^n
在数学上比0.5^n
收敛到0慢,而是因为在IEEE-754浮点实现中,它根本不会收敛到0。 / p>
IEEE-754表示中的最小正double
为2 -1074 ,最小正正常为2 -1021 ,因此使用{{1} },循环遇到53个次正规数。一旦达到最小的正次正规,下一个乘积将是2 -1075 ,但是由于舍入到最后一位零的默认舍入模式被四舍五入为0.(IEEE -754浮点数的表示和默认的round-ties-to-last-bit-zero舍入模式在标准的消费者硬件上几乎无处不在,即使标准没有完全实现。)然后,你有一个乘法y = 0.5
,它是一个普通的浮点乘法(即使0*y
是一个次正规数,它也很快)。
使用y
,一旦达到(正)次正常范围的下限,0.5 < y < 1
的结果再次向x*y
的值舍入({{1}迭代的固定点是5 * 2 -1074 )。由于在几千次迭代(x
)之后达到了这个值,所以基本上将一个次正规数乘以整数循环的非零数。在许多处理器上,这样的乘法不能直接处理,必须在微码中处理,这要慢得多。
如果速度比IEEE-754语义更重要(或者由于其他原因而不合适),许多编译器提供禁用该行为的选项,并在硬件支持时将次正常数字刷新为0。我在gcc的手册页中找不到明确的选项,但是y = 0.9
在这里做了诀窍。