我的理解是,在C中,
float加法和乘法是确定性的;和A += B应该表现得像A = A + BA 没有副作用时一样。那么,为什么这个程序中的断言会失败呢?
#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;
}
Run Code Online (Sandbox Code Playgroud)
在启用了足够的 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)
Run Code Online (Sandbox Code Playgroud)
该断言在 GCC 中也失败了。-O0在 Clang 中,即使使用!也会失败。
查看程序集,我认为编译器发出了一条融合乘加指令,+=但不是 for +。也许该指令不会将乘法结果四舍五入到最接近的float值。
这是合法的吗?也就是说,C 标准允许这种行为吗?据我所知,C 的最新标准草案在附录 F 中规定,支持 IEC 标准浮点运算的实现必须float根据该标准实现加法和乘法,这是确定性的。我不明白 C 在哪里允许舍入操作被优化掉。
Eri*_*hil 14
\n\n\n
\n- \n
 float加法和乘法是确定性的;
这是错误的。您链接到的答案指出 \xe2\x80\x9cfloating-point\xe2\x80\x9d 是确定性的,并阐明 \xe2\x80\x9c 在相同硬件上运行的相同浮点运算总是产生相同的结果。 \xe2\x80\x9d 但是,C 和 C++ 标准并不要求 C 和 C++ 实现始终使用 \xe2\x80\x9c 相同的浮点运算。\xe2\x80\x9d 所以float算术在这个意义上不是确定性的。
\n\n\n
\n- \n
 A += B应该表现得像A = A + BA 没有副作用时一样。
是的,A += B行为类似A = A + B,只是左值A仅计算一次。然而,C 标准不要求A = A + B具有确定性行为。尤其,A = A + B*C;允许在两个不同的情况下给出两个不同的结果。
具体来说,C 2018 5.2.4.2.2 10 说:
\n\n\n\xe2\x80\xa6 由具有浮动操作数 \xe2\x80\xa6 和浮动常量的运算符生成的值被计算为其范围和精度可能大于类型所需的格式。
\n
考虑target[i] += g * src[i];。C 实现可以通过执行 的乘法float以及该乘积与 的加法来实现这一点。然而,由于上述许可,还允许通过执行乘法并将该乘积与 相加来实现这一点。或者它可以通过有效的无限精确乘法和无限精确加法来实现它。gsrc[i]floattarget[i]doublegsrc[i]doubletarget[i]
该标准将其留给实现者来做出选择。它为实现提供了一种方法来报告有关其所做选择的一些信息;FLT_EVAL_METHOD中定义的值<float.h>是以下之一:
float和double操作都在 中执行double,并且long double在 中执行long double,long double, 或中执行在您的情况下,编译器似乎使用了融合乘加指令target[i] += g * src[i];,这相当于使用无限精确的算术执行运算并将结果四舍五入为float。
请注意,扩展精度不能无限期地保留。5.2.4.2.2 10 的更完整引用是:
\n\n\n除了赋值和转换(删除所有额外的范围和精度)之外,具有浮点操作数的运算符生成的值以及经过通常算术转换和浮点常量的值将被评估为其范围和精度可能大于所要求的格式方式。
\n
因此,每当执行赋值或转换时,该值都必须转换为其名义类型。
\n因此,对于float x = g * src[i];后跟 的a + x,编译器无法使用融合乘加,因为在分配给;时g * src[i],必须将乘积转换为精度。额外的精度无法传递到floatxa + x。
C++ 标准具有实质上等效的文本。
\nC 2018 6.5 8 说:
\n\n\n浮动表达式可以被缩写,即像单个操作一样进行计算,从而忽略源代码和表达式计算方法隐含的舍入错误\xe2\x80\xa6
\n
这意味着,即使FLT_EVAL_METHOD上面讨论的 为 0 并且float仅以精度执行算术float,处理器也可以使用融合乘加指令来执行,该指令的计算就好像在与 相加时a = b + c*d没有舍入误差一样。6.5 8 允许其他形式的缩写,但融合乘加和融合乘减是最常见的。c*db
C 2018 7.12.2 指定了一个FP_CONTRACT编译指示,因此,如果翻译单元具有#include <math.h>and #pragma STDC FP_CONTRACT OFF,则编译器不应使用缩写。(请注意,让编译器遵守 C 标准的此规则和其他规则可能需要使用某些开关,例如-std=c18与 GCC 或 Clang 一起使用,以请求比其默认行为更好地符合标准。)
避免收缩和额外精度的另一种方法是使用带有单个浮点运算的赋值或强制转换,例如:
\nfloat t0 = c*d;\na = b + t0;\nRun Code Online (Sandbox Code Playgroud)\n或者:
\na = b + (float) (c*d);\nRun Code Online (Sandbox Code Playgroud)\n从技术上讲,标准中的措辞仍然允许以额外的精度执行上述操作,然后舍入为float。这可能会导致双舍入错误。double但是,编译器使用算术计算单个操作然后使用另一条指令将其舍入为 的效率很低float,因此启用优化的编译器应仅使用float算术来计算此类表达式。
另请注意,缩写的使用强制对浮点算术的评估产生某种非确定性。在 中y = a*b + c*d,编译器可以使用融合乘加来添加其中一个乘积而不进行舍入,但不能对两者都执行此操作。它会选择哪一个呢?对于任何特定表达式来说,答案可能是确定的,但是当a*b + c*d在较大表达式中作为子表达式出现时,我们通常无法确定编译器将选择将哪一个与融合乘加合并。
C 2018 5.2.4.2.2 7 说:
\n\n\n浮点运算 (
\n+、-、*、 )以及返回浮点结果的/库函数的精度是实现定义的,\xe2\x80\xa6<math.h><complex.h>
目前尚不清楚这段文字是否允许任何形式的非决定论。也许这意味着,虽然精度是实现定义的,但它必须是始终由 C 实现再现的某些特定精度。现代硬件很大程度上符合 IEEE 754,这是处理次正规数的一个值得注意的例外。所以这段话可能很大程度上是对两件事的认可:
\n无论如何,这个句子足够模糊,以至于我们无法确定 C 中的浮点表达式在给定相同输入的情况下总是返回相同的结果。
\n|   归档时间:  |  
           
  |  
        
|   查看次数:  |  
           248 次  |  
        
|   最近记录:  |