零附近的 C++ 浮点比较与 Gtest 失败
C++ Float Comparison Around Zero Fails With Gtest
我们为我的一个单元测试苦苦挣扎了很长一段时间。在调查过程中我们找到了根本原因,这似乎是浮点数的比较(参见以下代码片段,我简化了计算但仍然失败)。
TEST_F( MyFloatTest, thisOneDoesFail)
{
const float toCompare = 0.2f - 1.0f + 0.9f;
EXPECT_FLOAT_EQ( toCompare, 0.1f );
}
结果是:
Actual: 0.1
Expected: toCompare
Which is: 0.099999964
虽然有一定的数值数学背景,但我们仍然无法弄清楚为什么这个测试失败了,而使用 std::numerical_limits::epsilon 的自定义浮点数比较却通过了。
所以在某个时候我们开始认为,GTest 是错误的,我们对其进行了调试。它使用奇怪的表达方式,我们没有完全抓住。
更奇怪的是:下面的测试通过了,即使我只是加了一个 1:
TEST_F( MyFloatTest, thisOnePasses)
{
const float toCompare = 1.2f - 1.0f + 0.9f;
EXPECT_FLOAT_EQ( toCompare, 1.1f );
}
我们认为在包含负浮点值时可能会出现一些问题,但下一个测试也通过了:
TEST_F( MyFloatTest, thisOnePassesAlso)
{
const float toCompare = 0.2f - 1.0f + 1.9f;
EXPECT_FLOAT_EQ( toCompare, 1.1f );
}
所以对我们来说,Gtest 的 EXPECT_FLOAT_EQ 宏似乎确实有一个大约为零的问题。
有谁知道这种行为?你在你的环境中见过类似的东西吗? (顺便说一句:我们使用 MSVC2015)是否由于 GTest 中提到的 4 ULP 精度而意外失败? (我们也不完全清楚)。
问题在于,具有较小值和较大中间值的浮点和往往会产生较大的相对误差。您通过编写
来减少错误
const float toCompare = 0.2f - (1.0f - 0.9f);
在您的原始代码中,最大的中间值是 0.2 - 1.0 = -0.8,比最终结果大八倍。修改后的代码,最大的中间值为0.1,等于最终结果。而且,如果您检查通过的示例测试,则在每种情况下,您都没有比最终结果大的中间结果。
问题不在于 EXPECT_FLOAT_EQ 宏,而在于计算。
Does it just fail by accident due to the 4 ULP precision mentioned in GTest?
在我看来是这样。
尝试以下(非常粗糙,不可移植!)测试代码:
float toCompare = 0.2f - 1.0f + 0.9f;
int i = *reinterpret_cast<int*>(&toCompare);
std::cout << i << '\n';
float expected = 0.1f;
i = *reinterpret_cast<int*>(&expected);
std::cout << i << '\n';
在我的系统上输出是:
1036831944
1036831949
尾数恰好相隔 5 个 ULP。 4个ULP比较,计算误差不够
0.2f - 1.0f
很好,在我的系统上根本没有准确度错误。你剩下的是 -0.8f + 0.9f
。这是错误的来源(在我的系统上)。我不是专家,无法告诉您为什么此计算有 5 ULP 精度误差。
如果预计会有一定程度的错误,请使用EXPECT_NEAR
。
在我看来,问题在于您假设 0.2f - 1.0f + 0.9f 等于 0.1f。 None of 0.2 0.9 0.1 可以精确地表示为浮点数(或表示为双精度数或任何其他二进制浮点数表示形式)。
0.2f 和 0.9f 实际上是 0.2 和 0.9 的近似值,没有理由假设您的总和将给出与 0.1f 给出的 0.1 相同的近似值。虽然 0.2f 和 0.9f 中的相对误差大致相同,但总和中的相对误差可能会因为取消而大得多。
如果您对可以精确表示为浮点数的数字尝试同样的操作,例如 0.25f - 1.0f + 0.875f,您会发现它等于 0.125f
我们为我的一个单元测试苦苦挣扎了很长一段时间。在调查过程中我们找到了根本原因,这似乎是浮点数的比较(参见以下代码片段,我简化了计算但仍然失败)。
TEST_F( MyFloatTest, thisOneDoesFail)
{
const float toCompare = 0.2f - 1.0f + 0.9f;
EXPECT_FLOAT_EQ( toCompare, 0.1f );
}
结果是:
Actual: 0.1 Expected: toCompare Which is: 0.099999964
虽然有一定的数值数学背景,但我们仍然无法弄清楚为什么这个测试失败了,而使用 std::numerical_limits::epsilon 的自定义浮点数比较却通过了。 所以在某个时候我们开始认为,GTest 是错误的,我们对其进行了调试。它使用奇怪的表达方式,我们没有完全抓住。 更奇怪的是:下面的测试通过了,即使我只是加了一个 1:
TEST_F( MyFloatTest, thisOnePasses)
{
const float toCompare = 1.2f - 1.0f + 0.9f;
EXPECT_FLOAT_EQ( toCompare, 1.1f );
}
我们认为在包含负浮点值时可能会出现一些问题,但下一个测试也通过了:
TEST_F( MyFloatTest, thisOnePassesAlso)
{
const float toCompare = 0.2f - 1.0f + 1.9f;
EXPECT_FLOAT_EQ( toCompare, 1.1f );
}
所以对我们来说,Gtest 的 EXPECT_FLOAT_EQ 宏似乎确实有一个大约为零的问题。 有谁知道这种行为?你在你的环境中见过类似的东西吗? (顺便说一句:我们使用 MSVC2015)是否由于 GTest 中提到的 4 ULP 精度而意外失败? (我们也不完全清楚)。
问题在于,具有较小值和较大中间值的浮点和往往会产生较大的相对误差。您通过编写
来减少错误const float toCompare = 0.2f - (1.0f - 0.9f);
在您的原始代码中,最大的中间值是 0.2 - 1.0 = -0.8,比最终结果大八倍。修改后的代码,最大的中间值为0.1,等于最终结果。而且,如果您检查通过的示例测试,则在每种情况下,您都没有比最终结果大的中间结果。
问题不在于 EXPECT_FLOAT_EQ 宏,而在于计算。
Does it just fail by accident due to the 4 ULP precision mentioned in GTest?
在我看来是这样。
尝试以下(非常粗糙,不可移植!)测试代码:
float toCompare = 0.2f - 1.0f + 0.9f;
int i = *reinterpret_cast<int*>(&toCompare);
std::cout << i << '\n';
float expected = 0.1f;
i = *reinterpret_cast<int*>(&expected);
std::cout << i << '\n';
在我的系统上输出是:
1036831944
1036831949
尾数恰好相隔 5 个 ULP。 4个ULP比较,计算误差不够
0.2f - 1.0f
很好,在我的系统上根本没有准确度错误。你剩下的是 -0.8f + 0.9f
。这是错误的来源(在我的系统上)。我不是专家,无法告诉您为什么此计算有 5 ULP 精度误差。
如果预计会有一定程度的错误,请使用EXPECT_NEAR
。
在我看来,问题在于您假设 0.2f - 1.0f + 0.9f 等于 0.1f。 None of 0.2 0.9 0.1 可以精确地表示为浮点数(或表示为双精度数或任何其他二进制浮点数表示形式)。
0.2f 和 0.9f 实际上是 0.2 和 0.9 的近似值,没有理由假设您的总和将给出与 0.1f 给出的 0.1 相同的近似值。虽然 0.2f 和 0.9f 中的相对误差大致相同,但总和中的相对误差可能会因为取消而大得多。
如果您对可以精确表示为浮点数的数字尝试同样的操作,例如 0.25f - 1.0f + 0.875f,您会发现它等于 0.125f