相同代码中的行为不一致

时间:2017-09-03 22:28:08

标签: c++ gcc codeblocks heisenbug

在运行物理模拟约20分钟后,错误陷阱会跳闸。实现这一点很难调试,我在一个新项目中复制了相关的子程序,并在发生错误时用原始输入数据的硬编码副本调用它。但错误陷阱没有绊倒!经过两天繁琐的工作来隔离子程序的两个实例的行为分歧的确切点,我已经将问题追溯到一个非常简单的函数来计算Cross_Product。

这两个程序中的Cross_Product功能完全相同。我甚至检查了反汇编并确保编译器生成相同的代码。在这两种情况下,该功能也接收相同的输入数据。我甚至已经明确检查了函数内部的舍入模式,它们是相同的。然而,他们返回的结果略有不同。具体地说,LSB对于三个返回的矢量分量中的两个是不同的。甚至调试器本身也确认这三个变量中的这两个变量不等于它们已明确赋值的表达式。 (见下面的截图。)

Debug screenshot

在原始程序中,调试器在监视列表的所有最后三行中显示“true”,而不是仅显示最后一行。

我正在使用Code :: Blocks 13.12和XP上的GCC编译器,以及AMD Athlon 64 CPU。但是,我在一台更现代的Windows 10机器上重新编译并运行了Code :: Blocks 16.01中的测试程序,并使用了Intel Core i5 CPU,结果完全相同。

这是我的最小,完整和可验证的代码,用于重现奇怪的结果,这与我的原始程序和调试器本身不一致(不幸的是,我不能包括原始的物理程序,因为它是巨大的):

extern "C" {
    __declspec(dllimport) int __stdcall IsDebuggerPresent(void);
    __declspec(dllimport) void __stdcall DebugBreak(void);
}

struct POLY_Triplet {
   double XYZ[3];
};

POLY_Triplet Cross_Product(POLY_Triplet Vector1, POLY_Triplet Vector2) {
   POLY_Triplet Result;

   Result.XYZ[0] = Vector1.XYZ[1] * Vector2.XYZ[2] - Vector1.XYZ[2] * Vector2.XYZ[1];
   Result.XYZ[1] = Vector1.XYZ[2] * Vector2.XYZ[0] - Vector1.XYZ[0] * Vector2.XYZ[2];
   Result.XYZ[2] = Vector1.XYZ[0] * Vector2.XYZ[1] - Vector1.XYZ[1] * Vector2.XYZ[0];

   return Result;
}

int main() {
   POLY_Triplet Triplet1;

   POLY_Triplet Collision_Axis_Vector;

   POLY_Triplet Boundary_normal;

   *(long long int *)(&Collision_Axis_Vector.XYZ[0]) = 4594681439063077250;
   *(long long int *)(&Collision_Axis_Vector.XYZ[1]) = 4603161398996347097;
   *(long long int *)(&Collision_Axis_Vector.XYZ[2]) = 4605548671330989714;

   *(long long int *)(&Triplet1.XYZ[0]) = -4626277815076045984;
   *(long long int *)(&Triplet1.XYZ[1]) = -4637257536736295424;
   *(long long int *)(&Triplet1.XYZ[2]) = 4589609575355367200;

   if (IsDebuggerPresent()) {
      DebugBreak();
   }

   Boundary_normal = Cross_Product(Collision_Axis_Vector, Triplet1);

   return 0;
}

为方便起见,以下是观察列表的相关行,如屏幕截图所示:

(Result.XYZ[0] == Vector1.XYZ[1] * Vector2.XYZ[2] - Vector1.XYZ[2] * Vector2.XYZ[1])
(Result.XYZ[1] == Vector1.XYZ[2] * Vector2.XYZ[0] - Vector1.XYZ[0] * Vector2.XYZ[2])
(Result.XYZ[2] == Vector1.XYZ[0] * Vector2.XYZ[1] - Vector1.XYZ[1] * Vector2.XYZ[0])

有人可以解释一下这种行为吗?

3 个答案:

答案 0 :(得分:3)

*(long long int *)(&Collision_Axis_Vector.XYZ[0]) = 4594681439063077250;

并且所有类似的行都会在程序中引入Undefined Behavior,因为它们违反了Strict Aliasing rule

您可以访问long long int

的双精度值

答案 1 :(得分:2)

使用Visual C ++编译样本。 我可以确认输出与您在调试器中看到的略有不同,这是我的:

CAV: 4594681439063077250, 4603161398996347097, 4605548671330989714
T1: -4626277815076045984, -4637257536736295424, 4589609575355367200
CP: 4589838838395290724, -4627337114727508684, 4592984408164162561

我不确定可能导致差异的原因,但这是一个想法。

既然您已经查看了机器代码,那么您正在编译什么,遗留x87或SSE?我认为它是SSE,大多数编译器默认情况下都是这个目标。如果你将-march native传递给gcc,很可能你的CPU有一些FMA指令集(AMD自2011年底以来,英特尔自2013年起)。因此,您的GCC编译器使用了这些_mm_fmadd_pd / _mm_fmsub_pd内在函数,导致您的1位差异。

然而,这都是理论。我的建议是,你应该修改外部代码,而不是试图找出造成这种差异的原因。

因为这样的条件而陷入调试器是个坏主意。

数值差异很小。这是52位尾数中最不重要的位,即误差仅为2 ^( - 52)。

即使你会发现导致这种情况的原因,也可以禁用例如FMA或导致问题的其他一些事情,这样做很脆弱,即你将在项目的整个生命周期中支持这种黑客攻击。您将升级您的编译器,或者编译器将决定以不同方式优化您的代码,甚至您将升级CPU - 您的代码可能会以类似的方式中断。

更好的方法,只是停止比较浮点数以获得完全相等。相反,计算,例如绝对差异,并将其与足够小的常数进行比较。

答案 2 :(得分:2)

我可以确认您获得的有问题的输出可能是由x87精度的变化引起的。精度值存储在x87 FPU控制寄存器中,当更改时,该值在线程的生命周期内持续存在,影响线程上运行的所有x87代码。

显然,您的大型程序的某些其他组件(或您使用的外部库)有时会将尾数长度从53位(默认值)更改为64位(这意味着使用这些80位x87寄存器的完整精度)

修复的最佳方法,将编译器从x87切换到SSE2目标。 SSE总是使用32位或64位浮点数(取决于所使用的指令),它根本没有80位寄存器。甚至你的2003 Athlon 64也已经支持该指令集了。作为副作用,您的代码会变得更快。

更新:如果您不想切换到SSE2,可以将精度重置为您喜欢的任何值。以下是在Visual C ++中如何做到这一点:

#include <float.h>
uint32_t prev;
_controlfp_s( &prev, _PC_53, _MCW_PC ); // or _PC_64 for 80-bit

对于GCC,它是这样的(未经测试的)

#include <fpu_control.h>
#define _FPU_PRECISION ( _FPU_SINGLE | _FPU_DOUBLE | _FPU_EXTENDED )
fpu_control_t prev, curr;
_FPU_GETCW( prev );
curr = ( prev & ~_FPU_PRECISION ) | _FPU_DOUBLE; // or _FPU_EXTENDED for 80 bit
_FPU_SETCW( curr );