展开的 for 循环之间的程序集差异会导致不同的浮点结果

Assembly differences between unrolled for-loops cause differing float results

提问人:Kyle Ponikiewski 提问时间:11/19/2022 最后编辑:Eric PostpischilKyle Ponikiewski 更新时间:11/22/2022 访问量:120

问:

请考虑以下设置:

typedef struct
{
    float d;
} InnerStruct;

typedef struct
{
    InnerStruct **c;
} OuterStruct;


float TestFunc(OuterStruct *b)
{
    float a = 0.0f;
    for (int i = 0; i < 8; i++)
        a += b->c[i]->d;
    return a;
}

TestFunc 中的 for 循环在我正在测试的另一个函数中完全复制了一个循环。 两个循环都由 gcc (4.9.2) 展开,但这样做后产生的汇编略有不同。

我的测试循环的组装:ᅠᅠᅠᅠᅠᅠᅠ原始循环的组装:

lwz       r9,-0x725C(r13)                   lwz       r9,0x4(r3)    
lwz       r8,0x4(r9)                        lwz       r8,0x8(r9)    
lwz       r10,0x0(r9)                       lwz       r10,0x4(r9)   
lwz       r11,0x8(r9)                       lwz       r11,0x0C(r9)  
lwz       r4,0x4(r8)                        lwz       r3,0x4(r8)    
lwz       r10,0x4(r10)                      lwz       r10,0x4(r10)  
lwz       r8,0x4(r11)                       lwz       r0,0x4(r11)   
lwz       r11,0x0C(r9)                      lwz       r11,0x10(r9)  
efsadd    r4,r4,r10                         efsadd    r3,r3,r10
lwz       r10,0x10(r9)                      lwz       r8,0x14(r9)   
lwz       r7,0x4(r11)                       lwz       r10,0x4(r11)  
lwz       r11,0x14(r9)                      lwz       r11,0x18(r9)  
efsadd    r4,r4,r8                          efsadd    r3,r3,r0
lwz       r8,0x4(r10)                       lwz       r0,0x4(r8)    
lwz       r10,0x4(r11)                      lwz       r8,0x0(r9)    
lwz       r11,0x18(r9)                      lwz       r11,0x4(r11)  
efsadd    r4,r4,r7                          efsadd    r3,r3,r10
lwz       r9,0x1C(r9)                       lwz       r10,0x1C(r9)  
lwz       r11,0x4(r11)                      lwz       r9,0x4(r8)    
lwz       r9,0x4(r9)                        efsadd    r3,r3,r0
efsadd    r4,r4,r8                          lwz       r0,0x4(r10)   
efsadd    r4,r4,r10                         efsadd    r3,r3,r11
efsadd    r4,r4,r11                         efsadd    r3,r3,r9
efsadd    r4,r4,r9                          efsadd    r3,r3,r0

问题是这些指令返回的浮点值并不完全相同。而且我无法更改原始循环。我需要以某种方式修改测试循环以返回相同的值。我相信测试的组装相当于一个接一个地添加每个元素。我对汇编不是很熟悉,所以我不确定上述差异是如何转化为 c 的。我知道这是问题所在,因为如果我将打印添加到循环中,它们不会展开,并且结果与预期完全匹配。

C 汇编 编译器优化 浮动精度 PowerPC

评论

