operator ≤ UB 是否用于浮点数比较?

Is operator ≤ UB for floating point comparison?

关于该主题的参考资料很多 (here or here)。但是我仍然不明白为什么以下内容不被视为 UB 并且被我最喜欢的编译器(插入 clang and/or gcc)正确报告并带有一个简洁的警告:

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

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

Except for assignment and cast (which remove all extra range and precision), the values of operations with floating operands and values subject to the usual arithmetic conversions and of floating constants are evaluated to a format whose range and precision may be greater than required by the type. [...]

使用典型的编译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)和大小正好是 64 位的 epsilon 之间进行。

我真正难以理解的是 equality 与众所周知的 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!)。

<= 明确定义了 所有 可能的浮点值。

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

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

你会得到未定义行为的唯一情况是当一个大的浮点变量被降级为一个不能表示内容的较小的变量时。我不太明白这在这种情况下如何适用。

你引用的文字关注浮点数是否可以被评估为双精度等,正如你不幸没有包含在引用中的文字所示:

The use of evaluation formats is characterized by the implementation-defined value of FLT_EVAL_METHOD:

-1 indeterminable;

0 evaluate all operations and constants just to the range and precision of the type;

1 evaluate operations and constants of type float and double to the range and precision of the double type, evaluate long double operations and constants to the range and precision of the long double type;

2 evaluate all operations and constants to the range and precision of the long double type.

但是,我不认为这个宏会覆盖通常算术转换的行为。通常的算术转换保证你永远不能比较两个不同大小的浮点变量。所以我不明白你怎么能在这里 运行 进入未定义的行为。您唯一可能遇到的问题是性能。

理论上,如果 FLT_EVAL_METHOD == 2 那么您的操作数确实可以被评估为类型 long double。但请注意,如果编译器允许对更大类型进行此类隐式提升,那将是有原因的。

根据您引用的文本,显式转换将抵消此编译器行为。

在这种情况下,代码 if ( (double)(f1 / f2) <= epsilon ) 是无稽之谈。当你将 f1 / f2 的结果转换为 double 时,计算已经完成并已在 long double 上执行。然而,结果 <= epsilon 的计算将在 double 上执行,因为你用强制转换强制了它。

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

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

或者为了增加可读性,最好是:

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

但同样,只有当您知道会有隐式提升时,这样的代码才有意义,您希望避免隐式提升以提高性能。在实践中,我怀疑你是否会 运行 遇到那种情况,因为编译器很可能比程序员更有能力做出这样的决定。

聊了一会儿,就清楚了错误传达的来源。

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

有问题的标准部分也称其为"type promotion"。它仅指正在使用的格式

因此,f1 / f2 可以以某种任意内部格式完成,但不会使结果成为除 double.[=26= 之外的任何其他 类型 ]

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

FLT_EVAL_METHOD 的值可能 说明具体实现的具体内容(如果设置为 012),也可能为负值,表示"indeterminate"(-1)或"implementation-defined",表示"look it up in your compiler manual".

这给出了一个实现 "wiggle room" 可以用浮动操作数做任何有趣的事情,只要至少保留实际类型的范围/精度 . (一些较旧的 FPU 具有 "wobbly" 精度,具体取决于所执行的浮动操作的类型。标准的引用部分恰好满足了这一点。)

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