Pull to refresh

Comments 92

Прошу прощения, конечно, но зачем это надо?
Ускорение работы при выполнении некоторых операций, выполнение некоторых машинно-зависимых операций, выполнение операций, недоступных для ЯВУ — вот они, причины.
То есть вы всерьёз планируете превзойти современный оптимизирующий С/C++ компилятор? Должно быть вы очень хорошо знаете ассемблер.
В некоторых случаях, это таки помогает. Даже если человек не очень хорошо знает ассемблер.
Яркий пример — x264, в предыдущем Google Summer of Code новички написали несколько патчей, переписывая функции на ассемблер, что ускорило работу.
Да и сами создатели x264 ругаются на компиляторы (в частности, gcc), которые плохо оптимизируют некоторые вещи.
Хорошо, может быть.
Но, тем не менее, мне кажется, что в условиях коммерческой разработки софта, большее значение имеет скорость разработки конечного продукта и стоимость его дальнейшего сопровождения, нежели пара десятков сэкономленных тактов. Совсем уж специфические ситуации и задачи не рассматриваем, где как-раз требуется пресловутая микро-оптимизация.
Почему же не рассматриваем? Вроде как только для этого и нужно.
Возможных применений вижу два:
1. Высокая производительность участка кода (например, специфичное шифрование/хэширование)
2. Низкоуровневое общение с железом (особенно не на PC)
По поводу пункта №1 есть сомнения, мне почему то помнится, что сам факт обращения к JNI является весьма ресурсоёмкой операцией, так что выносить туда небольшие участки будет невыгодно. А если выносить что-то большое, то вообще какой смысл в Java приложении?
Когда-то на смартфоне с Android замерял скорострельность вызовов JNI. Не помню, был там JIT или нет, но порядка миллиона вызовов в секунду стабильно делалось. А ресурсоёмкость вызова не столь велика, как кажется — ведь, насколько я помню, даже никаких локов не задействуется во время JNI-вызова.
Всё таки dalvik не совсем JVM, так что не очень корректно сравнивать.
А оверхед JNI возникает из-за того, что используются разные области памяти, между которыми нужно переносить данные.
«в условиях коммерческой разработки софта, большее значение имеет скорость разработки конечного продукта и стоимость его дальнейшего сопровождения, нежели пара десятков сэкономленных тактов.»

+100
Расскажите это авторам тех самых видеокодеков, прозрачных шифровальщиков трафика (под VPN давно видели последний раз?), а ещё Кармаку — он в своё время Fast Inverse Square Root, наверное, только для удобства сопровождения использовал вместо 1.0 / sqrt(x).
Да, есть еще софт, управляющий крылатыми ракетами.

Но в большинстве областей разработки важнее время разработки и стоимость труда программистов
А я с этим и не спорил, между прочим. PATOGEN написал:
Ускорение работы при выполнении некоторых операций, выполнение некоторых машинно-зависимых операций, выполнение операций, недоступных для ЯВУ — вот они, причины.

