1

之前的一个 SO question提出了关于哪个成语在执行效率方面更好的问题:

[ (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

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

还有其他意见吗?

4

1 回答 1

5

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

temp := <expression>.
temp > 0

或者

(temp := <expression>) > 0

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

a 的可执行部分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再次压入以恢复堆栈。Apop后面跟着一个push相同的东西。相反,在第二种情况下,没有pushpop发生,temp只是从堆栈中读取。

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

这也解释了为什么这种差异如此难以衡量:指令可以直接翻译成机器代码,CPU 会非常快地执行它们pushpop

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

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

于 2018-07-07T07:03:41.007 回答