Dalvik和Android工具链能带来哪些优化?

67

我正在设计一个高性能的Android应用程序(一个游戏),虽然我试图首先编写易读的代码,但我也喜欢将发生在内部的事情想象在脑海里。 在C ++中,我对编译器会为我做什么以及不会做什么有了相当好的直觉。 我正在尝试为Java / Android做同样的事情。

因此,出现了下面这个问题。 我几乎找不到有关此主题的信息。Java编译器,Dalvik转换器(dx)和/或JITter(在Android 2.2+上)是否会执行如下优化?

  • 方法内联。 在什么条件下会内联?private 方法总是可以安全地进行内联; 它会被执行吗? 像 public final 方法这样的方法呢? 其他类别的对象的方法? static 方法? 如果编译器可以轻松推断对象的运行时类型,该怎么办? 我应该尽可能声明方法为finalstatic吗?

  • 公共子表达式消除。例如,如果我两次访问someObject.someField,查找只会执行一次吗? 如果是调用getter,那该怎么办? 如果我两次使用某个算术表达式,它会被计算一次吗? 如果我将某个表达式的结果用作知道值不会更改的 for 循环的上限,会怎样?

  • 数组查找的边界检查。 在什么条件下,工具链会消除这种情况,例如典型的for循环?

  • 值内联。对某些public static final int的访问是否总是内联的? 即使它们在另一个类中? 即使它们在另一个包中?

  • 分支预测。 这是多大的问题? 在典型的Android设备上,分支是很大的性能损失吗?

  • 简单算术运算。 是否将someInt * 2 替换为 someInt << 1

等等...


5
这可能很有用:http://developer.android.com/guide/practices/design/performance.html - pablochan
2
这些也可能会有用:http://www.netmite.com/android/mydroid/dalvik/docs/dexopt.html,http://www.taranfx.com/android-internals-jit-froyo,以及演示文稿的pdf版本:http://www.android-app-developer.co.uk/android-app-development-docs/android-jit-compiler-androids-dalvik-vm.pdf - Lior
@pablochan:那个我已经藏了一段时间了,但还是谢谢 :) @Lior:好的参考资料,这些对我来说都是新的,谢谢! - Thomas
哦,谢谢 :) (即使在Java中,它们可能在溢出或负数方面具有微妙的不同语义?不知道,从未检查过...) - Thomas
应该可以编写一个针对.class(或.dex)文件的优化器,它至少可以实现其中一些优化(例如将本地或最终值升提)。我曾希望Proguard能够做到这一点,但看起来他们没有;可能是因为JVM已经实现了这个功能。 - Laurens Holst
显示剩余2条评论
3个回答

106

这是Ben,他是谷歌JIT工程师之一。当Bill和我开始这个项目时,目标是尽快交付一个能够在低端设备上运行的工作JIT,并最小化对资源争用(例如内存占用、被编译器线程占用的CPU)的影响。因此,我们使用了一种非常原始的基于跟踪的模型。也就是说,传递给JIT编译器的编译实体是一个基本块,有时只有一条指令那么短。这些跟踪将通过称为chaining的技术在运行时拼接起来,以便不经常调用解释器和代码缓存查找。在某种程度上,加速的主要来源是消除频繁执行的代码路径上的重复解释器解析开销。

话虽如此,我们确实实现了许多本地优化Froyo JIT:

  • 寄存器分配(针对v5te目标8个寄存器,因为JIT产生Thumb代码/针对v7 16个寄存器)
  • 调度(例如Dalvik寄存器的冗余ld/st消除、负载提升、存储下沉)
  • 冗余空检查消除(如果在基本块中可以找到这样的冗余)。
  • 简单计数循环的循环形成和优化(即循环体中没有边缘退出)。对于这样的循环,基于扩展感应变量的数组访问被优化,使得空值和范围检查仅在循环前导中执行。
  • 每个虚拟调用站点一个入口内联高速缓存,运行时动态修补。
  • 乘/除法文字操作数的缩减等小优化。

在Gingerbread中,我们为getter/setter实现了简单的内联。由于底层JIT前端仍然是简单的跟踪,如果被调用者在其中有分支,它将不会被内联。但是,内联缓存机制已经实现,以便可以无问题地内联虚拟getter/setter。