Я всего лишь примеры привёл, что не всегда "пара десятков сэкономленных тактов" того не стоит.
«вы всерьёз планируете превзойти современный оптимизирующий С/C++ компилятор» это уже становится нормой размышлений, как будто люди зашли в тупик своего развития и производить что то лучшее могут только машины. Печаль.
Компиляторы, по-вашему, тоже машины пишут?
Нет, тут дело в другом. Тут дело в ошибочной уверенности, что «команда специалистов-разработчиков компилятора — идиоты, я в одиночку смогу лучше».
А вообще, холиваров «компилятор vs ассемблер» навалом, предлагаю не начинать ещё один и каждому остаться при своём мнении. :)
Я много раз попадал в ситуации, когда хитрая перестановка команд в C++ коде и ручной constant propagation ускорял код на десятки процентов, притом что gcc 4.4.1 с O3 и прочими жестокими флагами и MSVCC с настройкой на аггресивную оптимизацию не могли ничего сделать. Само собой, код вычислительный, а не т.н. «бизнес-логика».
Вот и я о том же, печально что сейчас масса разработчиков уверена в магии компиляторов на столько, что мысли о возможности сделать лучше отсекаются в зачаточном состоянии.
Просто не нужно думать, что компилятор сделает всю работу за программиста/проектировщика. Его удел — оптимизировать тонкие места с учётом машинной архитектуры, а не, видя пузырьковую сортировку, бросаться менять её на quicksort. А это значит, что думать во время написания кода таки надо.
Интересная статья, спасибо.
А указано с какими настройками компилился код? Правильно настроенный компилятор и дефолтные настройки — это разные оптимизации.
З.Ы. Я полагаю что на вопрос человек против компилятора нет однозначного ответа.
Я смотрю, комментарии вы не читали.
В нашем игровом проекте, есть конкретная задача, считающая бой юнитов.
Основная проблема — индивидуальный просчет атаки и защиты каждого юнита (упрощать алгоритм не хотелось)
При просчете боя в миллионы юнитов, проблема выделения памяти и элементарных математических вычислений стала критичной (несколько часов — один бой). После написания библиотеки на ассемблере, средний бой считается доли секунды, крупные — несколько секунд.
Си, как и любой другой язык высокого уровня, достаточно много времени тратит на выделение памяти под хранение переменных.
При просчете боя в миллионы юнитов, проблема выделения памяти и элементарных математических вычислений стала критичной (несколько часов — один бой). После написания библиотеки на ассемблере, средний бой считается доли секунды, крупные — несколько секунд.

несколько часов -> несколько секунд

То есть вы написали код, минимум в полторы-две тысячи раз эффективней, чем тот, что сгенерировал компилятор?
Не понимаю, почему вас это удивляет. Ассемблер работает с памятью быстрее чем откомпилированный код, ибо текст на ассемблере можно оптимизировать буквально по тактам.
Задача стояла достаточно простая — большая куча данных и 4 элементарных математических действий.
Нет, ну я понял бы прирост в скорости в 300-500%, но в 200000% — это да, это меня удивляет.
Ну, возможно, разница с откомпилированным Си была бы меньше.
Но ява это же байт-код, а не машинный код, а писать алгоритм на разных языках, если предварительно было видно, что мы можно это за пару дней сделать на асме не было смысла, лучше сразу.

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

P.S. Просьба минусовать с аргументами.
UFO just landed and posted this here
UFO just landed and posted this here
Я же явно указал, что основная проблема — объем данных, то есть память, а операции простейшие, зачем цепляться к другим словам?

По сути, основная доля времени уходит на:
1. В бою может участвовать несколько миллионов юнитов по обе стороны баррикады, у каждого юнита есть несколько параметров, которые меняются в процессе боя (щиты, броня, статус мертв/жив), на это нужно выделить память и быстро к ней обращаться.
3. Случайный выбор очередной жертвы при каждом выстреле, включая грамотный выбор очередной жертвы, если текущая уже мертва, и сортировка оставшихся живых.

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

Текущие эмуляторы просчитывают бои гораздо дольше нашего (несколько минут то, что у нас секунды), в результате, учитывая случайную составляющую — для прогноза результата боя, было бы хорошо его прогнать раз 10-100 и затем взять уже среднее, на что уходят часы (прогнозируют обычно для самых крупных сражений).

P.S. Еще раз хочу сказать, что я просот привел пример удачного взаимодействия ассемблерной вставки для веб-проекта на практике. Спорить о том, что круче — компилятор или ассемблер я не хочу.
>> Основная проблема — индивидуальный просчет атаки и защиты каждого юнита (упрощать алгоритм не хотелось)

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

Простая игра с типами данных (boxing-unboxing, ArrayList-LinkedList, HashMap — хеш для примитивных типов из того же colt) может дать хороший прирост, но без изменения самой логики (не обязательно упрощать, достаточно отбрасывать заранее ненужные рассчеты) я просто НЕ ВЕРЮ, что вы сделали это:

>> (несколько часов — один бой). После написания библиотеки на ассемблере, средний бой считается доли секунды, крупные — несколько секунд.

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

Я знаю что у явы бывают узкие места, но тут кто-то откровенно врет.

p.s. а можно описание этого алгоритма? просто для саморазвития
хотя, извиняюсь что не увидел этого:

>> Задача стояла достаточно простая — большая куча данных и 4 элементарных математических действий.

