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
。相反,在第二种情况下,没有 push
或 pop
发生,只是从堆栈中读取 temp
。
所以结论是,在第一种情况下,我们将生成两个取消指令 pop
,然后是 push
。
这也解释了为什么差异如此难以衡量:push
和 pop
指令直接转换为机器代码,而 CPU 执行它们的速度非常快。
但是请注意,没有什么可以阻止编译器自动优化代码并意识到实际上 pop + push
等同于 storeInto
。通过这样的优化,两个 Smalltalk 片段将产生完全相同的机器代码。
现在,您应该可以决定自己喜欢哪种形式了。我认为这样的决定应该只考虑您更喜欢的编程风格。考虑到执行时间是无关紧要的,因为差异很小,并且可以通过实施我们刚才讨论的优化轻松地减少到零。顺便说一句,对于那些愿意了解无与伦比的 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
。相反,在第二种情况下,没有 push
或 pop
发生,只是从堆栈中读取 temp
。
所以结论是,在第一种情况下,我们将生成两个取消指令 pop
,然后是 push
。
这也解释了为什么差异如此难以衡量:push
和 pop
指令直接转换为机器代码,而 CPU 执行它们的速度非常快。
但是请注意,没有什么可以阻止编译器自动优化代码并意识到实际上 pop + push
等同于 storeInto
。通过这样的优化,两个 Smalltalk 片段将产生完全相同的机器代码。
现在,您应该可以决定自己喜欢哪种形式了。我认为这样的决定应该只考虑您更喜欢的编程风格。考虑到执行时间是无关紧要的,因为差异很小,并且可以通过实施我们刚才讨论的优化轻松地减少到零。顺便说一句,对于那些愿意了解无与伦比的 Smalltalk 语言的低级领域的人来说,这将是一个很好的练习。