1. Введение
Недавно я задался вопросом сравнения производительности bytecode виртуальных машин (VM). Таких как JVM, LLVM, .Net, etc.
Прямое сравнение трудно было произвести по нескольким причинам. Во-первых, нужен был подходящий набор тестов, который хоть как-то покрывает предоставляемые VM функции. Во-вторых, нужно было найти язык, который бы компилировался в байт-код всех тех VM, которые я хотел протестировать, и наконец - нужно было просто выкроить время. Не прошло и полгода, как я решил эти проблемы, хоть и не в полной мере.
В качестве языка для примеров я выбрал Lua (5.1), т.к. компиляторы в байткод с него существуют для всех интересных мне машин. В качестве же тестов я взял исходники с The Computer Language Benchmarks Game, и кое-где доработал их рашпилем (правда, должен заранее сказать, что не знаю Lua). Предпочтение отдавалось "straightforward" реализациям, не имеющим системных завязок и минимально зависящим от библиотек языка. Понятно, что несложно приделать к любому языку быстродействующую библиотеку (к примеру на С), но тогда сравниваться будет совсем не то, что нам нужно.
Вот четыре теста, которые я использовал:
Binary tree - бинарные деревья, тест на работу с памятью
Heap sort - сортировка кучи, память + работа с целыми
Sieve - решето Эратосфена, битовые операции
Pi-digit - вычисление знаков числа Пи, числодробилка
Эти примеры были взяты из источника выше, с минимальными модификациями.
Какие же виртуальные машины были протестированы? Это:
Также в качестве референсных имплементаций были использованы "pure Java" и "pure Lua". Первая - это код из того же источника, но на Java, вторая - версия на Lua, но интерпретируемая стандартным интерпретатором Lua. Также в тест был включен весьма продвинутый (но языково-специфичный) компилятор и VM LuaJIT (2.0). Результаты, которых достиг этот компилятор всего за пару лет разработки весьма впечатляют (первый релиз версии 2.0 был в 2009 году).
Тесты прогонялись на моем компьютере на базе Intel Core 2 Duo. Первая VM запускалась под Windows XP (что логично) остальные - под Linux. Каждый тест занимавший более 30 секунд прогонялся три раза. В таблицу записывалось значение медианы. В случае, если дождаться результата для данной VM и входных параметров было проблематично, тест не проводился.
2. Собственно результаты
2.1. Цифры (время в секундах)
2.2. Графики




