Pull to refresh

Канделябр vs леденец

Game development *Development for Android *
Не успела наша команда разработчиков Марьяжа для Android протрезветь после празднования нового 2015 года, как в отзывах на Google Play начали сыпаться единицы за “тормоза”. Единицы сыпались от пользователей достаточно мощных устройств вроде Nexus 5, 6 и 7. Единственное, что их объединяло — это операционная система: Android 5 (Lollipop).

Как известно, незадолго до нового года, Google выпустил обновление для своей мобильной операционной системы Android. Среди ее отличительных особенностей была миграция на принципиально новую среду исполнения ART (сокращенно от Android RunTime) вместо морально устаревшей виртуальной машины Dalvik. Собственно, в этом и есть отличие ART от Dalvik: новая среда исполнения больше не является виртуальной машиной. Вместо этого она компилирует приложения во время установки, что должно значительно ускорить их работу.

Откуда же взялись “тормоза”? Для начала, мы решили их воспроизвести. Для этого взяли два идентичных Nexus 5: один с самой последней прошивкой Android 5, другой — с Android 4.4 (KitKat). Загрузили идентичные файлы сохраненных игр, зашли в оба приложения и запустили сохраненную игру. Индикатор прогресса на устройстве с самой последней версией Android едва дошел до половины, в то время как устройство с KitKat уже сделало несколько ходов. Действительно, тормоза есть.

Google представил ART широкой общественности как раз в Android 4.4, как опцию для разработчиков, оставив Dalvik по-умолчанию. Мы переключили наш Nexus 5 с Android 4.4 на ART и попробовали ту же сохраненную игру. К нашему удивлению, расчет хода прошел с той же скоростью, что и до этого на Dalvik.

Должно же быть этому всему объяснение? Мы попробовали стандартный профайлинг, который идет в комплекте с Android SDK. Но к нашему удивлению, значительной разницы в скорости расчетов он не показал.

Быть может ART еще сырой и скомпилированный им код медленнее, чем код, генерируемый JIT-компилятором Dalvik? Мы могли бы предположить такое. Но ведь ART на KitKat выполнял наш код ничуть не медленнее Dalvik. Как наши, так и чужие средства измерения производительности показывали, что ART в целом быстрее Dalvik. Но был один unit test в нашем преферансе, который выполнялся на Lollipop в разы медленнее. Мы решили углубиться в код и поискать возможные причины такого поведения.

Анализируя код, мы заметили одну особенность в unit test, который выполнялся значительно медленнее на самой современной среде выполнения приложений от Google. Сам по себе код состоял из примитивных битовых операций, что не могло вызвать подозрений. Особенность состояла в способе их вызова.

Наш код был организован таким образом: абстрактный базовый класс LookOver с несколькими наследниками. Внутри этого класса — static final методы, которые используются в расчетах как в самом LookOver, так и наследниках. Поэтому статические методы были объявлены protected, чтобы слишком не расширять их область видимости:

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);
	}
}


Вот так выглядел примерный наследник этого класса:

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");
						}
					}
				}
			}
		}
	}
}


В Java-байткоде такие статические вызовы представлены как INVOKESTATIC LookOverTest.methodName().

В то же время если вызывать их не из наследника класса LookOver, то вызов выглядит как INVOKESTATIC LookOver.methodName().

Мы попробовали эту гипотезу: убрали extends, добавили

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


И запустили. Если до изменений вышеприведенный код выполнялся на Lollipop 36 секунд, то после — всего 0.8 секунды! Проблема найдена! Мы вынесли весь подобный код в нашем преферансе в отдельные утилитарные классы, состоящие исключительно из статических методов. И это сделало расчет хода в игре даже быстрее, чем до этого на KitKat — надо сказать спасибо среде исполнения ART.

Конечно, не мешало бы кому-то из разработчиков в Google пригрозить канделябром за такие регрессии. Но самое главное — наша проблема решена, обновленный релиз уже выложен на Google Play. И мы рады поделиться нашими изысканиями с Хабром.

UPD: datacompboy прислал ссылку: android-review.googlesource.com/#/c/125900 Ошибка исправлена, ревью пройдено.
Tags:
Hubs:
Total votes 56: ↑51 and ↓5 +46
Views 23K
Comments Comments 10