5

就我(有限的)优化知识而言,我一直认为编译器优化并不重要我的意思是,我们肯定可以通过使用寄存器而不是内存和展开循环来获得相当多的时间(可能是 0...100% 作为一个非常粗略的初步想法),但限制代码性能的基本因素确实是算法的选择。


我最近开始了一个宠物项目 - 一种小型解释脚本语言。它编译成字节码并使用虚拟机执行。为了便于调试、分析和测试,我首先自然地用(and ) 的-O0 -g标志编译了代码,然后用. 然后我在虚拟机上运行了一个小程序,它基本上是这样做的(伪代码,在项目公开之前我不会向你展示实际的语法):gccclang-O2time

i = 1
sum = 0

while (i < 10000000) {
    sum = (sum + i * i) mod 1000000
    i++
}

print sum

这大致转换为以下伪程序集:

load r0, 0   # sum = 0
load r1, 1   # i = 1
load r2, 1000000
load r3, 10000000

loop:
mul  r4, r1, r1 # i * i
add  r0, r0, r4 # sum += i * i
mod  r0, r0, r2 # sum %= 1000000
inc  r1
gt   r5, r1, r3 # if i > 10000000
jz   r5, loop   # then don't goto loop

ret  r0

所以基本上,这是一个有 10000000 次迭代的紧密循环。time报告它在编译时运行 0.47...0.52 秒-O2,在编译时运行 1.51...1.74 秒,在启用-O0 -g分析 () 时运行 3.16...3.47 秒。-pg

如您所见,最快和最慢的执行时间之间存在 7 倍的差异。

这本身并不令人惊讶,因为我知道额外的调试信息和缺乏小的优化确实会使代码运行速度变慢,但现在是有趣的部分。为了更真实地了解实际发生的情况,我使用 Lua 5.2 玩过同样的游戏。摆弄Makefile,我发现同样的 Lua 程序:

local sum = 0

for i = 1, 10000000 do
    sum = (sum + i * i) % 1000000
end

print(sum)

使用 编译 Lua 时运行大约 0.8...0.87 秒,启用-O0 -g -pgonly 时运行时间为 0.39...0.43 秒。-O2

因此,当优化器对其执行棘手的修复时,我的代码似乎有 7 倍的速度提升,而 Lua 参考实现似乎从这些改进中受益要少得多。

现在我的问题是:

  1. 知道为什么会这样吗?我怀疑主要原因是 Lua 的创建者比我更聪明,并且更了解编译器和他们的 VM 在做什么。

  2. 我也发现自己在想“这一定是过早的优化;让我把它传递给优化器,无论如何它都会完成这项工作”几次。这些包括在几乎每条VM指令的实现中调用的单行静态函数(我认为无论如何都会在必要时内联它们),使用各种复杂的表达式(有时具有不太容易预测的副作用)不过,这可以简化。我的这种态度也算吗?(顺便说一句,这是正确的态度吗?)

  3. 无论如何,我应该担心这种现象吗?毕竟,我的代码运行速度比 Lua 慢 1.5 倍——这非常好(至少对我来说已经足够好了)。我是否应该仅仅因为不这样做表明我对自己的代码缺乏深入的了解而尝试提高调试构建的性能?或者我可以完全忘记它,只要发布(优化)构建足够快?

(如果这个问题更适合 Prog.SE 或 CodeReview,请告诉我,我会迁移它。)

4

1 回答 1

1

首先,您关于算法比优化更重要的断言通常是正确的,但是可以对相同的算法进行编码以最佳或最差地使用您正在执行的平台,因此应始终考虑优化......只是避免优化过早地,而不是完全避免优化。

接下来,请记住调试构建会增加很多开销。不仅仅是禁用优化。要查看优化器在做什么,请使用禁用优化的发布版本。

Lua 和您的语言之间的差异将取决于您的字节码解释器的效率。这里的微小低效率将对如此大循环的执行速度产生巨大影响。您也许还可以添加优化,例如:

  • 使用“寄存器”来保存循环中使用的变量(在字节码中,将变量加载到插槽 1,然后使用新指令使用简单的数组索引而不是命名变量来乘法和修改插槽,然后编写最终的在循环结束时将槽的值返回给变量。
  • 检测到循环执行恒定次数,也许有一种方法可以在字节码中表达这一点,以便循环变量和逻辑由本机代码执行,而不是通过解释字节码。您显然可以为很多情况添加特殊情况,所以这里的技巧是找出最常见的结构并首先优化它们,以获得最大的收益。

最后,不要担心调试代码的效率。当你有一个工作的解释器时,你可以分析一个发布版本来寻找你可以改进的地方。过早地这样做不会有帮助,因为优化部分完整的代码然后发现需要对其进行更改以支持新功能是没有意义的。只有当你有一个工作系统,你才能开始编写典型的脚本,以一种现实的方式锻炼你的解释器——你可能会发现像上面的例子那样优化循环在日常脚本中没有任何好处。

于 2013-09-01T10:41:56.113 回答