Candelabrum vs Lollipop

    Our Android development team did not have time to sober up after the celebration of the new 2015, as units in the reviews on Google Play began to pour in for “brakes”. Units poured from users of powerful enough devices like 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 Android mobile operating system. Among its distinguishing features was the migration to a completely new ART runtime (short for Android RunTime) instead of the outdated Dalvik virtual machine. Actually, this is the difference between ART and Dalvik: the new runtime is no longer a virtual machine. Instead, it compiles applications during installation, which should significantly speed up their work.

    Where did the “brakes” come from? To begin with, we decided to reproduce them. To do this, they took two identical Nexus 5s: one with the latest Android 5 firmware, the other with Android 4.4 (KitKat). We downloaded identical files of saved games, went 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 KitKat device had 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 did not show a 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 suggest that. But ART on KitKat executed our code no more slowly than Dalvik. Both our and other people's performance measuring instruments showed that ART as a whole is faster than Dalvik. But there was one unit test in our preference that ran on Lollipop many times slower. We decided to delve into the code and look for possible reasons for this behavior.

    While analyzing the code, we noticed one feature in the unit test, which ran much slower on Google’s most advanced application runtime. The code itself consisted of primitive bitwise operations, which could not arouse suspicion. The peculiarity was the way they were called.

    Our code was organized this way: an abstract LookOver base class with several descendants. Inside this class are static final methods that are used in calculations both in LookOver itself and in successors. Therefore, static methods were declared protected so as not to expand 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 what the approximate descendant 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, these static calls are represented as INVOKESTATIC LookOverTest.methodName ().

    At the same time, if you call them not from the heir of the LookOver class, then the call looks like INVOKESTATIC LookOver.methodName ().

    We tried this hypothesis: removed extends, added

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


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

    Of course, it would not hurt one of the developers at Google to threaten with a candelabrum for such regressions. But most importantly, our problem has been resolved, an updated release has already been posted on Google Play . And we are pleased to share our research with Habr.

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

    Also popular now: