div.main {margin-left: 20pt; margin-right: 20pt}
Ява медленно работает
Миф о том, что программы написанные на Java медленно работают
когда-то был действительностью. Изначально виртуальные машины, на которых
выполнялся байт-код Java, были несовершенны и , к тому же, язык Java и
виртуальная машина разрабатывались не для компьютеров, в привычном их понимании,
а как некая универсальная среда для различных устройств, где скорость работы
программ была не критична, да и объемы данных были совсем не те. Разница в
скорости выполнения одинаковых задач между обычным компилируемым кодом и
интерпретируемым кодом Java могла достигать сотен раз. Да-да, Java код
интерпретируемый. Сегодня же скорость работы байт-кода на современных
виртуальных машинах уступает лучшим компиляторам машинных кодов всего лишь на
несколько десятков процентов, а в некоторых случаях, даже и превосходит их! Т.е.
выполнение программ на виртуальных машинах в плотную приблизилось по скорости по
выполнению программ, специально скомпилированных в машинные коды. "Такого не
может быть!" - Воскликнут одни - "Интерпретируемый код, выполняемый виртуальной
машиной, никогда не обгонит компилируемый код, выполняемый напрямую
процессором!". И они будут правы. Но с тех пор, когда появилась Java, утекло уже
много воды. И Java байт-код теперь уже не интерпретируется виртуальной машиной,
а компилируется, да еще и оптимизируется, причем динамически!
Что бы разобраться в фокусе, почему байт-код может работать очень
быстро, а иногда быстрее, чем нативный (код для целевой платформы),
скомпилированный код, следует для начала разобраться в принципах того, как
вообще могут выполняться программы. Кто знаком с этими принципами, может
пропустить следующие несколько абзацев.
Принципы выполнения кодов программ.
Что бы лучше понять, каким образом может выполнятся программный код
давайте отвлечемся от программ, виртуальных машин и прочих информационных
технологий.
Представим себе нескольких водителей, от начинающего до
профессионала. Первый водитель будет представлять собой "чайника", только-то
получившего "права". Он будет действовать по принципам интерпретатора. Второй
водитель уже более опытный, будет руководствоваться принципами статического
компилятора. Третий, как и первый новичок, но очень способный и быстро
обучающийся. Он будет действовать по принципу динамического компилятора с
оптимизатором. А вот четвертый уже настоящий профессионал и будет он
руководствоваться принципами гибридного компилятора.
Задача всех водителей, каждое утро, каждый рабочий день, добираться
из своего дома, расположенного на окраине крупного города, в центр этого
крупного города. На работу. Представьте себе крупный город, например Москву,
утро рабочего дня, час пик. Сотни тысяч автомобилей спешат по делам их
водителей. Большое количество улиц, шоссе, все дороги переплетены, как нитки в
клубке.
Если следовать по методике интерпретации кода, т.е. пошагового, по
одной команде, его распознавания и выполнения, то у первого водителя есть только
направление на ту точку в центре, куда нужно попасть, схемы улиц города у него
нет, а про пробки и заторы он и не подозревает. Водитель выезжает на улицу и
пытается проехать кратчайшим путем к намеченному месту. Перед каждым поворотом,
на каждом перекрестке он должен принять решение, куда же ему повернуть и нужно
ли ему поворачивать. Он не знает и даже не подозревает куда ведет выбранная
улица и что можно проехать по соседней и, причем гораздо быстрее. Так медленно,
но верно, водитель добирается до места назначения. День за днем, каждый раз по
одному и тому же пути. Примерно так и работают интерпретаторы.
Следующий водитель, уже купил карту города и имеет общие
представления о том, как, когда и где могут появляться проблемы с движением.
Согласно карте и предположениям, он заранее, дома, один раз выбирает себе
маршрут и следует строго по нему. И так каждый день. Не изменяя маршрута и не
пересматривая его. Так действуют статические компиляторы, компилирующие код до
стадии выполнения.
Третий водитель карты не имеет, о пробках и заторах не подозревает.
Первоначально он действует так же, как и первый (интерпретирующий) водитель. Он
проехал так первый день, второй день, на третий день, учтя опыт предыдущих дней,
водитель выбирает новый, более оптимальный маршрут. На следующий день водитель
опять анализирует предыдущий опыт и опять выбирает наиболее оптимальный маршрут.
Со временем маршрут будет наиболее оптимальным. Такое поведение характерно для
большинства динамических компиляторов с оптимизацией.
Четвертый, опытный, водитель имеет и карту, и общие предположения о
причинах и принципах возникновения пробок. Он заранее выбрал себе маршрут
движения. После нескольких дней поездок, водитель может изменить маршрут в
зависимости от дорожной обстановки. Такую методику используют гибридные
компиляторы. Т.е. код компилируется до выполнения, а во время выполнения он
может перекомпилироваться с оптимизацией. При этом не теряется драгоценное время
при первых поездках (когда маршрут вообще не оптимизирован) и не теряется время,
если изменилось окружение (длительный ремонт дороги приводит к изменению
маршрута, в отличие от статических компиляторов).
И вот в один прекрасный день в городе произошло ЧП. Произошла
авария на трубопроводе, который случайно проложили под дорогой. Две трети дороги
перегородили, движение автомобилей значительно замедлилось, время в пути
значительно увеличилось. Как в таких случаях поступят различные водители. Первый
водитель (интерпретирующий), второй (статически компилирующий) будут упорно
продолжать стоять в пробке и не отклонятся от маршрута. Хотя первый водитель
может в нее и не попасть, ведь он может постоянно ездить и по другому, пути, не
по кратчайшему. Третий (динамически компилирующий) проедет раз, другой, а потом
уже будет искать объездные пути, пока не найдет наиболее оптимальный маршрут для
сложившейся ситуации, а когда дорогу приведут в порядок он вернется опять на
прежний маршрут (если он конечно же будет лучше объездного). Так же поступит и
четвертый водитель (и компилирующий и оптимизирующий динамически).
Что же лучше?
Так какая же из схем наиболее оптимальна для массового
использования? Интерпретирующая схема наиболее проста в реализации. Не нужно
применять никаких ухищрений. Однако при этом время, затрачиваемое на исполнение
программы, весьма значительно. Ведь нужно при каждом выполнении операции заново
ее разбирать, интерпретировать, превращать в коды понятные конкретному
процессору и только после этого данная инструкция исполнится. Схема простая, но
эффективная только в простых и не нагруженных системах. Например, она очень
хорошо подходит для скриптовых или командных языков, где не важна скорость
выполнения и выполняются они не так часто. Именно так и работали первые
виртуальны машины Java.
Схема со статической компиляцией, когда исходный код заранее
компилируется в двоичный код целевой системы, хороша тем, что можно заранее
произвести оптимизацию кодов для целевой платформы. Причем время оптимизации не
ограничено, ведь она производится один раз и до того, как программа будет
выполняться. Этот подход широко применяется в современных компиляторах языка С,
а так же других языков. Данное преимущество является, как ни странно,
одновременно и недостатком. Как же такое может произойти? А вся соль заключается
в том, что оптимизация производится только на основе неких предположений о
будущей среде исполнения. Нет, можно конечно компилировать с оптимизацией под
конкретный объем памяти, конкретную версию операционной системы, конкретный
набор драйверов устройств, конкретную модель процессора. Но тогда и программа
уже будет работать только на одном конкретном компьютере. А так обычно делаются
некие допущения, например, что операционная система будет Windows, версии не
менее 3.11, объем памяти будет не менее 16 Мб, процессор будет совместим с Intel
486. Но ведь даже под такие казалось бы достаточно жесткие ограничения попадает
множество вариантов: операционная система может быть и Windows 95, и Windows 98,
и Windows 98 OSR2, и Windows NT и в конце концов Windows 2000, а объемы памяти
вообще могут различаться в разы, это может быть и 32, и 64 и 512 Мб, с
процессорами такого разнообразия нет, но все-таки это может быть и процессор
Intel 486, и AMD DX4-100, и Cyrix M2 и т.д. Так что такая оптимизация носит
некий усредненный характер. Нет, ну можно конечно, создать компилятор, который
будет учитывать все возможные вариации, и генерировать несколько вариантов
машинного кода с логическими их объединениями. Но какой тогда при этом будет его
размер?
Вариант с динамической компиляцией, т.е. с компиляцией во время
выполнения программы, это когда некий код компилируется, основываясь на текущей
среде окружения в момент запуска. Т.е. не нужно создавать несколько вариантов
кода, а только один, не нужно создавать некую усредненную оптимизацию, а можно
оптимизировать по максимуму основываясь на текущих, на действительных, а не
предполагаемых компонентах системы. Это что же, компилятору нужно знать все типы
процессоров, все драйвера устройств и т.д.? Какой же у него тогда будет размер?
Нет, если пойти другим путем и основываться на данных полученных на совокупности
параметров, полученных после нескольких прогонов блоков кода, то нет нужны
хранить данные о каждом из типов устройств. В результате получается некое
подобие самоподстраевающейся системы, которая со временем сможет создать
оптимальный вариант. Но и в такой схеме есть недостатки. Все преимущества
оптимального варианта будут ощутимы только после прошествия некоторого времени
после запуска программы. Для проведения оптимизации и компиляции все-таки
требуются ресурсы - память и процессорное время. Оптимизация или должна
отрабатывать во время бездействия основной программы, отнимая ресурсы у нее, или
же во время ее простоя, а если простоя не бывает? Таким образом, требуется
разрешить некую дилемму, когда же оптимизировать и компилировать код. Примерно
по такому принципу и работают современные виртуальные машины Java. Одни
компилируют код сразу перед запуском, а потом его перекомпилируют и
оптимизируют, другие же сначала интерпретируют код, одновременно его профилируя,
а уже затем компилируют и оптимизируют.
Что бы не тратилось время на предварительные процедуры по
интерпретации или предварительной компиляции можно осуществлять статическую
оптимизированную компиляцию, а уже затем во время работы производить
динамическую перекомпиляцию с оптимизацией. Такая схема выглядит достаточно
красиво и обещает наибольшую производительность. Но она настолько же сложна,
насколько и красива. Сложна и реализация самой схемы, сложно и внедрение в
существующие системы. К применению совместно с байт-кодом Java можно заметить и
то, что компиляция в целевые коды вообще не возможна, поскольку Java это не
зависящая от процессора, операционной системы и т.д. платформа. Все, что
существует для байт-кода это только лишь виртуальная машина. Можно конечно
передавать дополнительные сведения вместе с байт-кодами, но в стандарте этого
нет. Кроме этого могут возникнуть проблемы с безопасностью, а безопасность, это
одна из сильных сторон Java.
Как все это стыкуется с Java
Принцип Java - выполнение платформонезависимого кода на
платформозависимой виртуальной машине. Значит, что бы код выполнялся быстро, его
должна быстро выполнять виртуальная машина. Первоначально виртуальные машины
Java были интерпретирующими байт-код, т.е. из файла выбирался очередной
оператор, он распознавался, интерпретировался, а только затем уже исполнялся.
Отсюда большие временные издержки на эти операции. Как же работают современные
виртуальные машины? Они переводят байт-код в двоичный код платформы, на которой
выполняется виртуальная машина, тем самым осуществляется качественный переход от
интерпретации к компиляции в двоичные коды. К этому же прибавляется еще и
оптимизация полученного кода. Компиляция и оптимизация происходят на стадии
выполнения и основываются на действительных параметрах системы (в отличии от
статически компилируемых и оптимизируемых бинарных кодов). Тем самым получается
сильно оптимизированный двоичный код для целевой платформы. Причем оптимизация
может продолжаться на всей стадии работы программы. Наилучшими результатами
вправе могут гордиться фирмы IBM (www.ibm.com) и Sun (www.sun.com). Их
виртуальные машины на сегодня самые быстрые из распространенных коммерческих
систем. Низкоуровневые тесты показывают, что сих скорость работы не хуже чем
оптимизированных программ на одном из самых быстрых языков программирования С. А
иногда даже и превосходят их. В этих виртуальных машинах используются технологии
IBM JIT (Just In Time - обработка во время выполнения) и HotSpot соответственно.
Обе эти технологии похожи друг на друга и различаются только в деталях. Хотя по
проводимым тестам (а посмотреть на их сборник можно, например, на русском сайте
http://www.javapower.ru/) JIT от IBM
работает несколько быстрее.
Почему же они быстрее?
Ключевое слово здесь - динамическая оптимизация. Т.е. с течением
времени программа сама подстраивается под среду окружения и при этом продолжает
эффективно работать. Естественно, что бы данная техника работала в полную силу,
требуются дополнительные вычислительные ресурсы. Ведь проку от динамической
оптимизации будет очень мало, если из-за нее компьютер будет постоянно
обращаться к виртуальной памяти. Так же не будет толку от простых и быстро
исполняющихся программ, ведь все эти оптимизирующие технологии, просто не успеют
отработать. Так же не следует слишком уж доверять оптимизаторам кода и писать не
эффективные алгоритмы. В таком случае никакого заметного выигрыша ожидать не
стоит.
А как дела обстоят в действительности?
По тестам "скорострельность" Java весьма не плоха, но как же
обстоят дела с реальными приложениями? К сожалению, с реальными пользовательским
приложениями дела обстоят не так уж и хорошо. Причина этому - медленная работа
графических компонент системы. Программисты Java знают, что в языке имеется две
стандартные возможности для работы с пользовательским интерфейсом, это AWT и
Swing. AWT это платформозависимая реализация графического интерфейса
пользователя. Скорость работы большинства реализаций вполне удовлетворительна.
Но количество функций весьма ограничено. В альтернативу AWT разработана
библиотека Swing. Эта библиотека основана полностью на возможностях языка ,
имеет множество функций и платформонезависима. Поэтому скорость ее работы
значительно отличается от скорости работы AWT. Это-то и является основной
причиной медленной скорости работы графического интерфейса пользователя в
пользовательских программах. А реализация платформозависимосй библиотеки
затруднена, поскольку графический интерфейс на разных платформах реализуется
по-разному. Но даже в таких условиях общая скорость системы остается на уровне.
А относительно серверных, или не графических приложений миф о медленности работы
- действительно только лишь миф.
Выводы
Слухи о низкой скорости работы программ Java слишком преувеличены.
В совокупности с современные виртуальными машинами Java скорость их работы
уступает скорости работы нативных оптимизированных приложений всего лишь на
несколько процентов. Но ведь разработчикам виртуальных машин еще есть над, чем
поработать. Пока основное нарекание вызывает медленная скорость работы
пользовательского интерфейса, и это по больше части объяснимо, ведь в
графических оконных средах пользовательского интерфейса используются различные
аппаратные ускорители и т.д. А для Java они пока не доступны.
Владислав Кравченко,
href="mailto:grigorenko@mail.ru">Григорий Григоренко
|