考虑以下C ++程序:
#include <iostream>
using std::cout;
using std::endl;
int main () {
float x = 0.0001;
float y = 0;
for (int i=0; i < 10000; i++) {
y += x;
}
cout << y << endl;
return 0;
}
编译并运行此程序,然后回答以下问题: 该程序的实际行为与预期行为有何不同?
为什么没有看到预期的行为?
在确保程序语义保持不变的同时,您会对此程序进行哪些更改以确保预期和实际行为匹配?
以上是我的任务。我知道我想做自己的作业,但我被卡住了。
对于a)部分,我只是说2个数字不同。
对于c)部分,我把浮子变成了双。 (我认为语义保持不变)
对于b)部分我知道这被称为灾难性取消,但教授可能希望看到更多,我不知道还有什么可说的。有人可以帮帮我吗?
感谢您的帮助
答案 0 :(得分:1)
该程序的实际行为与预期行为有何不同? - 该程序的实际行为是将IEEE表示加起来为0.0001 10000次; IEEE表示0.0001!=实际0.0001
为什么没有看到预期的行为? - 我们假设0.0001完全表示为0.0001,实际上它不是因为IEEE浮点不能完全代表0.0001,因为必须代表base2而不是base10中的所有浮点。
在确保程序语义保持不变的同时,您会对此程序进行哪些更改以确保预期和实际行为匹配? - 在这种情况下,将float转换为double将起作用,因为double给出了比float更多的小数精度。 - 替代解决方案是保持浮动而不是进行求和,你指定y = 10000 * x(这会导致更少的错误,当你想要避免舍入和逼近错误时它会更好)
答案 1 :(得分:1)
我认为这个程序的所谓“预期行为”是将.0001添加到初始化为零10,000次的总和,所有算术都是数学的,产生1.实际行为是转换十进制数“.0001”到一个double(可能是IEEE-754 64位二进制浮点),然后将该值转换为float(可能是IEEE-754 32位二进制浮点),然后将该float添加到10,000次,每次都使用浮点运算。因此,在将数字转换为double时,将double转换为float时,以及在每次添加时,实际行为都有潜在的舍入误差。
在这种情况下避免错误的一种方法是使用整数运算。我们可以将float x
设置为1.而不是将int x
设置为1.同样,y
将是一个int,我们将使用所有整数运算,直到我们完成循环。在获得最终总和之后,我们将其转换为浮动。然后我们必须调整我们使用的缩放,允许我们使用整数运算。由于我们添加的是1而不是.0001,因此我们必须将最终结果除以10000.f
以进行调整。 (这种技术不能完全避免所有情况下的错误,还有其他技术可以减少其他情况下的错误。)
没有灾难性的取消,因为没有取消。当两个数字相加或相减以产生较小的结果时会发生取消(因此,当添加两个相反符号的数字时,例如添加+8和-6来获得+2,或减去两个相同符号的数字,例如减去-6从+8到+2)。当结果远小于原始的两个数字时,会发生灾难性的取消。在这种情况下,我们使用的值变得更小,但原始数字中的任何错误通常保持相同的大小,因此错误比我们正在使用的值 relative 大得多。例如,假设我们应该从8.01中减去8并获得.01,但是,由于误差较小,我们有7.99代替8.从8.01减去7.99得到.02。此结果.02,是所需结果的两倍,。01,因此相对错误非常大。