3

据我所知,C 对逻辑表达式使用惰性计算,例如在表达式中

f(x) && g(x)

g(x)f(x)如果为假,则不会被调用。

但是算术表达式呢?

f(x)*g(x)

如果为零,是否g(x)会被调用?f(x)

4

5 回答 5

3

是的,算术运算是急切的,而不是懒惰的。

所以在f(x)*g(x)两者 中总是调用 and (迂腐的编译器正在将其转换为某种fA -normal 形式,如果不可观察,甚至可以避免一些调用),但不能保证调用before 或 after的顺序。当为 0时,评估or是未定义的行为。gfgx*1/xy*1/xx

这在 Haskell AFAIU 中是不正确的

于 2013-10-06T07:39:25.057 回答
2

是的,g(x)仍然会被调用。

通常,仅仅因为左侧为零而有条件地忽略右侧的评估会很慢。也许不是在右侧是昂贵的函数调用的情况下,但编译器不会假定知道这一点。

于 2013-10-06T07:39:39.637 回答
2

它被称为“短路”而不是懒惰。而且,至少就标准而言,是的——即,它没有为*.

如果可以确定g()没有副作用,编译器可能能够进行短路评估,但只能在 as-if 规则下(即,它只能通过发现没有外部可观察到的差异才能这样做,而不是因为标准给予它任何直接许可这样做)。

于 2013-10-06T07:42:55.960 回答
1

在逻辑运算符&&||评估顺序的情况下,必然会从左到右发生并且发生短路&&在(逻辑与)、||(逻辑或)(作为短路评估的一部分)的左右操作数的评估之间存在一个序列点。例如,在表达式*p++ != 0 && *q++ != 0中,子表达式的所有副作用*p++ != 0都在任何尝试访问之前完成q,但在算术运算符的情况下则不然。

于 2013-10-06T07:38:45.393 回答
1

虽然这种优化是可能的,但有一些反对意见:

  • 您为优化付出的代价可能比从中获得的回报多:与逻辑运算符不同,优化可能仅在所有算术运算符的一小部分情况下有益,但同时需要对 0 进行额外检查每一次操作。

    因为布尔真值只有两个可能的值,所以理论上有50% 的机会 (1 ÷ 2) 短路布尔表达式,不需要计算第二个操作数。(这假设均匀分布,这可能不现实,但请耐心等待。)也就是说,在相对较大比例的情况下,您可能会从优化中获利。

    将此与整数进行对比,其中 0 只是数百万个可能值中的一个。第一个操作数为 0 的概率要低得多:1 ÷ 2 32(对于 32 位整数,再次假设均匀分布)。即使 0 实际上比这更可能出现(即具有非均匀分布),我们仍然不太可能处理与真值相同的数量级。

    浮点数学进一步加剧了这个问题。在这里,您需要处理舍入错误和非规范化的可能性。某些计算恰好产生 0 的概率可能甚至低于整数。

    因此,优化相对不太可能导致剩余的操作数不被评估。但这导致 100% 的时间增加对零的检查!

  • &&如果您希望评估规则保持合理一致,则必须重新定义and的短路评估顺序||除法有一个重要的极端情况,即除以 0:即使第一个操作数为 0,商也不一定为 0。除以 0 将被视为错误(可能在 IEEE 浮点数学中除外);因此,您始终必须评估第二个操作数以确定计算是否有效。

    有一个替代优化/:除以 1。在这种情况下,您根本不必除,而只需返回第一个操作数。/因此,从第二个操作数(除数)开始会更好地优化。

    现在,除非您想要&&, ||, 和*从第一个操作数开始计算,而是/从第二个操作数开始(这可能看起来不直观),否则您通常必须重新定义短路行为,以便总是先计算第二个操作数,这将是对现状的背离。

    这本身不是问题,但如果因此更改 C 语言,可能会破坏许多现有代码。

  • 优化可能会破坏与可以重载运算符的 C++ 代码的“兼容性” 。优化是否仍适用于重载*/运算符?或者这些运算符是否必须有两种不同的形式,一种是短路的,一种是急切的评估?

    同样,这不是短路算术运算符固有的缺陷,而是如果将这种短路作为一项重大更改引入 C(和 C++)语言中会出现的问题。

于 2013-10-06T07:51:22.333 回答