1赞 Barmar 11/19/2022
浮点数本质上是不准确的,不同的代码编译方式可能会导致略有不同的不准确之处。
2赞 Eric Postpischil 11/19/2022
编辑问题以提供最小的可重现示例,包括生成汇编代码的源代码和编译命令。
3赞 hyde 11/19/2022
c[i]是一个指针,所以不应该编译。请提供一个最小的可重复示例b->c[i].d
1赞 user3386109 11/19/2022
指令的顺序将影响结果。但是优化器不一定会以相同的顺序对原始循环和测试循环进行添加。通过确保所有数字具有相同的大小并具有精确的浮点表示形式,可以使结果相同。这假设作为测试的一部分,您可以控制原始循环添加的数字。efsadd
2赞 njuffa 11/19/2022
浮点加法不像数学加法那样具有关联性。因此,如果操作顺序不同,结果可能会有所不同。左侧计算的代码段(从左到右):。右边的代码段计算:.尝试指示编译器保持对 IEEE-754 的最严格遵守(例如 适用于英特尔编译器)。可能会有所帮助,但不能保证有帮助。c[0].d + c[1].d + c[2].d + c[3].d + c[4].d + c[5].d + c[6].d + c[7].dc[1].d + c[2].d + c[3].d + c[4].d + c[5].d + c[6].d + c[0].d + c[7].d-fp-model:strict

答:

2赞 Tom V 11/19/2022 #1

我认为这是为了对一个函数与另一个函数进行单元测试。

一般来说,浮点计算在 C 或 C++ 中从来都不是精确的,通常不被认为是合法的。

Java 语言标准需要精确的浮点结果。这样做一直是对 Java 的仇恨的根源,有各种指责认为,使结果可重复通常会使它们不那么准确,有时也会使代码变慢。

如果您正在用 C 或 C++ 进行测试,那么我建议采用这种方法:

尽可能精确地计算结果,同时具有高精度和高精度。在这种情况下,输入数据采用 32 位浮点数,因此在计算预期结果之前,将它们全部转换为 64 位浮点数。

如果输入是双精度的(并且您没有更大的长双精度类型),则按顺序对值进行排序,并将它们从小到大相加。这将导致最小的准确性损失。

获得预期结果后,请测试函数输出是否在某个范围内匹配。

有两种方法可以设置将测试视为通过所需的精度:

一种方法是检查数字的真正物理含义是什么,以及您实际需要的准确性。

另一种方法是只要求结果精确到理想结果的几个最低有效位以内,即:误差小于理想结果乘FLT_EPSILON的几倍。

评论

0赞 Kyle Ponikiewski 11/21/2022
诚然,与预期结果相比,计算将导致不精确。但正如@SteveSummit所说,这两个函数的不精确性不应该是一样的吗?如果它们都在相同的编译开关下运行,我希望它们始终产生相同的结果。是的,与预期结果相比,这些结果都是不精确的,但相互比较,它们应该保持完全相等。我已经在特定范围内测试浮点数 (±0.001),并且由于 2 个循环之间的差异,22% 的测试失败。
0赞 Tom V 11/22/2022
基本上,您的建议在Java中是正确的,但在C或C++中却不正确。原因是在 C/C++ 中,编译器被允许以类型提供的精确的方式传播中间体,并且不必一致地这样做。Java 要求在每次操作后都放弃额外的精度,许多人说这很愚蠢,但至少会提供你所希望的那种确定性。
0赞 Kyle Ponikiewski 11/22/2022
如果这是真的,为什么禁用快速数学可以解决问题?
0赞 Tom V 11/22/2022
这就是不能保证准确无误的本质。它可以在任何时候都是一样的,只是不必如此。不过,我仍然会支持你的答案,因为这是你在这种情况下想要的解决方案。
0赞 Kyle Ponikiewski 11/22/2022
我想我不明白。我已经对 d 的 10,000 个随机浮点值进行了测试。通过快速数学计算,我们得到了这些不同的加法顺序,其中大约 30% 的测试超出了可接受的 ±0.001 范围。但是,在禁用快速数学运算的情况下,每个差值都计算为正好为 0,并且所有 10,000 个测试都通过了。 我错过了什么吗?
1赞 Kyle Ponikiewski 11/22/2022 #2

禁用快速数学似乎可以解决这个问题。感谢@njuffa的建议。我希望能够围绕这种优化设计测试功能,但这似乎是不可能的。至少我知道现在的问题是什么。感谢大家对这个问题的帮助!