Smalltalk 字节码优化值得付出努力吗?

Are Smalltalk bytecode optimizations worth the effort?

考虑Juicerclass中的以下方法:

Juicer >> juiceOf: aString
    | fruit juice |
    fruit := self gather: aString.
    juice := self extractJuiceFrom: fruit.
    ^juice withoutSeeds

它生成以下字节码

25 self                     ; 1
26 pushTemp: 0              ; 2
27 send: gather:
28 popIntoTemp: 1           ; 3
29 self                     ; 4
30 pushTemp: 1              ; 5
31 send: extractJuiceFrom:
32 popIntoTemp: 2           ; 6 <-
33 pushTemp: 2              ; 7 <-
34 send: withoutSeeds
35 returnTop

现在注意 32 和 33 抵消了:

25 self                     ; 1
26 pushTemp: 0              ; 2
27 send: gather:
28 popIntoTemp: 1           ; 3 *
29 self                     ; 4 *
30 pushTemp: 1              ; 5 *
31 send: extractJuiceFrom:
32 storeIntoTemp: 2         ; 6 <-
33 send: withoutSeeds
34 returnTop

接下来考虑 28、29 和 30。它们在 gather 的结果下方插入 self。通过在发送第一条消息之前按 self 可以实现相同的堆栈配置:

25 self                     ; 1 <-
26 self                     ; 2
27 pushTemp: 0              ; 3
28 send: gather:
29 popIntoTemp: 1           ; 4 <-
30 pushTemp: 1              ; 5 <-
31 send: extractJuiceFrom:
32 storeIntoTemp: 2         ; 6
33 send: withoutSeeds
34 returnTop

现在取消29和30

25 self                     ; 1
26 self                     ; 2
27 pushTemp: 0              ; 3
28 send: gather:
29 storeIntoTemp: 1         ; 4 <-
30 send: extractJuiceFrom:
31 storeIntoTemp: 2         ; 5
32 send: withoutSeeds
33 returnTop

临时文件 1 和 2 已写入但未读取。因此,调试时除外,它们可以被跳过导致:

25 self                     ; 1
26 self                     ; 2
27 pushTemp: 0              ; 3
28 send: gather:
29 send: extractJuiceFrom:
30 send: withoutSeeds
31 returnTop

最后一个版本,它节省了 7 个堆栈操作中的 4 个,对应于表达能力较差且清晰的来源:

Juicer >> juiceOf: aString
    ^(self extractJuiceFrom: (self gather: aString)) withoutSeeds

另请注意,还有其他可能的优化 Pharo(我没有检查 Squeak)没有实现(例如,跳转链)。这些优化将鼓励 Smalltalk 程序员更好地表达他们的意图,而不必支付额外计算的成本。

我的问题是这些改进是否是幻觉。具体而言,Pharo/Squeak 中缺少字节码优化是因为已知它们几乎没有相关性,还是它们被认为是有益的但尚未得到解决?

编辑

使用寄存器+堆栈架构的一个有趣优势 [cf. A Smalltalk Virtual Machine Architectural Model by Allen Wirfs-Brock 和 Pat Caudill] 是寄存器提供的额外 space 使得为了优化而更容易地操纵字节码。当然,即使这些类型的优化不像方法内联或多态内联缓存那样相关,正如下面的答案中所指出的,它们也不应该被忽视,尤其是当与 JIT 编译器实现的其他优化结合使用时。另一个值得分析的有趣话题是 破坏性 优化(即需要反优化以支持调试器的优化)是否真的有必要,或者 可以获得足够的性能提升非破坏性 技术。

开始使用此类优化时的主要烦恼是调试器界面。

过去和现在仍然在 Squeak 中,调试器正在模拟字节码级别,需要将字节码映射到相应的 Smalltalk 指令。

所以我认为收益太低,无法证明复杂化的合理性,甚至更糟的是调试工具的退化。

Pharo 想要更改调试器以在更高级别(抽象语法树)上运行,但我不知道它们将如何以 VM 所知道的字节码结束。

IMO,这种优化最好在 JIT 编译器中实现,它将字节码转换为机器本机代码。

编辑

最大的收获是消除发送本身(通过内联),因为它们比堆栈操作昂贵得多 (x10) - 当您测试 1 个 tinyBenchmarks (COG) 时,每秒执行的字节码比发送多 10 倍VM).

有趣的是,这种优化可以在 Smalltalk 图像中进行,但只能在 VM 检测到的热点上进行,就像在 SISTA 中所做的那样。参见示例 https://clementbera.wordpress.com/2014/01/22/the-sista-chronicles-iii-an-intermediate-representation-for-optimizations/

因此,根据 SISTA,答案是:有趣,尚未解决,但正在积极研究(并且正在进行中)!

当必须调试方法时,所有用于去优化的机制仍然是我理解的难点之一。

我认为一个更广泛的问题值得回答:字节码值得付出努力吗?字节码被认为是接近目标机器的紧凑且可移植的代码表示。因此,它们很容易解释,但执行起来很慢。

字节码在任何这些游戏中都不会excel,如果您想编写解释器或快速 VM,这通常使它们不是最佳选择.一方面,AST 节点更容易解释(只有少数节点类型与大量不同的字节码)。另一方面,随着 JIT 编译器的出现,很明显 运行 本地代码不仅可行而且速度更快.

如果您查看 JavaScript(可以被认为是当今最现代的编译器)和 Java(HotSpot、Graal)的最高效 VM 实现,您会看到他们都使用分层编译方案。方法最初是从 AST 中解释的,只有当它们成为热点时才会被 jitted。

在最难的编译层没有字节码。编译器中的关键组件是它的中间表示,而字节码不满足所需的属性。最可优化的 IR 的粒度要细得多:它们采用 SSA 形式,并允许寄存器和内存的特定表示。这允许更好的代码分析和优化。

话又说回来,如果您对可移植代码感兴趣,没有比 AST 更可移植的了。此外,实现基于 AST 的调试器和分析器比基于字节码的调试器和分析器更容易、更实用。唯一剩下的问题是紧凑性,但无论如何你可以实现类似 ast-codes(编码的 asts,类似于字节码但代表树)

另一方面,如果您想要全速,那么您将选择具有良好 IR 且没有字节码的 JIT。我认为字节码并没有填补当今 VM 中的许多空白,但仍然主要是为了向后兼容(也有许多直接执行 Java 字节码的硬件架构示例)。

还有一些与字节码相关的 Cog VM 的很酷的实验。但据我了解,他们将字节码转换为另一个 IR 以进行优化,然后再转换回字节码。我不确定最后的转换除了重用原始的JIT架构之外是否有技术上的收获,或者是否真的在字节码级别进行了任何优化。