В этом случае да, mmx и sse вам в руки, но уж как-то смутило про грамотный выбор, сортировку и индивидуальный подход к каждому юниту
Основная суть — на ассемблере вместо работы с типами данных, просто выделяется область памяти и с юнитами работали просто по смещению адреса. Это действительно быстрее, чем работа с хэшами и массивами.

После окончания одного раунда, делаем дефрагментацию, оставляя только живых юнитов для более адекватного рандомайза — если рандомайзер выпадает на уже убитого в раунде юнита, вместо вычисления еще одного рандома (который опять может попасть на убитого), просто выбирается следующий юнит. Если убито 10 юнитов, следующих подряд, вероятность попадения в 11 юнит значительно увеличивается, но дефрагментировать чаще — большой расход времени.

В результате — в цикле у нас остались однотипные операции, которые можно оптимизировать буквально потактово, увеличив эффективность общего цикла.

Вот поискал по нашему форуму… Официальный лог боя — расчет длился около 20 секунд на сервере (двухпроцессорный intel)
Имеющийся симулятор1 на Си, написанным игроком, (домашний целерон) считает от 10 до 20 минут (много рандома).
Имеющийся симулятор2 на яве, написанный игроком, (домашний целерон) завис.

В бою участвовало примерно 2.5 миллиона юнитов.

P.S. Еще раз хочу сказать, что совершенно не хочу сказать что у нас какое-то уникальное достижение, просто удачно повезло с самой задачей. При открытии проекта существование подобного размера армий вообще не предполагалось, но не рассчитали с экономикой…
> Основная суть — на ассемблере вместо работы с типами данных, просто
> выделяется область памяти и с юнитами работали просто по смещению
> адреса. Это действительно быстрее, чем работа с хэшами и массивами.

что мешает сделать тоже самое на той же java?
1) То, что это уже сделано на ассемблере.
2) видимо на ассемблере разработчик шарил больше, чем на яве.

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

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

P.S. Если у вас есть свободное время, я могу кинуть детальный алгоритм боевки, напишете его на java, сравним скорость. Не то, чтобы сейчас было важно что оно будет считаться считается 5 секунд или 6, но все же я не думаю, что ява сможет обогнать ассемблер в конкретно этой задаче.
Я вам не предлагаю ничего переделывать, раз оно УЖЕ работает. Но вот если бы я только собирался переписать некоторый участок кода я бы хорошо подумал прежде чем использовать ассемблер. Потому что поддерживать ассемблерный код гораздо сложнее.

Насчет свободного времени не уверен — только между компиляцией, но было бы интересно, только на с++: с java я знаком слишком поверхностно для того, чтобы заниматься оптимизаторством.
Если убито 10 юнитов, следующих подряд, вероятность попадения в 11 юнит значительно увеличивается
Отнюдь, вероятность остаётся 50%… Ну при прочих равных. Если вы знаете, что у вас 20 из ста убито, и уже 10 нашли, тогда конечно увеличивается.
есть 10 юнитов, убиты 4,5,6,7,8.
Сравните вероятность попадания в 3 юнит и в 9-й, учитывая что попадания в мертвые 4,5,6,7,8 также приведут к попаданию в 9-й.

Более красивый подход, который не злоупотребляет ресурсами, как дефрагментацию после окончания каждого раунда, пока не нашли…

Но всегда готовы прислушаться :)
Ай, я не до конца понял условие. Тут вы конечно правы.
Весь выигрышь в скорости съест очень высокий overhead JNI.
Например, можно выполнить какие-нибудь SSE-инструкции для оптимизации обработки данных.
А как же переносимость на другие платформы? А если о ней не идёт речи, тогда зачем Java?
А для остальных платформ использовать условную компиляцию/линковку, тогда будет выполняться обычный байткод.
Ну это всё как-то через ж… неправильно, что ли.
В вычислительной задаче переносимость часто может быть не нужна — код работает на единственной машине или одном виде машин.
В вычислительной задаче будет довольно-таки странно использовать Java. Не забывайте также об оверхеде на JNI.
Сложно представить какой-то другой тип задач, в котором есть потребность в JNI. Все-таки Java иногда используется для вычислений. (Помнится кто-то на хабре рассказывал про портирование вычислительного C++-кода на Яву!)

