Эксперимент, показывающий двойную производительность кода, работающего на JVM, по сравнению с эквивалентным собственным кодом C.

Название этого проекта провокационное, и оно призвано привлечь внимание к определенным идеям. Пожалуйста, прочтите этот документ, прежде чем делать какие-либо выводы. А пока я просто скажу, что заголовок относится только к представленному здесь алгоритму, а не к Java и C в целом. Я также далек от того, чтобы убедить кого-либо выбрать Java вместо любого другого языка, и я даже вижу веские причины отговаривать вас от использования Java во множестве случаев. В последнее время я редко программирую Java сам.

Вот проект GitHub, содержащий код, описанный здесь:



Мой типичный диалог прошлых дней

«Ваш код, работающий на виртуальной машине, будет ВСЕГДА медленнее, чем эквивалентный нативный код».

"Почему?"

«Из-за автоматического управления памятью».

"Почему так?"

«Такие вещи, как автоматическое управление памятью, ВСЕГДА увеличивают нагрузку на выполнение».

"Хм, давайте попробую, вот код на Java и прямой аналог на C, первый почти в 2 раза быстрее."

«Это потому, что вы делаете что-то неправильно. Никто не стал бы писать такой код на C».

"Почему?"

"Потому что для повышения эффективности вам необходимо правильно управлять памятью".

"Как вы это делаете?"

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

"Хорошо, значит, вы только что сделали противоречивые заявления?"

"Я так не думаю, просто добавьте эти несколько строк в свой код".

"Как вы думаете, после этого алгоритм останется тем же?"

"Да".

"Но приспособлено ли ваше решение для управления памятью к этому конкретному коду C и, следовательно, расширено ли алгоритм?"

"Да".

"Значит, это уже не алгоритмически эквивалентный код, не так ли?"

"Да".

"Вы снова сделали противоречивые заявления?"

«Я так не думаю».

Покажи мне код

Код почти одинаков на обоих языках, по-прежнему используются типичные для них соглашения:

Я почти уверен, что алгоритмически они равны, за исключением очевидного явного освобождения памяти в версии C. Вот старая, но исчерпывающая статья, проливающая свет на мои результаты.

Я не писал никакого кода на C в течение 2 десятилетий, и было приятно написать его сейчас, чтобы заново открыть для себя, насколько Java на самом деле близок и повлиял на C, и как он спроектирован так, чтобы работать на удивление близко к оборудованию (примитивные типы данных) .

Код сначала устанавливает кольцо узлов, а затем непрерывно мутирует его, удаляя узлы в одном направлении и вставляя их в другом. Количество вставленных и удаленных узлов непредсказуемо. То же самое относится и к размеру узла. Тем не менее, псевдослучайное распределение будет точно таким же для Java и C.

Для этого я взял детерминированное, почти случайное распределение, которое часто использую в GLSL, которое позаимствовал из Книги шейдеров. Я также написал тест для этого:

private static double almostPseudoRandom(long ordinal) {
  return (Math.sin(((double) ordinal) * 100000.0) + 1.0) % 1.0;
}

Я ожидал, что на этот раз C-код будет в 2 раза быстрее, но, к моему удивлению, Java-версия снова быстрее (хотя и не в 2 раза), чего я не могу объяснить. У меня много гипотез:

  • HotSpot делает несколько агрессивных инлайнов, которые возможны после того, как работающий код некоторое время анализируется.
  • Математические функции C взяты из библиотеки, поэтому, возможно, их нельзя встроить, в то время как HotSpot может вставлять все, что захочет.
  • В отличие от C, Java позволяет использовать оператор % также для чисел с плавающей запятой. Это может быть отображено на более эффективный машинный код.

Пожалуйста, не стесняйтесь разбирать код и создавать PR с надлежащим объяснением. Также возможен дамп сборки, работающей на JVM:

https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly

Ускорение версии C

Мой пример доводит вещи до абсурда по какой-то причине. Конечно, можно превзойти Java-версию, лучше управляя памятью в C. Но это означало бы встраивание дополнительных алгоритмов управления памятью в мой исходный код, поэтому я бы не стал больше называть его «эквивалентным» в алгоритмическом смысле, потому что выделение памяти, и освобождение его неявно или явно, является важной частью этого алгоритма.