3. Обсуждение результатов
Многие графики далеки от линейности, поскольку тесты таковы, что даже при линейном увеличении входных параметров, вычислительная сложность растет нелинейно. В любом случае - лидеры нашего сравнения вполне очевидны, это LLVM и JVM. Microsoft-овский .Net не сильно уступает, а вот реализация Parrot пока может рассматриваться разве что в качестве proof of concept.
Не кажется странным и то, что машины специально заточенные под язык (такие как JVM и LuaJit) значительно обгоняют "универсальные" машины, деля между собой первенство в общем зачете. То, что JVM, которую вылизывают уже больше 10 лет быстро работает для pure Java реализации говорит еще и о том, что ее компилятор достаточно хорошо осведомлен о внутренней структуре виртуальной машины и способен сгенерировать оптимальный код. Если же код генерируется другим компилятором и для другого языка, то производительность резко падает. Это можно увидеть сравнив Pure Java реализации и Lua JVM.
Также наглядно видно, насколько большого выигрыша удается достичь используя технологию JIT. Мы видим, что LuaJIT близок к Java, а на одном тесте даже опережает ее. Отключение JIT в Java снижает производительность в 4-10 раз.
Наконец, имеет место довольно странный результат, который я пока не могу обьяснить в тесте Heap sort. А именно - при том, что рост времени с очевидностью должен быть линеен, мы наблюдаем странный провал на границе между миллионом и десятью миллионами item-ов как у Java так и у LuaJIT. Исследовав ситуацию более детально я обнаружил следующее.
У LuaJIT излом находится между 5 и 6 миллионами (5*10^6=4.67 sec, 6*10^6=16.7 sec) у Pure Java - между 4 и 10 (4*10^6=2.63 sec, 10*10^6=8.122 sec). Такое ощущение, что в этот момент VM (или даже OS?) меняет механизм выделения памяти.
4. Resume
Итак, исходя из результатов можно дать очевидные рекомендации: если вам нужна language-specific VM и ваш язык - Lua, то следует использовать LuaJIT, показавший очень высокую скорость. Если вам нужна универсальная машина - то больше подойдет LLVM. И наконец, если вы уже пользуетесь Java, то нет нужды мигрировать на другие решения, поскольку пока производительность JVM для "родного" языка не уступает другим VM (хотя перемены в будущем похоже грядут).
Недавно я задался вопросом сравнения производительности bytecode виртуальных машин (VM). Таких как JVM, LLVM, .Net, etc.
Прямое сравнение трудно было произвести по нескольким причинам. Во-первых, нужен был подходящий набор тестов, который хоть как-то покрывает предоставляемые VM функции. Во-вторых, нужно было найти язык, который бы компилировался в байт-код всех тех VM, которые я хотел протестировать, и наконец - нужно было просто выкроить время. Не прошло и полгода, как я решил эти проблемы, хоть и не в полной мере.
В качестве языка для примеров я выбрал Lua (5.1), т.к. компиляторы в байткод с него существуют для всех интересных мне машин. В качестве же тестов я взял исходники с The Computer Language Benchmarks Game, и кое-где доработал их рашпилем (правда, должен заранее сказать, что не знаю Lua). Предпочтение отдавалось "straightforward" реализациям, не имеющим системных завязок и минимально зависящим от библиотек языка. Понятно, что несложно приделать к любому языку быстродействующую библиотеку (к примеру на С), но тогда сравниваться будет совсем не то, что нам нужно.
Вот четыре теста, которые я использовал:
Binary tree - бинарные деревья, тест на работу с памятью
Heap sort - сортировка кучи, память + работа с целыми
Sieve - решето Эратосфена, битовые операции
Pi-digit - вычисление знаков числа Пи, числодробилка
Эти примеры были взяты из источника выше, с минимальными модификациями.
Какие же виртуальные машины были протестированы? Это:
- .Net CLR от Microsoft - без комментариев (использовался компилятор Lua2IL)
- Parrot VM - относительно новая и подающая надежды регистровая VM общего назначения (использовался компилятор Lua on Parrot)
- LLVM - одна из наиболее развитых регистровых VM общего назначения (использовался компилятор LLVM-Lua 2.7)
- JVM (последняя Sun-овская версия) - классика жанра (использовался компилятор LuaJ)
Также в качестве референсных имплементаций были использованы "pure Java" и "pure Lua". Первая - это код из того же источника, но на Java, вторая - версия на Lua, но интерпретируемая стандартным интерпретатором Lua. Также в тест был включен весьма продвинутый (но языково-специфичный) компилятор и VM LuaJIT (2.0). Результаты, которых достиг этот компилятор всего за пару лет разработки весьма впечатляют (первый релиз версии 2.0 был в 2009 году).
Тесты прогонялись на моем компьютере на базе Intel Core 2 Duo. Первая VM запускалась под Windows XP (что логично) остальные - под Linux. Каждый тест занимавший более 30 секунд прогонялся три раза. В таблицу записывалось значение медианы. В случае, если дождаться результата для данной VM и входных параметров было проблематично, тест не проводился.
2. Собственно результаты
2.1. Цифры (время в секундах)
| Binary tree | Pure Lua | Lua .Net | Lua Parrot | Lua LLVM | Lua JVM | LuaJIT | Pure Java | Java (no JIT) |
| 13 | 2.480 | 10.797 | 218.190 | 3.590 | 4.360 | 0.740 | 0.146 | 0.740 |
| 15 | 13.440 | 58.813 | 18.520 | 17.700 | 3.310 | 0.416 | 3.470 | |
| 17 | 65.210 | 300.000 | 89.330 | 79.600 | 15.650 | 1.510 | 16.240 | |
| Heap sort | Pure Lua | Lua .Net | Lua Parrot | Lua LLVM | Lua JVM | LuaJIT | Pure Java | Java (no JIT) |
| 100000 | 0.710 | 0.704 | 119.280 | 0.460 | 2.000 | 0.060 | 0.031 | 0.162 |
| 1000000 | 8.860 | 9.109 | 6.120 | 16.200 | 0.700 | 0.457 | 2.413 | |
| 10000000 | 107.580 | 118.328 | 75.650 | 193.300 | 28.440 | 8.122 | 31.440 | |
| Sieve | Pure Lua | Lua .Net | Lua Parrot | Lua LLVM | Lua JVM | LuaJIT | Pure Java | Java (no JIT) |
| 8 | 0.400 | 8.093 | 69.560 | 0.510 | 2.715 | 0.030 | 0.019 | 0.058 |
| 12 | 7.160 | 140.891 | 8.920 | 33.025 | 0.480 | 0.124 | 0.975 | |
| 16 | 121.500 | 154.960 | 539.000 | 8.900 | 2.678 | 18.650 | ||
| Pi-digits | Pure Lua | Lua .Net | Lua Parrot | Lua LLVM | Lua JVM | LuaJIT | Pure Java | Java (no JIT) |
| 1000 | 1.520 | 460.630 | 1.140 | 13.840 | 0.100 | 0.989 | 5.530 | |
| 2000 | 14.590 | 5.230 | 59.700 | 0.420 | 3.045 | 23.600 | ||
| 4000 | 64.790 | 22.660 | 270.700 | 1.840 | 12.020 | 99.800 | ||
| 8000 | 9.260 | 50.500 |
2.2. Графики




