Pull to refresh

Сравнение производительности Xamarin (monodroid) и Java (DalvikVM) на Android устройствах

Reading time 5 min
Views 44K
image
Добрый день. Многие интересуются насколько сильно отличается производительность Xamarin на Android или iOS. Вопрос с iOS я пока оставлю открытым, а вот все вопросы по производительности monodroid предлагаю закрыть раз и навсегда.

Зачастую эти вопросы вызваны из-за неправильного понимания как устроен monodroid, мне например задавали вопросы типа «А Xamarin потом пересобирается под JVM?». Это конечно же не так. Важно понимать, что Xamarin выполняется на том же уровне Android где работает виртуальная машина Android Dalvik. Поэтому при сравнении производительности мы на деле имеем сравнение эффективности работы двух виртуальных машин: Mono VM и Dalvik VM.


Методика проверки


Для проверки производительности необходим реализованный на Java и общепризнанный метод, который необходимо будет реализовать на C#. Я решил использовать известный тест производительности LINPACK в первую очередь потому что его исходный код открыт и легко будет реализовать C# версию, а во-вторую — есть уже готовая Android версия LINPACK for Android написанная на Java.

Тест производительности LINPACK это метод оценки производительности путем оценки скорости выполнения операций с плавающей точкой вычислительной системы. Бенчмарк создан Джеком Донгарра (Jack Dongarra) и измеряет как быстро компьютер ищет решение плотной СЛАУ Ax = B размерностью N x N методом LU-декомпозиции. Решение получено методом Гаусса с выделением главного элемента (описание), при котором выполняется 2/3*N3 + 2*N2 операций с плавающей точкой. Результат определяется в Floating-point Operations Per Second(MFLOP / s, чаще просто FLOPS). Сам тест производительности был описан в документации к фортрановской библиотеке линейных вычислений LINPACK и с тех пор его вариации используются, например, для составления рейтинга TOP500 суперкомпьютеров.

Реализация


Несмотря на довольно простую задачу я решил реализовывать ее согласно выработанному мною гайдлайну на кроссплатформенную разработку.

На первом шаге строим кроссплатформенное решение:

Потом согласно шаблону проектирования MVP создаем Presenter и View
image
Пишем тесты и реализуем Presenter через тест. Для этого я использую NUnit и NSubstitute. Описывать NUnit особого смысла нету, на мой взгляд более удобного фреймворка для тестирования просто нету. NSubstitute это крайне удобный фреймворк для создания заглушек на основании интерфейсов. Фреймворки устанавливаются через NuGet.
Небольшой пример использования NSubstitute из доков
// Let's say we have a basic calculator interface:
    public interface ICalculator
    {
        int Add(int a, int b);
        string Mode { get; set; }
        event Action PoweringUp;
    }
// We can ask NSubstitute to create a substitute instance for this type. We could ask for a stub, mock, fake, spy, test double etc., but why bother when we just want to substitute an instance we have some control over?
    _calculator = Substitute.For<ICalculator>();
// Now we can tell our substitute to return a value for a call:
    _calculator.Add(1, 2).Returns(3);
    Assert.That(_calculator.Add(1, 2), Is.EqualTo(3));
// We can check that our substitute received a call, and did not receive others:
    _calculator.Add(1, 2);
    _calculator.Received().Add(1, 2);
    _calculator.DidNotReceive().Add(5, 7);
// If our Received() assertion fails, NSubstitute tries to give us some help as to what the problem might be:
    NSubstitute.Exceptions.ReceivedCallsException : Expected to receive a call matching:
        Add(1, 2)
    Actually received no matching calls.
    Received 2 non-matching calls (non-matching arguments indicated with '*' characters):
        Add(1, *5*)
        Add(*4*, *7*)

// We can also work with properties using the Returns syntax we use for methods, or just stick with plain old property setters (for read/write properties):
    _calculator.Mode.Returns("DEC");
    Assert.That(_calculator.Mode, Is.EqualTo("DEC"));
    _calculator.Mode = "HEX";
    Assert.That(_calculator.Mode, Is.EqualTo("HEX"));