Говоря это, я получил удивительные отзывы, показывающие мне, как добиться чрезвычайно эффективного управления памятью в C, например, в билете #1, и я благодарен за этот вклад и возможность учиться. Поэтому я хотел бы включить также дополнительную версию этого алгоритма в C, но с более эффективным управлением памятью, а также с учетом переменного размера структур данных. К сожалению, мой ограниченный опыт работы с C не позволяет мне на данный момент написать его самостоятельно. :( Если вы готовы принять этот вызов, пожалуйста, внесите свой вклад в этот проект.

И вот моя гипотеза:

Определенные классы алгоритмов могут повысить производительность только благодаря тому, что они выражены на языке, предполагающем автоматическое управление памятью.

Мой опыт написания сложных распределенных систем, а также моя интуиция подсказывает мне, что эти алгоритмы довольно распространены, и в то же время у меня есть ощущение, что эти случаи редко освещаются в микробенчмарках, сравнивающих скорость кода, написанного и скомпилированного на разных языках. . Если и есть что-то минимальное, чего я хочу добиться с помощью этого эксперимента, так это убедить себя и других, всегда подвергать сомнению определенные догмы современной разработки программного обеспечения и обоснованность определенных аргументов в данном контексте. Пожалуйста, проверьте вопрос #2 как образец того, о чем я говорю.

Имеет ли это какое-то практическое значение?

Это не так, за исключением того, что с методологической точки зрения кажется, что некоторые утверждения с обобщенными кванторами фальсифицируются. Таким образом, это становится чем-то вроде:

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

«всегда» становится «обычно», а «обычно» подразумевает, что с этого момента мы должны скорее перепроверять достоверность для каждого случая, чем делать категоричные заявления.

Здравый смысл микробенчмарков обычно показывает, что JVM примерно на 10-20% медленнее, чем эквивалентный оптимизированный нативный код, с большими выбросами в пользу нативного кода. Мой простейший микробенчмарк с almost pseudo random показывает нечто противоположное, но я бы не стал делать из него поспешных выводов.

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

В распределенных системах, поддерживающих динамические интернет-сервисы, которые мы используем, на производительность ЦП обычно влияет пропускная способность ввода-вывода. Общее улучшение производительности произойдет не за счет того, что наш код стал быстрее, а за счет того, что мы ждем лучшего, с меньшей конкуренцией, при параллельном сборе мусора и при этом не допуская перехода всей системы в пробуксовывающее состояние. » с помощью таких инструментов, как автоматические выключатели. Если ваш код представляет собой типичный динамический веб-стек с данными, извлеченными из базы данных, возможно, переданными в потоковом режиме и преобразованными в JSON на лету, каждый запрос обычно включает в себя множество новых экземпляров данных непредсказуемого размера в конвейере, которые немедленно отбрасываются в конце. запроса. Цель состоит в том, чтобы свести к минимуму время отклика, и виртуальные машины, похоже, в значительной степени способствуют этому.

Мифы и городские легенды современных вычислений :)

Просто повторим мифы, связанные с виртуальными машинами и автоматическим управлением памятью:

  • код, выполняемый на виртуальной машине, ВСЕГДА медленнее, чем нативный
  • сборка мусора ВСЕГДА вредит производительности
  • сборка мусора вызывает «остановить мир»

Ничто из этого не похоже на правду в наши дни:

  • кажется, что код, выполняемый на виртуальной машине, может быть на самом деле вполне оптимальным благодаря технологиям, таким как HotSpot, что показывает даже мой самый простой тест.
  • сборка мусора на самом деле может значительно улучшить производительность обычных алгоритмов
  • на JVM GC в основном происходит как параллельная операция в наши дни

Должен ли я сейчас переписать весь свой код на Java?