我们目前正在努力扩大编译范围,超出简单的跟踪,以便编译器具有更大的代码分析和优化窗口。请关注。


11
你特意注册来回答这个问题?谢谢!可惜运营商推广Gingerbread太慢了,我认为至少还要等待一年才能指望得到那些优化。这些都是很好的内容,但由于我不是编译器的作者,我很难看出如何在实践中应用它。特别是:... - Thomas
2
(1) 如果循环的上限取决于非最终变量(例如字段),那么循环优化是否也会执行,还是应该将上限存储在最终本地变量中? (2) 对于所有重复访问的字段,同样的问题。 (3) 我应该尽可能地声明我的方法为“final”吗?或者这仍然算作虚拟调用点? (4) 通常情况下,JIT级别不会进行内联处理,但您可能知道编译器和/或dx工具是否会这样做? - Thomas
2
(1) 只要上限/下限是循环不变量,声明一个本地变量并在循环外部读取它是最干净的方法。(2) JIT尚未进行循环不变量优化。因此,您需要再次使用本地变量来在循环外部读取它。(3) 是的,请。(4) 我们希望保持开发人员调试代码的能力,因此dx不执行内联。但是很快JIT将会积极地进行内联。 - Ben
3
@Ben +1你能回顾一下蚂蚁做了哪些优化吗?考虑到这个问题仍然有意义。那将非常棒! - loretoparisi

10

我相信我的答案可能无法回答你所有的问题,但如果它可以回答一个问题,那么这也是一种胜利。

你似乎对这个主题有深入的了解,并知道自己想要什么,所以你可以尝试以下步骤:建立一个包含你想调查的方面的示例应用程序。

使用APK Tool运行所得到的APK。反向工程您自己的代码来实现您打算做的事情是可以的,因为我们知道这是合法的。

APK工具将提取和解码您的资源,并将反向工程.dex文件生成.smali文件。你可能需要查阅smali项目,以获取有关如何阅读.smali文件及其限制的更多信息。

再次强调,我相信这不会回答你所有的问题,但这可能是一个好的开端。


1
好的答案,谢谢。这种调查方法之前没有出现在我的脑海中,主要是因为需要很多时间。这至少可以展示Java编译器和dx正在做什么,尽管JITter的影响仍然不确定。如果我感到好奇并走这条路,我一定会在这里发布我的结果。 - Thomas
好的,请这么做。我自己也很感兴趣了解结果。 - Octavian Helm
1
javac 进行了一些优化,但并没有什么显著的变化。 "dx" 提供了其输入的忠实转换。正如Ben所指出的,如果这些事情不是真实的,您将会在调试器中遇到很多问题。有关实践示例,请参见http://groups.google.com/group/android-platform/browse_thread/thread/e4749164474fb429/93901e2e43a657c8(尤其是在不传递“-g”给javac的情况下,“dx”生成更好的代码的部分)。您还应该研究ProGuard的优化。 - fadden

5
首先,我要说明一下,我不是dalvik方面的专家,我的回答可能有误。但我已经深入研究了dalvik中的JIT代码,并且对dalvik运行的字节码非常熟悉。
  1. 方法内联 - 据我所知,这从未发生过。我几乎可以确定它从未在字节码级别上发生过,我认为它目前也没有在JIT级别上发生 - 尽管将来可能会。

  2. 公共子表达式消除 - 我认为只有对不使用任何非final变量/字段的子表达式才会执行此操作。即使是这样,我也不完全确定它是否会发生。如果执行此操作,我希望它会在字节码级别上完成,而不是在JIT级别上。

  3. 数组查找的边界检查 - 不知道

  4. 值内联 - 据我所知,在所有这些情况下,它们都将被内联。

  5. 分支预测 - 不确定

  6. 简单算术 - 据我所知,不会

另外,我想向您提供另一种途径 - dx和dalvik都是开源的,所以您可以随心所欲地深入研究它们。不过,它们显然不是小型的代码库,因此需要花费相当大的精力才能深入挖掘。


好的,如果这是一个参考,那么我在手动内联我的方法和缓存子表达式结果方面做得很好。谢谢! - Thomas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接