Smalltalk中同一语句中分配和比较的效率

时间:2018-07-07 04:57:00

标签: benchmarking smalltalk pharo visualworks gnu-smalltalk

一个previous 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

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

还有其他意见吗?

1 个答案:

答案 0 :(得分:5)

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

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语言的低级领域的人来说,这将是一个极好的练习。