只有40亿个浮点-所以全部测试

2020-10-11 22:00:04

几个月前,我看到一篇博客文章吹捧用于实现向量FLOOR、CEIL和ROUND函数的新奇SSE3函数。人们不可避免地自豪地宣称,他们令人印象深刻的表现和正确性令人印象深刻。然而,CEIL函数对于它应该处理的许多数字给出了错误的答案,包括像“1”这样的奇数。

地板和圆形的功能也同样有缺陷。Reddit讨论了这些问题,然后讨论了另外两组向量数学函数。他们两个人都有类似的缺陷。

这些函数中的一些已经生成了修复版本,并且它们得到了很大的改进,但是其中一些仍然有错误。

浮点数学很难,但是测试这些函数很简单,而且速度很快。就这么做。

CEIL、FLOOR和ROUND函数特别容易测试,因为您可以对照假定良好的CRT函数来检查它们。而且,您可以测试每个浮点位模式(全部40亿!)。大约90秒后。实际上这很容易。只需迭代所有40亿(技术上为2^32)位模式,调用您的测试函数,调用您的引用函数,并确保结果匹配。正确比较NaN和零结果需要一些注意,但仍然不算太差。

旁白:浮点数学素有产生不可预测的错误结果的名声。然后,这个名声被用来证明马虎是正当的,然后它又证明了声誉是正当的。事实上,IEEE浮点数学的设计宗旨是,只要可行,就能给出可能的最佳答案(正确地四舍五入),扩展浮点数学的函数应该遵循这种模式,只有在显然正确性代价太高的情况下才会偏离它。

稍后我将展示ExhaustiveTest函数的实现,但现在是函数声明:

使用ExhaustiveTest的典型测试代码如下所示。在本例中,我测试的是开始讨论的原始sse2_mm_ceil_ps2函数,它使用一个包装器在FLOAT和__m128之间进行转换。该函数没有声称可以处理32位整数范围之外的浮点数,因此我将测试范围限制在这些数字:

请注意,此代码使用FLOAT_t类型来获取特定浮点数的整数表示形式。几年前,我用浮点格式在Tricks中描述了Float_t。

_mm_ceil_ps2声称可以处理32位整数范围内的所有数字,它已经忽略了大约38%的浮点数。即使在这个有限的范围内,它也有872,415,233个错误-这比它试图处理的2,650,800,128个浮点数的失败率高出33%。_mm_ceil_ps2对0.0和flt_epsilon*0.25之间的所有数字、低于8,388,608的所有奇数以及其他一些数字都得到了错误的答案。错误被指出后,很快就产生了一个固定的版本。

讨论的另一组矢量数学函数是DirectXMath。DirectXMath的XMVectorCeling的3.03版本声称可以处理所有浮点数。然而,它在许多微小的数字上失败了,在大多数奇数上都失败了。在它试图处理的4,294,967,296个数字(全部是浮点数)中,总共有880,803,839个错误。XMVectorCeling的一个补救之处在于,这些错误已经知道并修复了一段时间,但您需要最新的Windows SDK(VS2013附带)才能获得修复的3.06版本。即使是3.06版本也不能完全修复XMVectorround。

LiraNuna/GLSL-SSE2系列函数是文中提到的最后一组数学函数。LiraNuna ceil函数声称可以处理所有浮点数,但它对864,026,625个数字给出了错误的答案。这比其他的要好,但也好不到哪里去。

我没有详尽地测试FLOOR和ROUND函数,因为它会使本文复杂化,并且不会增加很大的价值。只要说他们也有类似的错误就够了。

几个CEIL函数是通过将输入值加0.5并舍入到最近的值来实现的。这不管用。此技术在几个方面失败:

四舍五入到最接近的偶数是默认的IEEE舍入模式。这意味着5.5舍入为6,6.5也舍入为6。这就是许多ceil函数在奇数上失败的原因。此技术在小于1.0的最大浮点数上也会失败,因为此加0.5将得出1.5,该值舍入为2.0。

