为什么语言不使用短路评估?不使用它有什么好处吗?
我知道这可能会导致一些性能问题……这是真的吗?为什么?
相关问题:使用短路评估的好处
不使用短路评估的原因:
因为如果您的函数、属性 Gets 或运算符方法有副作用,它将表现不同并产生不同的结果。这可能与:A) 语言标准,B) 您的语言的先前版本,或 C) 您的语言典型用户的默认假设相冲突。这就是VB不短路的原因。
因为您可能希望编译器可以自由地重新排序和修剪它认为合适的表达式、运算符和子表达式,而不是按照用户键入它们的顺序。这些是 SQL 不短路的原因(或者至少不是大多数使用 SQL 的开发人员认为的那样)。因此,SQL(和其他一些语言)可能会短路,但前提是它决定并且不一定按照您隐式指定的顺序。
我在这里假设您询问的是“自动的、隐式的特定于订单的短路”,这是大多数开发人员对 C、C++、C#、Java 等的期望。VB 和 SQL 都有明确强制特定订单的方法短路。然而,通常当人们问这个问题时,它是一个“做我的意思”的问题。也就是说,它们的意思是“为什么它不做我想要的?”,例如,按照我写它的顺序自动短路。
我能想到的一个好处是,某些操作可能会产生您可能期望发生的副作用。
例子:
if (true || someBooleanFunctionWithSideEffect()) {
...
}
但这通常是不受欢迎的。
Ada 默认不这样做。为了强制进行短路评估,您必须使用and then
oror else
而不是and
or or
。
问题是在某些情况下它实际上会减慢速度。如果第二个条件计算得很快,并且第一个条件对于“and”几乎总是为真,或者对于“or”为假,那么额外的检查分支指令有点浪费。但是,我知道对于具有分支预测器的现代处理器,情况并非如此。另一个问题是编译器可能碰巧知道后半部分更便宜或可能会失败,并且可能想要相应地重新排序检查(如果定义了短路行为,它就无法做到这一点)。
我听说过反对意见,即在第二个测试有副作用的情况下,它可能导致代码出现意外行为。恕我直言,如果您不太了解您的语言,这只是“意外”,但有些人会争论这一点。
如果您对实际的语言设计者对此问题的看法感兴趣,这里是Ada 83(原始语言)Rationale的摘录:
布尔表达式(如 A 和 B)的操作数可以按任意顺序求值。根据术语 B 的复杂性,仅在术语 A 的值为 TRUE 时评估 B 可能更有效(在某些但不是所有机器上)。然而,这是编译器做出的优化决定,假设这种优化总是完成是不正确的。在其他情况下,我们可能想要表达条件的结合,其中每个条件只有在满足前一个条件时才应该被评估(有意义)。这两件事都可以通过短路控制形式来完成......
在 Algol 60 中,只有使用条件表达式才能实现短路评估的效果,因为否则会执行完整的评估。这通常会导致难以遵循的结构......
几种语言没有定义如何评估布尔条件。因此,基于短路评估的程序将不可移植。这清楚地说明了将布尔运算符与短路控制形式分开的必要性。
看看我在On SQL Server boolean operator short-circuit中的示例,它说明了为什么如果不使用布尔短路,SQL 中的某个访问路径会更有效。我的博客示例它显示了如果您在 SQL 中假设短路,实际上依赖布尔短路会破坏您的代码,但是如果您阅读了为什么SQL 首先评估右侧的推理,您会发现这是正确的并且这会大大改善访问路径。
我会说 100 次中有 99 次我更喜欢短路运算符的性能。
但是有两个重要原因我发现我不会使用它们。(顺便说一下,我的示例是在 C 中,其中 && 和 || 是短路的,而 & 和 | 不是。)
1.) 当您想在 if 语句中调用两个或多个函数时,不管第一个返回的值如何。
if (isABC() || isXYZ()) // short-circuiting logical operator
//do stuff;
在这种情况下,仅当 isABC() 返回 false 时才调用 isXYZ()。但无论如何,您可能希望 isXYZ() 被调用。
因此,您可以这样做:
if (isABC() | isXYZ()) // non-short-circuiting bitwise operator
//do stuff;
2.) 当您使用整数执行布尔数学运算时。
myNumber = i && 8; // short-circuiting logical operator
不一定与以下内容相同:
myNumber = i & 8; // non-short-circuiting bitwise operator
在这种情况下,您实际上可以获得不同的结果,因为短路运算符不一定会评估整个表达式。这使得它对布尔数学基本上没用。所以在这种情况下,我会改用非短路(按位)运算符。
就像我暗示的那样,这两种情况对我来说真的很少见。但是您可以看到这两种运算符都有真正的编程原因。幸运的是,今天大多数流行的语言都有。甚至 VB.NET 也有 AndAlso 和 OrElse 短路运算符。如果今天的一种语言没有这两种语言,我会说它已经落伍了,并且确实限制了程序员。
Bill 提到了一个不使用短路但更详细地拼写它的正当理由:高度并行的架构有时会出现分支控制路径的问题。
以 NVIDIA 的 CUDA 架构为例。图形芯片使用 SIMT 架构,这意味着在许多并行线程上执行相同的代码。但是,这仅在所有线程每次都采用相同的条件分支时才有效。如果不同的线程采用不同的代码路径,则评估是序列化的——这意味着并行化的优势将丧失,因为一些线程必须等待而其他线程执行替代代码分支。
短路实际上涉及对代码进行分支,因此短路操作可能对 CUDA 等 SIMT 架构有害。
– 但正如比尔所说,这是硬件方面的考虑。就语言而言,我会以响亮的否定回答您的问题:防止短路没有意义。
如果您希望评估右侧:
if( x < 13 | ++y > 10 )
printf("do something\n");
也许您希望 y 增加无论 x < 13 与否。然而,反对这样做的一个很好的论点是,创建没有副作用的条件通常是更好的编程实践。
作为一个延伸:
如果您希望一种语言超级安全(以令人敬畏为代价),您将删除短路评估。当某些“安全”的事情需要不同的时间才能发生时,可以使用定时攻击来解决它。短路评估导致事情需要不同的时间来执行,因此为攻击戳了个洞。在这种情况下,即使不允许短路评估也有望帮助编写更安全的算法(无论如何都要进行定时攻击)。
Ada编程语言支持不短路的布尔运算符 ( , AND
) ,以允许编译器优化并可能并行化结构,以及在程序员需要时OR
明确请求短路 ( AND THEN
, ) 的运算符。OR ELSE
这种双管齐下的方法的缺点是使语言更加复杂(在同一个“让我们两者都做!”的脉络中做出的 1000 个设计决策将使编程语言总体上更加复杂;-)。
Lustre 语言不使用短路评估。在 if-then-elses 中,then 和 else 分支在每个滴答时都会被评估,并且根据条件的评估,一个被认为是条件的结果。
原因是这种语言和其他同步数据流语言具有简洁的语法来谈论过去。需要计算每个分支,以便在将来的周期中需要时可以使用每个分支的过去。该语言应该是函数式的,所以这无关紧要,但是您可以从中调用 C 函数(并且可能会注意到它们被调用的频率比您想象的要高)。
在 Lustre 中,编写相当于
if (y <> 0) then 100/y else 100
是典型的初学者错误。不能避免除以零,因为即使在 y=0 的循环上也会计算表达式 100/y。
并不是说我认为这就是现在任何语言中正在发生的事情,但是将操作的双方都提供给不同的线程会很有趣。大多数操作数可以预先确定为不相互干扰,因此它们将是切换到不同 CPU 的良好候选者。
这种事情在高度并行的 CPU 上很重要,这些 CPU 倾向于评估多个分支并选择一个。
嘿,这有点牵强,但您问的是“为什么要使用一种语言”……而不是“为什么要使用一种语言”。
因为短路会改变应用程序 IE 的行为:
if(!SomeMethodThatChangesState() || !SomeOtherMethodThatChangesState())
我会说它对可读性问题有效;如果有人以不完全明显的方式利用短路评估,维护人员可能很难查看相同的代码并理解逻辑。
如果没记错的话,erlang 提供了两种结构,标准和/或,然后是 andalso/orelse。这阐明了“是的,我知道这是短路,您也应该这样做”的意图,而在其他点上,意图需要从代码中得出。
例如,假设维护者遇到以下问题:
if(user.inDatabase() || user.insertInDatabase())
user.DoCoolStuff();
需要几秒钟才能识别出意图是“如果用户不在数据库中,则插入他/她/它;如果这样可以做很酷的事情”。
正如其他人指出的那样,这仅在做有副作用的事情时才有意义。
我不知道任何性能问题,但是避免它(或至少过度使用它)的一种可能的论点是它可能会使其他开发人员感到困惑。
关于副作用问题已经有很好的回应,但我没有看到关于问题的性能方面的任何内容。
如果不允许短路评估,性能问题是双方都必须进行评估,即使它不会改变结果。这通常不是问题,但可能在以下两种情况之一下变得相关:
短路评估自动提供表达式的一部分的条件评估。
主要优点是它简化了表达式。
性能可以提高,但您也可以观察到对非常简单的表达式的惩罚。
另一个后果是表达式评估的副作用可能会受到影响。
一般来说,依赖副作用不是一个好的做法,但在某些特定情况下,它可能是首选的解决方案。
VB6不使用短路评估,我不知道新版本是否这样做,但我对此表示怀疑。我相信这只是因为旧版本也没有,而且大多数使用 VB6 的人都不会想到会发生这种情况,这会导致混乱。
这只是让我非常难以摆脱编写意大利面条代码的菜鸟 VB 程序员并继续我成为真正程序员的旅程的原因之一。
许多答案都谈到了副作用。这是一个没有副作用的 Python 示例,其中(在我看来)短路提高了可读性。
for i in range(len(myarray)):
if myarray[i]>5 or (i>0 and myarray[i-1]>5):
print "At index",i,"either arr[i] or arr[i-1] is big"
短路确保我们不会尝试访问 myarray[-1],这会引发异常,因为 Python 数组从 0 开始。当然可以在没有短路的情况下编写代码,例如
for i in range(len(myarray)):
if myarray[i]<=5: continue
if i==0: continue
if myarray[i-1]<=5: continue
print "At index",i,...
但我认为短路版本更具可读性。