В нашем случае сначала создаем Setup метод
[TestFixtureSetUp]
public void Setup()
{
      view = Substitute.For<ILinpackView>();
      presenter = new LinpackPresenter(view);
}

И выполняем простой тест, естественно асинхронно
 [Test]
 public void CalculateAsyncTest()
 {
       presenter.CalculateAsync().Wait();
       Assert.That(view.Mflops, Is.EqualTo(130.0).Within(5).Percent);            
 }

Сторонним ПО я выяснил что производительность моего ПК — около 130 MFLOPS/s, так что его и впишем в ожидаемые значения, добавив погрешность.

Внутри метода у меня создание асинхронной Task и заполнение View. Все просто и понятно
public Task CalculateAsync()
{
    Linpack l = new Linpack();
    return Task.Factory.StartNew(() => l.RunBenchmark())
        .ContinueWith(t =>
        {                    
            _view.Mflops = l.MFlops;
            _view.NormRes = l.ResIDN;
            _view.Precision = l.Eps;
            _view.Time = l.Time.ToString();
        }
        );
}


Программа фактически реализована, ни строчки платформозависимого кода пока написано не было. Теперь создаем Android проект:

визуальный конструктор внутри Visual Studio позволит нам быстро и просто накидать необходимое приложение и посмотреть как оно выглядит с разными темами. Правда, так как ни одна из тем не была применена к Activity то на устройстве мы получим вид по умолчанию

Теперь остается только скомпилировать релиз, подписать его приложенным к студии мастером и установить на устройства с помощью консольной утилиты adb.exe


Недавно я приметил, что скорость выполнения кода зависит не только от Debug/Release но и от подключения устройства по USB. Происходит это из-за того что при подключенном телефоне в режиме разработки Android гонит огромные объемы отладочной информации на компьютер, что может повлиять на скорость выполнения операций (а может и не повлиять), особенно если есть конструкции логирования типа Trace. Поэтому отключаем устройства и запускаем тесты.

Готово!

Результат


У меня два тестовых устройства, HTC Desire с Android 2.3 (Qualcomm QSD8250, 1000 МГц, 512 ОЗУ) и Fly IQ443 с Android 4.0.4 (MediaTek MT6577, 1000 МГц, 512 ОЗУ)

Вот их результаты:
HTC Desire:


Fly IQ443:


И красивые графики


Вывод


Результаты теста показывают что работа Mono на Android как минимум сравнима, а иногда и превосходит производительность Dalvik, но в целом они примерно равны. Второй столбец некорректен, т.к. Mono самостоятельно распараллелил тест на два ядра без каких либо действий с моей стороны, полагаю где-то на уровне Task'ов, в то время как для Linpack for Android необходимо явно выбрать мультипоточный тест.

Кстати говоря, этот проект показывает различия в размерах для релизовых сборок. Java версия теста весит всего лишь 280Кб, когда Monodroid версия весит почти 2.3 МБ на один тип процессора (armeabi и armeabi-v7), т.е. в сумме 4.6 МБ, что впрочем в условиях современных сетей не кажется мне особо критичным. Если такой размер apk кажется вам неприемлемым можно отдельно собрать и распространять пакеты для armeabi и armeabi-v7, благо Google Play позволяет загружать разные apk для разных платфом.

Исходный код приложения находится здесь

P.S. Мне бы хотелось собрать статистику по устройствам. Так что если у кого есть Android и 10 минут свободного времени буду очень благодарен если вы измерите производительность вашего устройства с помощью LINPACK for Android и вновь созданной MonoLINPACK и запишите результат сюда (если у вас многоядерный процессор выбирайте сразу Run Multiple Thread в LINPACK for Android)

P.P.S. Аналогичный тест для iOS будет

UPD1 По текущим тестам видно что Mono и Task по умолчанию не использует все ядра на 4-ядерных процессорах. Поэтому результаты между Linpack for Android и MonoLinpack на таких устройствах сильно различаются. В ближайшее время MonoLINPACK будет модифицирован с использование TPL.
Tags:
Hubs:
+37
Comments 36
Comments Comments 36

Articles