我的理解是,在C中,

  • float加法和乘法are deterministic;以及
  • 当A没有副作用时,A += B应该表现得像A = A + B.

但是,为什么这个程序中的断言失败了呢?

#include <stdio.h>
#include <assert.h>

void trial(long long i, float *src, float *target, float g) {
  float a = target[i];
  float x = g * src[i];
  printf("%g %g %g\n", a, x, a + x);
  target[i] += g * src[i];
  assert(target[i] == a + x);
}

int main() {
  float target[2] = { 0.0, -4.52271e-06 };
  float src[2] = { -0.000926437, -0.00102722 };

  trial(1, src, target, 0.01235);
  return 0;
}

你自己试试:Compiler Explorer,git repo

在启用了足够SSE指令的x86上,它确实失败了.我用clang -O2 -march=native -Wall -o test test.c进行了编译,结果是:

-4.52271e-06 -1.26862e-05 -1.72089e-05
test: test.c:9: void trial(long long, float *, float *, float): Assertion `target[i] == a + x' failed.
Aborted (core dumped)

GCC的这一断言也失败了.在Cang,它即使有-O0个也失败了!

看一下程序集,我认为编译器为+=发出了一个融合的乘加指令,但对+不是.可能此指令不会将乘法结果舍入到最接近的float值.

Is this legit?也就是说,C标准允许这种行为吗?据我所知,最新的draft standard for C在附件F中规定,支持IEC标准浮点运算的实现必须按照该标准实现float加法和乘法,这是确定性的.我看不出C在哪里允许舍入操作被优化掉.

推荐答案

额外的精度

这是错误的.您链接到的答案声明"浮点"是确定性的,并澄清"相同的浮点操作,在相同的硬件上运行,总是产生相同的结果."然而,C和C++标准并不要求C和C++实现始终使用"相同的浮点运算".所以float算术在这个意义上并不是确定性的.

  • 当A没有副作用时,A += B应该表现得像A = A + B.

是的,A += B的行为与A = A + B类似,只是左值A只被计算一次.然而,C标准并不要求A = A + B具有确定性行为.具体地,A = A + B*C;被允许在两个不同的情况下给出两个不同的结果.

具体来说,C 2018 5.2.4.2.2 10表示:

…具有浮点操作数…的运算符产生的值计算结果的范围和精度可能大于该类型要求的格式.

考虑一下target[i] += g * src[i];个.C实现可以通过执行g乘以src[i] float和乘积与target[i]float加法来实现这一点.然而,由于上面的许可,还允许通过执行gsrc[i]double倍乘以及该乘积与target[i]double相加来实现这一点.或者它可以用有效的无限精确的乘法和无限精确的加法来实现它.

该标准将这一 Select 留给了实现.它为实现提供了一种方法来报告有关其所做 Select 的一些信息;<float.h>中定义的值FLT_EVAL_METHOD是以下值之一:

  • 0,表示所有浮点运算都以其标称类型执行,
  • 1,表示所有的floatdouble运算在double中执行,long doublelong double中执行,
  • 2,表示所有浮点运算都在long double中执行,或者
  • −1,这意味着该实现不断言如何执行浮点运算.

在您的例子中,编译器似乎对target[i] += g * src[i];使用了融合的乘法-加法指令,这相当于使用无限精确的算术执行运算并将结果舍入到float.

请注意,扩展精度不能无限期地携带.5.2.4.2.2 10中更完整的引述如下:

除了赋值和强制转换(它们删除了所有额外的范围和精度)之外,由具有浮点操作数的运算符和要进行常规算术转换的值以及浮点常量生成的值将被计算为其范围和精度可能大于类型要求的格式.

因此,每当执行赋值或强制转换时,必须将该值转换为其标称类型.

因此,对于float x = g * src[i];后跟a + x,编译器不能使用融合乘加运算,因为g * src[i]的乘积在赋值给x时必须转换为float的精度;额外的精度不能传递到a + x.

C++标准具有基本相同的文本.

收缩

C 2018 6.5 8表示:

浮点表达式可以是contracted,即,就好像它是单个运算一样进行求值,从而省略了源代码和表达式求值方法…所隐含的舍入误差

这意味着,即使上面讨论的FLT_EVAL_METHOD是0并且float算术仅以float精度执行,处理器也可以使用融合的乘加指令来执行a = b + c*d,当将其相加到b时,其计算就好像c*d中没有舍入误差一样.6.5 8允许其他形式的压缩,但融合乘法-加法和融合乘法-减法最常见.

C 2018 7.12.2指定了FP_CONTRACT编译指示,因此,如果翻译单元有#include <math.h>#pragma STDC FP_CONTRACT OFF,编译器就不应该使用缩写.(请注意,让编译器遵守此规则和C标准的其他规则可能需要使用某些switch ,例如将-std=c18与GCC或Clang一起使用,以请求比其默认行为更好地符合标准.)

避免收缩和额外精度的另一种方法是对单个浮点运算使用赋值或强制转换,例如:

float t0 = c*d;
a = b + t0;

或者:

a = b + (float) (c*d);

从技术上讲,标准中的措辞仍然允许以额外的精度执行上述操作,然后四舍五入为float.这可能会导致双舍入误差.然而,对于编译器来说,使用double算术计算单个运算,然后使用另一条指令将其舍入为float将是低效的,因此启用了优化的编译器应该只使用float算术来计算这样的表达式.

还要注意,收缩的使用迫使浮点算术的计算具有一种不确定性.在y = a*b + c*d中,编译器可以使用融合的乘法-加法来将其中一个乘积相加,而不进行舍入,但它不能同时对两个乘积执行这一操作.它会 Select 哪一个呢?对于任何特定的表达式,答案可能是确定的,但当a*b + c*d作为子表达式出现在较大的表达式中时,我们通常不能确定编译器将 Select 哪一个与融合的乘法-加法相结合.

缺乏准确性

C 2018 5.2.4.2.2 7表示:

浮点运算(+-*/)以及<math.h><complex.h>中返回浮点结果的库函数的精度是由实现定义的,…

目前还不清楚这段话是否允许任何形式的非决定论.这可能意味着,虽然精度是由实现定义的,但它必须是C实现始终重现的某种特定精度.现代硬件在很大程度上符合IEEE 754标准,这是处理不正常数字的显著例外.因此,这篇文章可能在很大程度上是对两件事的认可:

  • 当编译器在编译时计算浮点表达式时,它可能会使用比运行时更高的精度.
  • 浮点算术的软件实现旨在支持没有浮点指令的硬件上的C浮点,其精度可能低于correctly rounded(IEEE 754标准指定的舍入术语).

在任何情况下,句子都足够模糊,以至于我们不能确保C中的浮点表达式在给定相同输入的情况下总是返回相同的结果.

C++相关问答推荐

从C函数调用asm函数时生成错误的BLX指令(STM32H753上的gcc)

InetPton()函数无效的IP地址

exit(EXIT_FAILURE):Tcl C API类似功能

为什么在Linux(特别是Ubuntu 20.04LTS)上,POSIX共享内存对象在重启后仍然存在,然后突然变成了根用户?

堆栈在作用域阻塞后会被释放吗?

为什么在函数内部分配内存空间时需要添加符号?

为什么sscanf不能正确地从这个字符串格式中提取所有数字?

S将C语言宏定义为自身的目的是什么?(在glibc标题中看到)

从uint8_t*转换为char*可接受

如何按顺序将所有CSV文件数据读入 struct 数组?

I2C外设在单次交易后出现故障

Cairo STM32MP1 cairo_Surface_WRITE_TO_PNG始终返回CAROLIO_STATUS_WRITE_ERROR

我在反转双向链表时遇到问题

C11/C17标准允许编译器清除复合文字内存吗?

合并对 struct 数组进行排序

我可以创建适用于不同endian的 colored颜色 struct 吗?

为什么我的二叉树删除删除整个左部分的树?

*S=0;正在优化中.可能是GCC 13号虫?或者是一些不明确的行为?

在吉陀罗中,_2_1_和CONCAT11是什么意思?

GDB 用内容初始化数组