📜 ⬆️ ⬇️

Candelabrum vs Lollipop

Our Android development team did not have time to sober up after the celebration of the new 2015, when, in reviews on Google Play, units began to crumble for “brakes”. Units rained down from users of fairly powerful devices like the Nexus 5, 6 and 7. The only thing that united them was the operating system: Android 5 (Lollipop).

As you know, shortly before the new year, Google released an update for its mobile operating system Android. Among its distinctive features was the migration to a fundamentally new execution environment ART (abbreviated from Android RunTime) instead of the outdated Dalvik virtual machine. Actually, this is the difference between ART and Dalvik: the new execution environment is no longer a virtual machine. Instead, it compiles applications during installation, which should speed up their work significantly.

Where did the brakes come from? To begin with, we decided to reproduce them. To do this, they took two identical Nexus 5: one with the latest Android 5 firmware, the other with Android 4.4 (KitKat). We downloaded identical files of saved games, logged into both applications and launched the saved game. The progress indicator on the device with the latest version of Android barely reached half, while the device with KitKat has already made several moves. Indeed, there are brakes.

Google introduced ART to the general public just in Android 4.4 as an option for developers, leaving Dalvik by default. We switched our Nexus 5 from Android 4.4 to ART and tried the same saved game. To our surprise, the calculation of the move went at the same speed as before on Dalvik.
')
Should there be an explanation for all this? We tried the standard profiling that comes with the Android SDK. But to our surprise, he showed no significant difference in the speed of calculations.

Perhaps ART is still raw and the code compiled by it is slower than the code generated by the Dalvik JIT compiler? We could guess that. But after all, ART on KitKat executed our code no more slowly than Dalvik. Both our and others' performance measurement tools have shown that ART is generally faster than Dalvik. But there was one unit test in our preference, which was performed on Lollipop several times slower. We decided to go deep into the code and look for possible reasons for this behavior.

Analyzing the code, we noticed one feature in the unit test, which ran significantly slower on the most modern application runtime environment from Google. By itself, the code consisted of primitive bit operations, which could not cause suspicion. A special feature was the way they were called.

Our code was organized this way: an abstract base class LookOver with several heirs. Inside this class there are static final methods that are used in calculations both in LookOver itself and in successors. Therefore, static methods have been declared protected so as not to extend their scope too:

public class LookOver { protected static final long newCount(final int w, final int index) { return ((long) (w & 0xFF) << (index << 3)); } protected static final byte extractW0(final long count) { return (byte) count; } protected static final byte extractW1(final long count) { return (byte) (count >>> 8); } protected static final byte extractW2(final long count) { return (byte) (count >>> 16); } protected static final byte extractWin(final long count) { return (byte) (count >>> 24); } } 


This is how the approximate heir of this class looked like:

 public class LookOverTest extends LookOver { public void performCalculations() { final int min = 0; final int max = 50; for (int b1 = min; b1 <= max; b1++) { for (int b2 = min; b2 <= max; b2++) { for (int b3 = min; b3 <= max; b3++) { for (int b4 = min; b4 <= max; b4++) { final long value = newCount(b1, 0) | newCount(b2, 1) | newCount(b3, 2) | newCount(b4, 3); if (!(b1 == extractW0(value) && b2 == extractW1(value) && b3 == extractW2(value) && b4 == extractWin(value))) { throw new RuntimeException("Should not happen"); } } } } } } } 


In Java bytecode, such static calls are represented as INVOKESTATIC LookOverTest.methodName ().

At the same time, if they are not called from the heir of the LookOver class, the call looks like INVOKESTATIC LookOver.methodName ().

We tried this hypothesis: we removed extends, added

 import static com.example.benchmark.LookOver.*; 


And launched. If before the changes, the above code was executed on Lollipop for 36 seconds, then after - only 0.8 seconds! Problem found! We carried out all similar code in our preference into separate utilitarian classes consisting solely of static methods. And it made the calculation of the move in the game even faster than before on KitKat - I must say thanks to the ART execution environment.

Of course, it would not hurt for some Google developers to threaten a chandelier for such regressions. But the most important thing is that our problem has been solved, the updated release has already been posted on Google Play . And we are happy to share our research with Habr.

UPD: datacompboy sent the link: android-review.googlesource.com/#/c/125900 Bug fixed, review passed.

Source: https://habr.com/ru/post/249023/


All Articles