使用IEEE浮点数进行优化 - 有保证的数学恒等式?

4

我在使用IEEE浮点数规则时遇到了一些问题,这似乎阻碍了编译器进行明显的优化。例如,

char foo(float x) {
    if (x == x) 
       return 1;
    else 
       return 0; 
}

无法仅返回1进行优化,因为NaN == NaN为false。好吧,我想这样说。

但是,我希望编译器能够帮助我修复一些东西。是否有数学恒等式适用于所有浮点数?例如,如果意味着编译器可以假定它始终成立,我愿意写!(x - x)(虽然这也不是真的)。

我在网上看到了一些关于这种恒等式的参考资料,例如这里,但我没有找到任何组织信息,包括IEEE 754标准的简短扫描。

如果我可以让优化器假设isnormal(x)而不生成其他代码,那么也可以。

显然,我实际上不会在我的源代码中写(x == x),但我有一个设计用于内联的函数。该函数可能被声明为foo(float x, float y),但通常x为0,或y为0,或x和y都为z,等等。浮点数表示屏幕上的几何坐标。这些都是我手动优化愚蠢的东西时从未区分0和(x-x)的情况。因此,在内联我的函数后,我真的不在乎编译器在做什么时IEEE规则,我宁愿让编译器忽略它们。由于我们基本上在进行屏幕绘制,因此舍入差异也不是非常重要。

我认为-ffast-math对我来说不是一种选择,因为该函数出现在头文件中,使用该函数的.c文件不适合使用-ffast-math。

5个回答

2
您可能会有用的另一个参考资料是由Yossarian King在《游戏编程宝典》第二卷中写的一篇非常优秀的浮点数优化文章。您可以在这里阅读该文章。它详细讨论了IEEE格式,考虑了实现和架构,并提供了许多优化技巧。

谢谢,我会看一下。不过我还对这个身份问题很感兴趣。 - Ken

2
我认为,你会一直努力让计算机浮点数算术表现得像数学实数算术,并建议你不要为任何原因这样做。我认为你试图比较两个fp数字的相等性是在犯类型错误。由于fp数字在绝大多数情况下是近似值,所以你应该接受这一点,并将近似相等性用作测试。
计算机整数存在于用于数字值的相等性测试中。
好吧,这就是我想的,如果你愿意,可以和机器(实际上是所有机器)进行战斗。
现在,回答你问题的一些部分:
-- 对于你从实数算术中熟悉的每个数学标识,都有在浮点数域中的反例,无论是IEEE还是其他;
--“聪明”的编程几乎总是使编译器优化代码更加困难,而不是直接的编程;
-- 似乎你正在进行一些图形编程:最终,你概念空间中的点的坐标将映射到屏幕上的像素;像素始终具有整数坐标;从概念空间到屏幕空间的转换定义了您的近似相等功能。
问候,
马克

我认为你没有理解我的意思。我说的是除了NaN和Inf这些情况之外,一切都是真正相等的情况。这是一种通用编程方式。我想用一个过于通用但易读的函数来替换特定的转换代码,并让编译器将其优化为与我编写特定版本时相同的代码。这更类似于编译器允许假设有符号整数算术不会溢出进行优化。基本上,你给我讲的是初学者的浮点数知识,而这并不适用。 - Ken
从我个人的观点来看,问题在于IEEE浮点数对编译器的限制非常严格。这真的不是C通常所做的权衡。它在源代码中保留了实际上并不存在的“细节”,因为如果我手动实例化了我的通用模板,我会简单地删除像(x - x)*复杂表达式这样的东西。我可以理解数值计算需要它,并且它是一个很好的默认值,但是如果连一个精明的浮点数用户都无法表达这样的转换是合法的,那就太可惜了。Fortran允许这样做。 - Ken
@Ken:根本问题在于IEEE试图使用一组比较/关系运算符,而不是建议语言应该提供一个“数据处理”集和一个“数学”集。DP关系运算符集应该定义等价关系和传递排名[可能将NaN排在负无穷之下],而数学运算符则会特别处理NaN。几十年后仍然缺乏方便的等价关系测试,这让人难以置信。 - supercat

2
如果您可以假设此模块中使用的浮点数不是Inf/NaN,您可以使用-ffinite-math-only(在GCC中)进行编译。这可能会“改善”像您发布的示例代码生成。

嘿,史蒂芬!不,我可能也做不到。目前我的最佳线索是使用clang,然后加入像'if (isnan(f)) __builtin_unreachable();'这样的内容。这也不起作用,但它似乎应该可以,所以我可以为此提交错误报告。 - Ken
看起来你真正想要的是一个断言作为优化提示,或者一个 #pragma finite_math,这可能是可行的。 - Stephen Canon
我认为这就是builtin_unreachable的作用。它告诉优化器某些代码路径不可能发生。 - Ken
是的,这似乎是一个合理的(如果不是特别美丽的)方法。请注意,isnan是一个宏(在math.h中定义),因此我并不惊讶优化器无法使用builtin_unreachable来推断它。我建议尝试if (x != x) builtin_unreachable(); - Stephen Canon
那个版本也没有运行成功。rdar://problem/7502262 - Ken

1

你可以进行按位比较。虽然有些值是等效但按位不同的,可能会出现问题,但它将捕获所有你提到的真正相等的情况。我不确定编译器是否能够识别你所做的事情并在内联时将其删除(我相信这就是你想要的),但这很容易检查。


不错的观点,尽管这会在编译器无法证明相等性的情况下保留比较和分支。这与简化数学有些不同。我对现在的工作感到满意:在要内联的函数顶部断言isfinite(f),然后提交编译器错误报告,让编译器现在能够优化掉这些数学计算。 - Ken

0

当你尝试了显而易见的方法并对其进行了分析,或者检查了生成的汇编代码时,会发生什么情况?

如果函数与调用点已知的值内联,则优化器可以使用此信息。例如:foo(0, y)

您可能会惊讶于您不必要做的工作,但至少分析或查看编译器实际执行代码的方式将为您提供更多信息,并帮助您确定下一步该怎么做。

话虽如此,如果您知道某些优化器无法自行解决的问题,您可以编写多个版本的函数,并指定要调用的函数。这有点麻烦,但至少对于内联函数,它们将在一个头文件中一起指定。这比下一步要容易得多,即使用内联汇编来精确地执行您想要的操作。


我正在查看生成的代码。在一个简化的例子中,我正在查看一个单一函数调用,手写版本需要16条指令,而使用内联函数则需要56条指令。我想在热代码中使用该函数,并且通常会被多次调用。编写通用函数并将其内联的目的是使代码更易于阅读和维护。 - Ken

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接