运算符≤UB用于浮点比较?

时间:2015-09-11 11:26:24

标签: c floating-point precision

关于这个主题有很多参考(herehere)。但是我仍然无法理解为什么以下不被认为是UB并且由我最喜欢的编译器(插入clang和/或gcc)正确报告并带有一个整洁的警告:

// f1, f2 and epsilon are defined as double
if ( f1 / f2 <= epsilon )

根据C99:TC3,5.2.4.2.2§8:我们有:

  

除了赋值和强制转换(删除所有额外的范围和   精度),具有浮动操作数和值的操作值   受通常的算术转换和浮动常数的影响   被评估为范围和精度可能更大的格式   比所要求的类型。 [...]

使用典型编译f1 / f2将直接从FPU读取。我在这里尝试使用gcc -m32和gcc 5.2。所以f1 / f2是(在这里)80位(这里只是猜测没有确切的规格)浮点寄存器。这里没有类型促销(按标准)。

我还测试了clang 3.5,这个编译器似乎将f1 / f2的结果转换回正常的64位浮点表示(这是一个实现定义的行为但是对于我的问题,我更喜欢默认gcc行为)。

根据我的理解,比较将在我们不知道大小的类型(即。format whose range and precision may be greater)和epsilon之间进行,大小正好是64位。

我真正觉得难以理解的是平等与众所周知的C类型(例如64位double)和whose range and precision may be greater的比较。我会假设在标准的某个地方需要某种促销(例如,标准会强制epsilon被提升为更广泛的浮点类型。)

因此唯一合法的语法应该是:

if ( (double)(f1 / f2) <= epsilon )

double res = f1 / f2;
if ( res <= epsilon )

作为附注,我希望文学只记录operator <,在我的情况下:

if ( f1 / f2 < epsilon )

由于始终可以使用operator <比较不同大小的浮点数。

那么在哪种情况下第一个表达式会有意义?换句话说,标准如何在两个不同大小的浮点表示之间定义某种等式运算符?

编辑:这里的整个混乱,是我认为可以比较两个不同大小的浮动。哪个不可能发生。 (感谢@DevSolar!)。

4 个答案:

答案 0 :(得分:3)

<=所有可能的浮点值定义良好。

但有一个例外:至少有一个参数未初始化的情况。但这更多地与读取未初始化的变量UB有关;不是<=本身

答案 1 :(得分:3)

我认为你是confusing implementation-defined with undefined behavior。 C语言并不强制要求IEEE 754,因此所有浮点运算基本上都是实现定义的。但这与未定义的行为不同。

答案 2 :(得分:2)

经过一段时间的聊天后,很明显错误传达的来源。

标准的引用部分明确允许实现在计算中使用更宽格式来浮动操作数。这包括但不限于,使用long double操作数的double格式。

有问题的标准部分也称之为“类型促销”。它只是指使用的格式

因此,f1 / f2可能会以某种任意内部格式完成,但不会使结果与double相比任何其他类型

因此,当结果(由<=或有问题的==)与epsilon进行比较时,epsilon无促销 (因为除法的结果从未得到不同的类型),但是通过允许f1 / f2以更宽格式发生的相同规则,允许评估epsilon也是那种格式。在这里做正确的事情取决于实施。

FLT_EVAL_METHOD 的值可能分别告诉实体究竟做了什么(如果分别设置为012 ),或者它可能有一个负值,表示“不确定”(-1)或“实现定义”,这意味着“在编译器手册中查找”。

这提供了一个实现“摆动空间”来做浮动操作数的任何有趣的事情,只要至少保留实际类型的范围/精度。 (一些较旧的FPU具有“摇摆不定”的精度,具体取决于所执行的浮动操作的类型。标准的引用部分恰好满足了这一要求。)

在任何情况下都不会导致任何未定义的行为。 实施定义,是的。未定义,不。

答案 3 :(得分:1)

您将获得未定义行为的唯一情况是当一个大的浮点变量降级为一个不能代表内容的较小浮点变量时。在这种情况下,我不太清楚这是如何适用的。

您引用的文字关注的是浮点数是否可以被评估为双打等,正如您在报价中未包含的文字所示:

  

使用评估格式的特点是   实现定义的FLT_EVAL_METHOD值:

     

-1不确定;

     

0仅根据类型的范围和精度评估所有操作和常量;

     

1计算float类型的操作和常量以及double类型的范围和精度,将long double操作和常量计算为long double类型的范围和精度;

     

2评估long double类型的范围和精度的所有操作和常量。

但是,我不相信这个宏会覆盖通常的算术转换的行为。通常的算术转换保证您永远不能比较两个不同大小的浮点变量。所以我不知道你怎么会遇到未定义的行为。你唯一可能遇到的问题就是表现。

理论上,在FLT_EVAL_METHOD == 2的情况下,您的操作数确实可以被评估为类型long double。但请注意,如果编译器允许对较大类型进行此类隐式提升,则会有一个原因。

根据您引用的文字,显式转换将对抗此编译器行为。

在这种情况下,代码if ( (double)(f1 / f2) <= epsilon )是无意义的。当您将f1 / f2的结果投射到double时,计算已经完成并且已在long double上执行。然而,结果&lt; = epsilon的计算将在double上执行,因为你强制执行此操作。

要完全避免long double,您必须将代码编写为:

if ( (double)((double)f1 / (double)f2) <= epsilon )

或提高可读性,最好是:

double div = (double)f1 / (double)f2;
if( (double)div <= (double)epsilon )

但是,如果您知道会有隐式促销,您希望避免提高性能,那么这样的代码才有意义。在实践中,我怀疑你是否会遇到这种情况,因为编译器最有可能比程序员做出这样的决定。