对于非常小的数字(小于大约flt_epsilon*0.25),添加0.5恰好等于0.5,然后四舍五入为零。由于大约40%的正浮点数小于flt_epsilon*0.25,这会导致很多错误-超过8.5亿!

DirectXMath的XMVectorCeling的3.03版本使用了此技术的变体。他们没有添加0.5,而是添加了g_XMOneHalfMinusEpsilon。非常反常的是,这个常量的值与它的名称不匹配-它实际上是flt_epsilon的一半减去0.75倍。好奇。使用此常量可以避免1.0f上的错误,但在小数和大于1的奇数上仍会失败。

_mm_ceil_ps2的修复版本附带了一个方便的模板函数,可用于扩展该函数以支持所有浮点数。不幸的是,由于实现错误,它无法处理NAN。这意味着如果您使用NaN调用_mm_safeInt_ps<;new_mm_ceil_ps2>;(),那么您会得到一个正常的数字。只要有可能,NAN应该是“粘性的”,以便帮助追踪产生它们的错误。

问题是包装器函数使用cmpgt创建一个掩码,它可以使用该掩码来保留大浮点数的值-此掩码都是大浮点数的掩码。但是,由于所有与NAN的比较都为假,因此对于NAN,此掩码为零,因此将为它们返回一个垃圾值。如果将比较切换到CMPLE并且切换两个掩码操作(AND和AND NOT),则免费获得NaN处理。有时候,正确并不需要花费任何代价。以下是一个固定版本:

使用此修复和最新版本的_mm_ceil_ps2,可以正确处理所有40亿个浮点数。

传统观点认为,永远不应该将两个浮点数进行相等比较--应该始终使用ε。传统观点是错误的。

我已经非常详细地写过如何使用epsilon比较浮点值,但有时它就是不合适。有时候确实有一个正确的答案,在这种情况下,任何不完美的东西都是草率的。

在指出这些函数中的缺陷之后,_mm_ceil_ps2及其姊妹函数的修复版本很快就产生了,并且这些新版本工作得更好。

我没有测试每个函数,但以下是我测试的函数的最终版本的结果:

穷举测试对于将单个浮点数作为输入的函数非常有效。几年前,我在重写游戏机的所有CRT数学函数时用到了这一点,效果很好。另一方面,如果您有一个接受多个浮点数或双精度数作为输入的函数,那么搜索空间就太大了。在这种情况下,疑似问题区域的测试用例和随机测试的混合应该是可行的。一万亿次测试可以在合理的时间内完成,它应该可以捕获大多数问题。

下面是一个简单的函数,可用于测试所有浮点数的函数。下面链接的示例代码包含一个更健壮的版本,可以跟踪发现的错误数量。

我的测试代码遗漏了一个细微的区别-它没有检测到一种类型的错误。你发现它了吗?

CEIL(-0.5F)的正确结果是-0.0F。应保留符号位。矢量数学函数都无法做到这一点。在大多数情况下,这并不重要,至少对游戏数学来说是这样,但我认为至少承认这个(小)缺陷是很重要的。如果将比较函数置于“模糊”模式(只比较浮点数的表示,而不是浮点数),则每个CEIL函数将从所有介于-0.0和-1.0之间的浮点数中增加10亿次左右的故障。

这篇文章讨论了DirectXMath 3.03版本中的错误,以及如何获得修复版本:

VC++2013运行这些测试的示例代码。只需从main的主体中取消您想要运行的测试的注释即可。

我以前写过关于在所有花车上运行测试的内容。上一次我详尽地测试打印浮点数的往返,花了足够长的时间,我展示了如何轻松地并行化它,然后我验证了它们在VC++和GCC之间往返。这一次,测试运行得如此之快,甚至不值得增加额外的线程。

此条目以浮点和标记浮点、浮点比较的形式发布。为固定链接添加书签。