Если 90% процентов процессорного времени приходится как раз на нативные вставки, JNIшный оверхед перестает играть роль.
По-моему, тут был чисто академический интерес. Особой смысловой нагрузки этот пример не несет.

А можете замеры времени провести?
Ассемблерную вставку можно использовать как кросс-платформенный плагин к php/apache/etc
Ну и не всегда необходима переносимость в виде «взял и перенес как есть». А с небольшими правками, правильно написанную библиотеку можно скомпилировать под нужную платформу.
Сейчас есть популярные серверные платформы, работающие не на x86/x64?
А кто сказал, что речь идёт лишь о серверных платформах?
Оптимизация узких мест? Хотя, наверное, C/C++ будет уместнее — по слухам они оптимизируют во многом лучше «гуру» ассемблера.
Получается, что самое выгодное — линковка с C[++] со вставками ассемблера. Как собственно и без явы.
Материал из данной статьи очень поможет, при подключении нативных библиотек в android приложениях.
UFO just landed and posted this here
Скорее новым железом :)
Инструкция PCMPESTRI позволяет strcmp-нуть 16-символьные строки за 4 (!!!) такта. А GCC, и уж тем более явовский JIT ее и подобные вообще не используют.
UFO just landed and posted this here
UFO just landed and posted this here
Мне кажется, они не используют их для того, чтобы откомпилированный код можно было запустить на отличных от компилирующей машинах (не везде ведь есть SSE4). Вот, например, в Intel Compiler есть отдельные опции «Insert Intel-optimized code path» и «Perform compiling host specific optimizations» (за дословность не ручаюсь — нет компилятора под рукой), которые такими вещами не пренебрегают (сам видел, читая assembler output).
UFO just landed and posted this here
Ну в теории JIT, конечно, более мощная штука в плане оптимизаций под целевую машину. Но на практике для генерации действительно оптимизированного кода требуется слишком много времени и слишком сложный анализ, реализация логики которого неприлично увеличит объём программы-JIT компилятора.
Мне очень нравится схема, по которой работает JIT в платформе .NET — все программы JIT-компилируются не при первом запуске, а при установке на целевую машину (в идеале). И как раз в ngen.exe можно встроить очень мощные механизмы оптимизации и анализа кода, что при включении всего этого хозяйства в состав ОС (лишние пару сотен мегабайт вряд ли сильно повлияют на размер современной Windows =)) даст практически ту самую идеальную заточенность, о которой Вы говорите.
Кстати, теоретически можно создать объектные файлы с жёсткими оптимизациями под различные типы процессоров, а также «общую» версию. Скажем, под Intel и AMD компилируем своими компиляторами, а для общего случая — gcc. Затем либо выносим код в разные DLL с выбором нужной в рантайме, либо просто оформляем как различные секции в рамках одного бинарника (естественно, этим должна заниматься специальная программа или скрипт, а не человек) — всё равно большую часть объёма современных десктопных и смартфонных программ занимает далеко не программный код =). Кстати, такой механизм вовсю используется в играх под Android, использующих нативный код: просто в ресурсы кладутся версии нативных библиотек под разные версии ARM, а нужную выбирает JVM в момент загрузки.
UFO just landed and posted this here
Нет, я не занимаюсь разработкой JIT-компиляторов. А Вы, видимо, никогда в глаза не видели возможностей того же Intel Compiler и не пытались выжать из кода заветные проценты производительности ручной перестановкой инструкций, разворотом циклов и profile-guided optimization — ведь «JIT всё сделает, Intel — в помойку».
Привожу конкретный пример: один и тот же код софтварного рендеринга сцены из 27 текстурированных кубов в разрешении 480*800 (могу код скинуть, коль не верите на слово) имел разницу времени выполнения примерно в 2.5-3 раза между JIT-выполнением (с прогревом, разумеется) и банальным компилированием g++ с O3. Устройство — Samsung Galaxy S.
Если не верите тому, что я говорю (а я думаю, что не верите — правы-то, очевидно, Вы), предлагаю Вам, например, написать какое-нибудь умножение матриц на Java (или любой другой, интенсивно работающий с памятью, код) и обогнать хотя бы приблизиться по времени выполнения к написанному мной на коленке коду, скомпилированному g++. Готовы?
О ужас, как же я мог подумать, что какой-то там g++ может даже рядом стать по качеству оптимизирования числодробилки к самой HotSpot?!
UFO just landed and posted this here
Возможно данный вариант подойдет тем, у кого есть написанный модуль на ассемблере, выполняющий какие-либо вычисления, который жуть как не охота реализовывать на другом языке, и котороый хочется использовать к примеру в веб-проекте.
Да… Идет активная попытка вернуть вилки, ножи и веревки в Java. :-)

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

