Smalltalk 中同一语句中赋值和比较的效率

Efficency of assign-and-compare in the same statement in Smalltalk

一个提出了哪个成语在执行效率方面更好的问题:

[ (var := exp) > 0 ] whileTrue: [ ... ]

对比

[ var := exp. 
  var > 0 ] whileTrue: [ ... ]

从直觉上看,第一种形式在执行过程中似乎更有效,因为它节省了获取一个额外的语句(第二种形式)。大多数 Smalltalks 都是这样吗?

尝试两个愚蠢的基准测试:

| var acc |
var := 10000.
[ [ (var := var / 2) < 0  ] whileTrue: [ acc := acc + 1 ] ] bench.

| var acc |
var := 10000.
[ [ var := var / 2. var < 0  ] whileTrue: [ acc := acc + 1 ] ] bench

显示两个版本之间没有重大差异。

还有其他意见吗?

所以问题是:我应该使用什么来获得更好的执行时间?

temp := <expression>.
temp > 0

(temp := <expression>) > 0

在这种情况下,得出结论的最佳方法是在抽象层次上向下一级。换句话说,我们需要更好地了解幕后发生的事情。

CompiledMethod 的可执行部分由其 字节码 表示。当我们保存一个方法时,我们所做的是编译它成为一系列低级指令,以便VM能够执行该方法每次调用它。那么,让我们来看看上面每种情况的字节码。

由于 <expression> 在两种情况下都是相同的,所以让我们大幅减少它以消除噪音。另外,让我们把我们的代码放在一个方法中,这样就有一个 CompiledMethod 可以玩

Object >> m
  | temp |
  temp := 1.
  temp > 0

现在,让我们看看 CompiledMethod 及其超类,寻找一些可以向我们展示 Object >> #m 字节码的消息。选择器应该包含子字字节码,对吧?

...

这里是#symbolicBytecodes!现在让我们计算 (Object >> #m) symbolicBytecodes 得到:

pushConstant: 1
popIntoTemp: 0
pushTemp: 0
pushConstant: 0
send: >
pop
returnSelf

请注意我们的 temp 变量如何在字节码语言中重命名为 Temp: 0

现在和另一个重复并得到:

pushConstant: 1
storeIntoTemp: 0
pushConstant: 0
send: >
pop
returnSelf

区别是

popIntoTemp: 0
pushTemp: 0

对比

storeIntoTemp: 0

这表明在这两种情况下 temp 都是以不同的方式从堆栈中读取的。在第一种情况下,我们的<expression>的结果从执行栈弹出到temp,然后再次压入temp恢复栈。 pop 后跟相同的 push。相反,在第二种情况下,没有 pushpop 发生,只是从堆栈中读取 temp

所以结论是,在第一种情况下,我们将生成两个取消指令 pop,然后是 push

这也解释了为什么差异如此难以衡量:pushpop 指令直接转换为机器代码,而 CPU 执行它们的速度非常快。

但是请注意,没有什么可以阻止编译器自动优化代码并意识到实际上 pop + push 等同于 storeInto。通过这样的优化,两个 Smalltalk 片段将产生完全相同的机器代码。

现在,您应该可以决定自己喜欢哪种形式了。我认为这样的决定应该只考虑您更喜欢的编程风格。考虑到执行时间是无关紧要的,因为差异很小,并且可以通过实施我们刚才讨论的优化轻松地减少到零。顺便说一句,对于那些愿意了解无与伦比的 Smalltalk 语言的低级领域的人来说,这将是一个很好的练习。