3. Обсуждение результатов
Многие графики далеки от линейности, поскольку тесты таковы, что даже при линейном увеличении входных параметров, вычислительная сложность растет нелинейно. В любом случае - лидеры нашего сравнения вполне очевидны, это LLVM и JVM. Microsoft-овский .Net не сильно уступает, а вот реализация Parrot пока может рассматриваться разве что в качестве proof of concept.
Не кажется странным и то, что машины специально заточенные под язык (такие как JVM и LuaJit) значительно обгоняют "универсальные" машины, деля между собой первенство в общем зачете. То, что JVM, которую вылизывают уже больше 10 лет быстро работает для pure Java реализации говорит еще и о том, что ее компилятор достаточно хорошо осведомлен о внутренней структуре виртуальной машины и способен сгенерировать оптимальный код. Если же код генерируется другим компилятором и для другого языка, то производительность резко падает. Это можно увидеть сравнив Pure Java реализации и Lua JVM.
Также наглядно видно, насколько большого выигрыша удается достичь используя технологию JIT. Мы видим, что LuaJIT близок к Java, а на одном тесте даже опережает ее. Отключение JIT в Java снижает производительность в 4-10 раз.
Наконец, имеет место довольно странный результат, который я пока не могу обьяснить в тесте Heap sort. А именно - при том, что рост времени с очевидностью должен быть линеен, мы наблюдаем странный провал на границе между миллионом и десятью миллионами item-ов как у Java так и у LuaJIT. Исследовав ситуацию более детально я обнаружил следующее.
У LuaJIT излом находится между 5 и 6 миллионами (5*10^6=4.67 sec, 6*10^6=16.7 sec) у Pure Java - между 4 и 10 (4*10^6=2.63 sec, 10*10^6=8.122 sec). Такое ощущение, что в этот момент VM (или даже OS?) меняет механизм выделения памяти.
4. Resume
Итак, исходя из результатов можно дать очевидные рекомендации: если вам нужна language-specific VM и ваш язык - Lua, то следует использовать LuaJIT, показавший очень высокую скорость. Если вам нужна универсальная машина - то больше подойдет LLVM. И наконец, если вы уже пользуетесь Java, то нет нужды мигрировать на другие решения, поскольку пока производительность JVM для "родного" языка не уступает другим VM (хотя перемены в будущем похоже грядут).