Точно нет!!! Производительность — не единственная причина, по которой мы выбираем данный язык. Когда я начал программировать в JDK 1.0.2 (первый стабильный выпуск), он был в 20 раз медленнее, чем нативный код, но код Java, который я скомпилировал в 1997 году, по-прежнему работает на новейшей JVM Java 15. Не могу сказать. то же самое о коде того времени, написанном на Паскале, Ассемблере, C, C++. Обещание Напиши один раз, работай везде, данное мне легендарной Sun Microsystems, было выполнено, в то время как вся среда выполнения и набор инструментов стали открытым исходным кодом. Это настоящая сверхспособность Java, которой я хочу отдать должное — она помогала мне в создании сложных программных систем в течение многих лет со скоростью отличной цепочки инструментов удаленных отладчиков, статистических профилировщиков и инкрементных компиляторов, присущих дизайну языка от не менее легендарный Джеймс Гослинг.

Но я также хочу воздать должное языку C, который лежит в основе ядра Linux — операционной системы, которую мы используем каждый день, даже если не осознаем этого. Это может быть наш Android-телефон или планшет, маршрутизатор и все серверы на пути передачи сигнала от одного человеческого мозга к другому. Даже сам git, инструмент управления исходным кодом этого проекта, написан на C. И все благодаря харизме одного человека — Линуса Торвальдса.

В последнее время я редко программирую на Java, скорее на GLSL, JavaScript, HTML, CSS и Kotlin, последний из которых все еще обычно работает на JVM, хотя с JavaScript и нативным кодом в качестве возможных целей компиляции. Моя IDE также работает на JVM. Иногда я транспилирую Java в JavaScript. Иногда я транспилирую JavaScript в JavaScript. Есть множество других возможных причин, по которым вам не следует использовать Java:

  • Вы владеете другим языком.
  • Вы предпочитаете чистое функциональное программирование.
  • Решения на основе JVM, как правило, требуют большего объема памяти, что не позволяет использовать их во многих встроенных системах.
  • Для кода, в основном зависящего от GPU, приростом производительности CPU можно пренебречь.
  • и Т. Д.

Но если для вашего решения требуется кластер из 100 серверов за балансировщиком нагрузки, то, возможно, вы можете улучшить среднее время отклика со 100 мс до 50 мс на том же виртуальном оборудовании, безопасно отключив половину этих машин? Это может сократить расходы на центр обработки данных Amazon, чтобы нанять еще 2 или 3 разработчиков :)

Я делал это для нескольких организаций в прошлом, всегда улучшая производительность стека на порядок.

Я не большой поклонник микротестов и языковых сравнений, которые часто предвзяты и вводят в заблуждение без контекста, поэтому подпитывают «святые крестовые походы» и «соревнования по генитальным измерениям». Но я прирожденный бунтарь, всегда стремящийся сравнить миф с реальностью. И на самом деле вы часто будете слышать «аргументы из выступления», которые так же часто не имеют отношения к контексту, в котором они выражены. Язык — это всего лишь инструмент. Разговорное часто возлагается на алтарь национальной идеологии, а компьютерное часто становится фетишем нашей идиосинкразии, которую мы навязываем другим. Мы можем сделать лучше. Я пишу «мы», потому что, очевидно, я тоже не свободен от этих тенденций. :)

Из моего опыта руководства блестящими командами разработчиков программного обеспечения я понял, что действительное качественное изменение производительности происходит не от конкретной технологии, а скорее от изменения парадигмы в архитектуре всей системы. Такие технологии, как JVM, могут быть инструментом улучшения, но ими также можно ужасно злоупотреблять.

Результаты теста

Вот результаты тестов моей машины:

$ time ./build/c/javaalmost pseudo randomtimes_faster_than_c 
node count: 1079
checksum: 410502150
real    1m17,218s
user    1m17,210s
sys     0m0,004s
$ time java -cp build/classes/java/main com.xemantic.test.howfast.Java2TimesFasterThanC
node count: 1079
checksum: 410502150
real    0m23,768s
user    0m24,515s
sys     0m0,731s

Будущие исследования

Я хотел бы протестировать эквивалентный код с некоторыми другими языками:

  • Go
  • Ржавчина
  • C#
  • Котлин на JVM
  • JavaScript на узле и в браузере
  • Kotlin транспилируется в JS также на узле и в браузере.

Приветствуется любой вклад в проект java-2-time-faster-than-c GitHub.