Нельзя ли сделать отдельный сервис и общайтесь с ним как нибудь. По крайней мере не будете зависеть от заморочек виртуальной машины, она ж не только ваш код выполняет, ей еще мусор собирать и другими делами заниматься.
UFO just landed and posted this here
ИМХО, эта возможность нивелирует идею явы о кроссплатформенности.
JNI нивелирует это по определению.
Вовсе нет. При разумном использовании, разумеется.
Ну ежу понятно, что

printf("%d", 123)


на всех платформах будет работать одинаково.

Я лично использую jni для работы с криптографией, и никакой кроссплатформенности там на С++ не будет, потому что апи у всех систем для этих дел свои.
UFO just landed and posted this here
«10 способов писать непереносимый и привязанный к одной платформе код на Java.»
Ну что? Следующая статья — как собрать в один екзешникобьектник из gcc и masm?
UFO just landed and posted this here
Ну почему же, если эту конструкцию запустить под Linux:
System.loadLibrary("mydll");
то будет загружена библиотека libmydll.so
Таким образом, если в пакете установщика иметь одновременно
mydll.dll и libmydll.so то будет вполне себе даже кроссплатформенно.
С таким успехом можно написать даже статью «Взаимодействие 1С и ассемблера»))
Я думал, честно говоря, тут что-то необычное, а не просто подключение внешней библиотеки…
UFO just landed and posted this here
Ваши источники врут. Я год назад демку писал под Андроид, и JNI просто летал, делая около миллиона чистых вызовов в секунду.
UFO just landed and posted this here
По-моему, там нечего особо оптимизировать — нужно всего лишь передать указатель на контекст JVM (он и так в Dalvik нативный — контекст, в смысле) и дёрнуть [возможно, виртуальный] метод.
UFO just landed and posted this here
Вряд ли это возможно, насколько я понимаю. Ведь тогда есть риск здорово облажаться с reflection. Ведь его средствами можно получить байткод метода, и если в него JIT'ом будет подмешиваться что-то ещё, то можно, например, получить рассинхронизацию данных (если JIT заинлайнит нативный вызов после того, как через reflection был колучен его байткод).
Далее, связывание native методов через JNI — позднее, а это значит, что метод может быть обработан JIT-ом до реальной загрузки соответствующей нативной библиотеки (простейший пример: библиотека загружается в теле этой же функции), а если «учить» JIT перекомпилировать методы, то это здорово скажется как на его ресурсоёмкости, так и на производительности.
Третий мой аргумент против инлайнинга — если у вас настолько часто используется нативная функция внутри вашей (и это, разумеется, является «бутылочным горлышком»), может, стоит просто перенести саму функцию в натив?
UFO just landed and posted this here
1) Например, вот. Или про Javassist гляньте. Странно, оба работаем в «Яндексе», но поиском Вы почему-то пользоваться так и не научились. Беда.
2) Читайте внимательно (особенно перед тем, как оголтело минусовать):
… можно получить байткод метода, и если в него JIT'ом будет подмешиваться что-то ещё, то можно, например, получить рассинхронизацию данных (если JIT заинлайнит нативный вызов после того, как через reflection был колучен его байткод).

Код обычный, джавовский, в котором есть вызов нативного метода.
3) Я говорю про любой JIT, который проводит компиляцию кода в рантайме (ещё раз прочитайте второй абзац моего предыдущего сообщения — там не зря про производительность написано)
UFO just landed and posted this here
Sign up to leave a comment.

Articles