Время выполнения программы java: javatalks.ru / Как узнать время выполнение программы?
Java и C++: тест на быстродействие | Java World
В те времена, когда все виртуальные машины Java представляли собой интерпретаторы байт-кода, эта технология вызывала справедливые нарекания за свою низкую производительность. Но по прошествии некоторого времени на рынке появились хорошие компиляторы just-in-time (JIT), и сегодня тесты показывают, что по быстродействию Java во многих областях практически сравнялась с C++. Наметившиеся тенденции позволяют надеяться на то, что уже в самое ближайшее время Java не уступит в скорости C++.
Как соотносится производительность приложений Java и аналогичных программ на C++, максимально оптимизированных по критерию быстродействия? Думаю, что пользователям было бы интересно ознакомиться как с результатами теоретического сравнения, так и с выводами, сделанными на основе анализа тестовых программ и реальных приложений. Большинство аналитиков, не желая углубляться в хитросплетения сложных технологий, с ходу заявляют, что Java всегда будет уступать в производительности другим языкам по причине многоплатформенности Java. Такие утверждения можно встретить во многих статьях, посвященных Java и сетевым компьютерам.
Мы решили провести самостоятельное исследование и выяснить, в чем Java уступает C++ (специалисты любят противопоставлять именно эти два языка). Были изучены компоненты архитектуры Java и определены сравнительные характеристики быстродействия идентичных программ, написанных на Java и на C++.
Мы готовились к тому, что программы Java будут отставать по всем показателям, хотя и не верили, что C++ окажется в несколько раз быстрее. К большому нашему удивлению, какой-либо разницы в производительности практически не ощущалось. В некоторых случаях скорость приложений Java действительно значительно уступала этому показателю у C++, что, впрочем, легко объясняется наличием строгой модели безопасности и издержками технологии «сборки мусора».
Любое повышение производительности какого-либо языка следует рассматривать в строго определенном контексте. С момента появления Java прошло уже немало времени, и это время не было потрачено впустую. Неслучайно сегодня по уровню быстродействия этогот язык в большинстве случаев сравним с C++ (а C++ славится своей высокой скоростью).
Тесты производительности были разбиты на четыре функциональные группы:
Программное обеспечение: Visual C++ 5.0 и Sun JDK 1.1.5.
В данной статье под платформой понимается комбинация процессора и ОС. Например, платформой можно считать комбинацию ОС Windows NT и процессора Intel, ОС Linux и процессора Intel или же ОС Linux и процессора Digital Alpha.
ЗАГРУЗКА ИСПОЛНЯЕМОГО КОДА
При создании нового приложения разработчики сначала записывают программный код в один или несколько исходных файлов. Компилятор/компоновщик транслирует исходный код в исполняемый. В готовом приложении работает именно исполняемый код. На первом этапе необходимо загрузить исполняемый код в память компьютера.
Загрузка исполняемого кода: Java против C++
Если программа находится на локальном диске, загрузка даже сложного приложения не займет много времени. Если же программа хранится на Web-узле в Internet или в корпоративной сети intranet, размер исполняемого кода может существенно повлиять на производительность. Программы и ресурсы Java занимают значительно меньше места на диске по сравнению с программами C++ и загружаются из Internet и intranet гораздо быстрее. При определении скорости загрузки необходимо учитывать размер исполняемого кода и возможность избирательной загрузки.
Размер исполняемого кода
Исполняемые модули Windows NT, написанные на C++, занимают на диске значительно больше места, чем исполняемые модули Java. На размеры исполняемого кода оказывают влияние три фактора:
Во-первых, двоичный формат исполняемого кода C++ увеличивает его размеры по сравнению с кодом Java почти в два раза.
Во-вторых, технология Java предусматривает активное использование ряда библиотек, содержащих математические и сетевые функции, классы для работы с составными элементами и графикой и так далее. В виртуальные машины Java (JVM) встроена поддержка этих библиотек. В C++ же, напротив, определяются интерфейсы прикладных программ (API), позволяющие разработчику унифицировать доступ к большинству функций. К сожалению, если программист захочет использовать какие-либо специальные функции, не имея доступа к базовому API C++, ему придется самому реализовывать поддержку библиотек для своей программы. Включение в программу библиотечных функций может удвоить или даже утроить размеры конечного кода.
В третьих, в комплект Java входят специальные библиотеки, позволяющие работать с графическими и звуковыми файлами, записанными в сжатом формате. Поддерживаются, в частности, графические форматы Joint Photographic Expert Group (JPEG) и Graphics Interchange Format (GIF), а также аудиоформат AU. Средства разработки программ на C++ для Windows NT поддерживают только несжатые форматы: bitmap (BMP) для изображений и wave (WAV) для аудиофайлов. Сжатие дает возможность на порядок уменьшить размеры графических файлов, и приблизительно в три раза — размеры аудиофайлов. Если вы хотите работать с другими форматами графических и звуковых файлов, можно воспользоваться дополнительными библиотеками классов.
Избирательная загрузка
Загрузчики программ, написанных на C++, перед началом выполнения должны полностью загрузить исполняемый файл. В среде Win32 имеется два способа связи с библиотеками DLL: статический и динамический. Статически связанные DLL (используемые в большинстве случаев) загружаются до начала выполнения программы. В случае динамического связывания библиотеки DLL загружаются по запросу. Однако такой вариант гораздо менее популярен, поскольку написание кода динамической загрузки и обращения к библиотечным функциям требует от разработчика значительных усилий.
Кроме того, не существует способов проверки целостности динамически связанной DLL на этапе выполнения. Это означает, что без аварийного завершения нельзя выяснить, изменялась ли DLL в процессе выполнения программы. Наконец, если программа будет загружаться с удаленного диска, необходимо предварительно скопировать библиотеки на локальный носитель. Реализация такой процедуры в автоматическом режиме потребует дополнительных затрат на программирование.
В свою очередь загрузчик Java способен выборочно загружать классы по мере необходимости. Рассмотрим, к примеру, полнофункциональный текстовый процессор со встроенным тезаурусом, орфографическим корректором и средствами создания и экспорта почты. Реализация данных возможностей обычно приводит к генерации многомегабайтного кода.
Средний пользователь в любой конкретный момент времени использует лишь малую часть потенциала средств разработки. Если программа была написана на C++, перед началом обработки пользователь обязан загрузить файл полностью. В случае, когда программа написана на Java, на первом этапе в памяти размещается только самое необходимое (например, главное окно), а дополнительные модули подгружаются по мере необходимости.
Загрузка исполняемого кода реальных программ
В таблице 1 представлены размеры протестированных программ и их ресурсов. Огромная разница между размерами кода C++ и Java объясняется наличием библиотек, необходимых для выполнения программы C++. Объемы ресурсов отличаются вследствие того, что приложения Java работают со сжатыми файлами (в формате GIF), а программы C++ — с обычными файлами bitmap.
Таблица 1
Имя программы | Размеры программы, Кбайт (C++/Java) | Ресурсы, Кбайт (C++/Java) |
Simple Loop | 46/3,9 | -/- |
Memory Allocation | 34/1,4 | -/- |
Bouncing Globes | 103/21 | 485/13 |
ВЫПОЛНЕНИЕ ПРОГРАММНЫХ ИНТСТРУКЦИЙ
После загрузки исполняемого кода процессор начинает выполнять программные инструкции. При традиционном программировании на C++ исполняемый файл содержит двоичные инструкции процессора выбранной платформы. Для переноса приложения на другую платформу разработчику потребуется создать новый исполняемый файл путем перекомпиляции исходного кода. Кроме того, особенности конкретной платформы каждый раз заставляют вносить определенные изменения в исходный код. Исполняемые же файлы, получаемые на выходе компилятора Java, представляют собой совокупность байт-кодов, которые не могут выполняться процессором без дополнительного преобразования в двоичный код. Это преобразование возлагается на JVM. Виртуальная машина Java может осуществлять преобразование двумя способами: с помощью интерпретатора байт-кода или посредством компилятора just-in-time (JIT).
Выполнение программных инструкций: Java против C++
У программ, написанных на Java, не слишком хорошая репутация. Их производительность заставляет вспомнит о старых JVM, для которых интерпретация байт-кода была единственным способом выполнения программных инструкций.
Интерпретаторы байт-кода работали в несколько раз медленнее исполняемого кода программ C++, поскольку каждую инструкцию в ходе выполнения необходимо было преобразовать в двоичный код. Это, в свою очередь, приводило к неоправданным расходам. Приведем простейший пример. Каждый цикл состоит из набора многократно повторяющихся инструкций. При выполнении очередной итерации JVM снова и снова интерпретирует один и тот же байт-код, при этом на процедуру интерпретации уходит довольно много процессорного времени.
Но методы, на которых базировалась работа старых JVM, уже уходят в прошлое. Большинство современных виртуальных машин Java оснащается JIT-компиляторами. Эти компиляторы транслируют и преобразуют в двоичный код сразу весь исходный файл. Таким образом, отпадает необходимость повторной трансляции каждой инструкции байт-кода.
Производительность кода различных компиляторов
Компиляторы C++ могут повысить скорость выполнения отдельных частей кода путем выявления неэффективных участков и их соответствующего преобразования. Этот процесс носит название оптимизации. К примеру, хороший компилятор способен распознать небрежность программиста и исключить из цикла «статические» вычисления. Под «статическими» вычислениями понимается выполнение в цикле определенной операции, результатом которой независимо от итерации всегда является константа.
Распознав такую конструкцию, компилятор выводит ее за рамки цикла. Таким образом, значение константы вычисляется еще до входа в цикл и может использоваться внутри него, сокращая время выполнения и не изменяя логики программы. Этот тип оптимизации называется перемещением выражений. Для его выполнения компилятору необходимо сделать несколько проходов и изучить все инструкции цикла. В приведенном выше примере перемещения выражения все команды цикла должны быть тщательно проверены, и компилятор обязан убедиться в том, что при любой итерации значением выражения является константа.
Виртуальная машина Java, не имеющая JIT-компилятора, последовательно интерпретирует каждую инструкцию и не может проводить подобную оптимизацию «на лету». Технология JIT-компилятора позволяет оптимизировать файл классов целиком.
Таким образом, единственная причина, по которой обрабатываемая JIT-компилятором программа Java и приложение C++ работают с разной скоростью, — это необходимость первоначальной трансляции файла классов; она же, в свою очередь, зависит от типа проводимой оптимизации.
Общее время выполнения программы, включающей в себя большое количество классов, складывается из времени, которое требуется на компиляцию этих классов, и времени обработки процессором двоичного кода. В реальных программах многократно встречаются одни и те же классы, поэтому на практике затраты на компиляцию всегда оказываются значительно ниже затрат на непосредственное выполнение двоичного кода.
На начальном этапе большинство компаний, разрабатывавших компиляторы JIT, пытались определять, какие классы следует компилировать, а какие — нет. Это зависело от того, как часто появляются классы в программе. Впоследствии многие производители отказались от такого решения, и компиляторы стали транслировать весь исходный код, поскольку общее время выполнения при этом практически не изменялось.
Теория и практика
Теоретически скорости выполнения байт-кода Java, обрабатываемого JIT-компилятором, и двоичного кода С++ не должны существенно различаться. На практикеже необходимо учитывать два фактора, оказывающих существенное влияние на производительность.
Во-первых, одной и той же инструкции байт-кода могут соответствовать несколько различных последовательностей команд конкретного процессора. В результате выполнения этих последовательностей вы получите один и тот же результат, но время обработки при этом будет заметно отличаться. Если команды, получаемые на выходе JIT-компилятора и компилятора C++, требуют одинакового количества тактов процессора, быстродействие программ Java и C++ оказывается практически одинаковым. (В данном случае все зависит от того, насколько хорошо компилятор оптимизирует исполняемый код по критерию быстродействия.)
Во-вторых, имеет смысл оценивать количество проходов компилятора, необходимое для оптимизации отдельных участков кода. В рассмотренном выше примере «статических» вычислений, чтобы определить, изменяется значение выражения или нет, компилятору, возможно, потребуется проверить все итерации цикла.
Один из видов более сложной оптимизации — это устранение неиспользуемого кода. В данном случае компилятор должен найти операторы, которые не выполняются ни при каких условиях. Если такие операторы существуют, они не включаются в исполняемый код.
Устранение неиспользуемого кода может привести к существенному повышению быстродействия программы, однако при этом значительно увеличивается и время компиляции. Необходимо учитывать, что появление неиспользуемого кода — это результат ошибок программиста. Опытные разработчики обязательно проверяют текст программы на наличие неиспользуемого кода, облегчая тем самым задачу компилятора.
В общем случае в зависимости от выигрыша в производительности и временных затрат все виды оптимизации можно разделить на несколько уровней.
Первый и второй уровень оптимизации как правило повышают быстродействие на 10-15 % при минимальных затратах.
Третий уровень оптимизации позволяет увеличить производительность еще на 5 %, однако это обойдется значительно дороже.
Примеры реальных программ
В таблице 2 сравниваются результаты выполнения нескольких процедур Java и аналогичных программ, написанных на C++. Нетрудно заметить, что при отсутствии JIT-компилятора производительность Java в три-четыре раза уступает этому показателю для C++.
Таблица 2
Тест | Описание | Время выполнения программы C++, сек. | Время выполнения программы Java (JIT- компилятор), сек. | Время выполнения программы Java (интер- претатор байт-кода), сек. |
Целочисленное деление | В цикле 10 тыс. раз выполнялась операция целочисленного деления. | 1,8 | 1,8 | 4,8 |
Неиспользуемый код | В цикле 10 млн. раз встречалось неиспользуемое выражение | 3,7 | 3,7 | 9,5 |
Комбинация неиспользуемого кода и целочисленного деления | В цикл была встроена одна исполняемая команда и 10 млн. раз неиспользуемых выражений | 5,4 | 5,7 | 20 |
Деление с плавающей точкой | В цикле 10 млн. раз выполнялось деление с плавающей точкой | 1,6 | 1,6 | 8,7 |
Статический метод | В цикле 10 млн. раз вызывался статический метод, реализующий целочисленное деление | 1,8 | 1,8 | 6,0 |
Компонентный метод | В цикле 10 млн. раз вызывался компонентный метод, реализующий целочисленное деление | 1,8 | 1,8 | 10 |
Виртуальный компонентный метод* | В данном тесте 10 млн. раз вызывался виртуальный компонентный метод, реализующий целочисленное деление | 1,8 | 1,8 | 10 |
Виртуальный компонентный метод с приведением типов и идентификацией типа в момент выполнения (RTTI) | В данном тесте 10 млн. раз вызывался виртуальный метод класса, в котором выполнялась операция приведения типов | 11 | 4,3 | 12 |
Виртуальный компонентный метод с неправильным приведением типа и идентификацией типа в момент выполнения (RTTI) | В данном тесте 10 млн. раз вызывался виртуальный метод класса, в котором выполнялась операция приведения типов | -** | -** | -** |
** Аварийное завершение
В полном соответствии с теоретическими исследованиями быстродействие программ Java, обрабатываемых JIT-компилятором, практически не отличается от этого показателя для процедур C++. Единственное исключение — выполнение операции приведения типов, где скорость C++ значительно превосходит скорость Java.
Сложные объектно-ориентированные программы имеют сложную иерархию, вследствие чего часто приходится выполнять операцию преобразования родительского класса в дочерний. Данная операция называется приведением типов.
В момент выполнения программы проверяется правильность преобразования родительского класса в дочерний (идентификация типа в момент выполнения — run-time type identification, RTTI). При эксплуатации сложных систем, в которых не реализованы методы RTTI, часто возникают ошибки. Это значительно снижает эффективность всей системы и подрывает доверие пользователей.
Большинство программистов, пишущих на C++, отключают RTTI, чтобы повысить быстродействие. Технология Java не позволяет воспользоваться таким приемом, поскольку это может привести к нарушению системы безопасности. Наши тесты еще раз продемонстрировали, насколько методы RTTI замедляют скорость выполнения программы.
РАСПРЕДЕЛЕНИЕ ПАМЯТИ
Для хранения информации и выполнения вычислений программы должны эффективно управлять имеющейся памятью. После того как потребность в ранее выделенной области памяти отпадает, эта память должна быть освобождена и возвращена системе. Механизмы выделения памяти C++ и Java очень похожи. Что касается освобождения, здесь имеются принципиальные отличия. Программы C++ обязаны явно освобождать память. Очень часто при выполнении программ C++ возникают ошибки, вызванные тем, что программист забыл явно освободить память. Эта память «блокируется» и становится недоступной до завершения работы приложения.
В среде Java реализован встроенный механизм освобождения памяти, называемый «сборкой мусора». Этот механизм автоматически определяет момент, когда конкретная область памяти больше не нужна программе, после чего память возвращается системе.
Распределение памяти: Java против C++
Поскольку «сборщик мусора» Java должен автоматически выявить незадействованные программой области памяти, общие расходы Java на распределение памяти значительно превышают соответствующие расходы C++. Но при этом механизм освобождения памяти Java обеспечивает два очень важных преимущества.
Во-первых, программы фактически гарантированы от утери памяти. Поскольку ошибки, связанные с потерей областей памяти, очень часто встречаются в сложных системах, отпадает необходимость поиска таких ошибок и длительного изучения кода с помощью отладчика, а следовательно значительно сокращается цикл разработки.
Еще один недостаток сложных систем — фрагментация памяти, возникающая в результате многочисленных операций выделения и освобождения памяти и существенно снижающая скорость выполнения приложения. Хорошо написанный «сборщик мусора» Java способен, эффективно перераспределяя память, предотвратить фрагментацию.
Таким образом, Java и C++ используют различные механизмы управления памятью. Эффективность распределения памяти, характерная для Java, достигается за счет снижения производительности.
Сложность реализации стратегии «сборки мусора» заключается в том, что механизм освобождения памяти должен самостоятельно выявить неиспользуемые объекты. Если программист твердо придерживается принципов объектно-ориентированной философии, процесс «сборки мусора» обойдется сравнительно недорого. В случае же использования процедурного подхода, сложность взаимосвязей значительно затруднит поиск неиспользуемых объектов.
Примеры программ
На выделение и освобождение памяти для 10 млн. 32-разрядных целых чисел программе на C++ потребовалось 0,812 сек., а программе на Java — 1,592 сек. На основании данного примера хорошо прослеживается снижение быстродействия Java. И все же несмотря на огромную дополнительную работу, выполненную «сборщиком мусора», производительность Java вполне соизмерима с производительностью C++.
ДОСТУП К СИСТЕМНЫМ РЕСУРСАМ
Помимо распределения памяти программе необходим доступ к другим системным службам, таким как вывод графических примитивов, обработка звука и управление окнами. Традиционные программы на C++ обращаются к системным функциям при помощи интерфейсов прикладных программ (API). Каждой платформе соответствует свой API, поэтому при переносе программы с одной платформы на другую разработчику приходится порой проводить существенные модификации.
Программы Java обращаются к системным службам любой платформы при помощи единственного API. Каждая платформа имеет сходные интерфейсы, которые в ответ на обращения Java выделяют программе необходимые ресурсы.
Доступ к системным ресурсам: Java против C++
В общем случае преимущества, которые дает однотипное обращение к системным службам, оправдывают стоимость разработки специальных интерфейсов Java. Программы Java, вызывающие системные функции, работают так же быстро, как программы C++.
Единственным исключением из этого правила является то, что средства, эквивалентные функциям API Java, имеются не на всех платформах. В частности, операционные системы компьютеров Macintosh не поддерживают потоки, поэтому соответствующие функции API Java невозможно задействовать на платформе Macintosh.
Примеры программ
Мы протестировали программу, разработанную известным специалистом в области анимации и мультимедиа Джеем Бартотом. Быстродействие реализующего ее алгоритм апплета Java ничем не отличалось от этого показателя программы, написанной при помощи средств Win32 SDK.
Данный пример поможет служить подтверждением двух важнейших постулатов.
Во-первых, производительность Java зависит от производительности JVM. Тестовая программа, работавшая под управлением Internet Explorer 4.0 и Netscape Communicator в среде Windows NT, не отличалась высокой скоростью выполнения. Объекты перемещались на экране неестественно, с высокой дискретностью. Однако стоило запустить браузеры в среде Windows 95, как скорость анимации стала вполне приемлемой.
Разница в производительности объясняется огромной работой, проделанной с тем, чтобы оптимизировать быстродействие браузеров в среде Windows 95. Принципиальные архитектурные различия не позволяют обеспечить одинаковую производительность браузера в обеих операционных системах.
Во-вторых, пакет JDK for Win32, разработанный корпорацией Sun, является общепринятым стандартом, который обязаны поддерживать все браузеры и JVM.
КОМПЕТЕНТНОСТЬ РАЗРАБОТЧИКА — ГАРАНТИЯ ВЫСОКОЙ ПРОИЗВОДИТЕЛЬНОСТИ
Создается впечатление, что аналитики, упрекающие Java в недостаточной производительности, никогда не переведутся. Впрочем, то же самое наблюдалось при появлении языков второго поколения, структурного и объектно-ориентированного программирования. Каждая новая технология повышает эффективность разработки, но и заставляет соблюдать определенную дисциплину. Поскольку Java — это объектно-ориентированный язык, наиболее активное его неприятие ощущается со стороны тех, кто до сих пор не осознал преимуществ новой парадигмы.
Проведенный нами анализ показал, что как с теоретической, так и с практической точки зрения значительной разницы в производительности приложений C++ и Java не наблюдается. А если такая разница и существует, программист, пишущий на Java, оказывается в гораздо более выгодном положении по сравнению с тем, кто работает на C++.
Если сравнивать программирование на Java и на C++, оказывается, что у первого, безусловно, есть три основных преимущества, позволяющих сократить время разработки и повысить быстродействие сложных приложений.
Остается лишь перечислить эти преимущества:
Теория и практика Java: Динамическая компиляция и измерение производительности
Проведение и интерпретация испытаний производительности для динамически компилируемых языков программирования, таких как Java, является намного более трудной задачей, чем для статически компилируемых языков, например C или C++. В данной статье серии Теория и практика Java Brian Goetz объясняет несколько из множества причин, по которым динамическая компиляция может усложнить тест производительности.
http://www-128.ibm.com/developerworks/java/library/j-jtp12214/
Bertrand Portier (brian@quiotix. com)
Ведущий консультант, Quiotix
21 Dec, 2004
Содержание
1 Динамическая компиляция — краткая история
2 Так какое отношение все это имеет к тесту производительности?
3 Удаление «мертвого» кода
4 Тренировка
5 Динамическая деоптимизация
6 Заключение
7 Ресурсы
8 Об авторах
Динамическая компиляция — краткая история
Процесс компиляции Java-приложения отличается от процесса компиляции статически компилируемых языков программирования, подобных С или С++. Статический компилятор преобразует исходный код непосредственно в машинные инструкции, которые могут быть выполнены на целевой платформе. Различные аппаратные платформы требуют применения различных компиляторов. Java-компилятор преобразует исходный Java-код в переносимые байткоды JVM, которые являются для JVM «инструкциями виртуальной машины». В отличие от статических компиляторов javac выполняет очень маленькую оптимизацию — оптимизация, проводимая статическими компиляторами, выполняется во время исполнения программы.
Первые поколения JVM были полностью интерпретируемыми. JVM интерпретировала байткод вместо компиляции его в машинный код и выполнения машинного кода. Этот подход, естественно, не предлагал наилучшей возможной производительности, поскольку система больше времени тратила на выполнение интерпретатора, а не самой программы.
Just-in-time (оперативная) компиляция
Интерпретация была хороша для proof-of-concept (доказательство идеи) реализации, но первые JVM сразу обвинили в медлительности. Следующие поколения JVM использовали just-in-time (JIT) компиляторы для ускорения работы. Строго определенная JIT-виртуальная машина перед выполнением преобразовывает все байткоды в машинный код, но делает это в неторопливом стиле: JIT компилирует code path только тогда, когда знает, что code path будет сейчас выполняться (отсюда и название just-in-time компиляция). Этот подход дает возможность программам стартовать быстрее, поскольку не требуется длительная фаза компиляции перед возможным началом исполнения кода.
Использование JIT выглядело многообещающим, но существуют недостатки. JIT-компиляция устранила накладные расходы интерпретации (за счет некоторого дополнительного замедления запуска), но уровень оптимизации кода по некоторым причинам был посредственным. Во избежание значительного замедления при запуске Java-приложений JIT-компилятор должен быть быстрым, т.е. он не может тратить много времени на оптимизацию. И первые JIT-компиляторы были консервативны в создании встроенных предположений, поскольку они не знали, какие классы могут быть загружены позднее.
С технической точки зрения JIT-виртуальная машина компилирует каждый байткод перед его выполнением. Термин JIT часто используется для обозначения любой динамической компиляции байткода в машинный код, даже когда возможна интерпретация байткода.
Динамическая компиляция HotSpot
Исполняющий процесс HotSpot объединяет интерпретацию, профилирование и динамическую компиляцию. Вместо преобразования всех байткодов в машинный код HotSpot начинает работу как интерпретатор и компилирует только «горячий» код, то есть код, выполняющийся наиболее часто. Во время выполнения он собирает данные анализа. Эти данные используются для определения фрагментов кода, выполняющихся достаточно часто и заслуживающих компиляции. Компиляция только часто исполняемого кода имеет несколько преимуществ: не тратится время на компиляцию кода, выполняющегося редко, таким образом, компилятор может тратить больше времени на оптимизацию «горячего» кода, поскольку он знает, что время будет потрачено не зря. Кроме того, откладывая компиляцию, компилятор имеет доступ к данным анализа, которые могут быть использованы для улучшения оптимизации, например, встраивать ли вызов конкретного метода.
HotSpot поставляется с двумя компиляторами: клиентским и серверным. По умолчанию используется клиентский компилятор. Вы можете выбрать серверный компилятор, указав параметр -server
при запуске JVM. Серверный компилятор оптимизирован для повышения пиковой скорости работы и предназначен для «долгоиграющих» серверных приложений. Клиентский компилятор оптимизирован для уменьшения времени начального запуска приложения и занимаемого объема памяти, реализует менее сложную оптимизацию, чем серверный компилятор, и, следовательно, требует меньше времени для компиляции.
Серверный компилятор HotSpot может выполнять впечатляющее число видов оптимизации. Среди них множество стандартных видов оптимизации, имеющихся в статических компиляторах, например, выведение кода из тела циклов, общее удаление подвыражения, развертка цикла, удаление проверки диапазона, удаление «мертвого» кода, анализ движения данных, а также множество оптимизаций, обычно не применяющихся в статических компиляторах, например, принудительное встраивание вызовов виртуальных методов.
Непрерывная перекомпиляция
Еще одна интересная особенность HotSpot — компиляция не осуществляется по принципу «все или ничего». Code path компилируется в машинный код после интерпретации его определенное количество раз. Но JVM продолжает анализ и может перекомпилировать код заново с более высоким уровнем оптимизации, если решит, что code path является особенно «горячим» или последующий анализ данных показал возможность дополнительной оптимизации. JVM может перекомпилировать одни и те же байткоды много раз при выполнении одиночного приложения. Для получения более подробной информации о работе компилятора попробуйте вызвать JVM с флагом -XX:+PrintCompilation
, который заставляет компилятор (клиентский или серверный) каждый раз во время своей работы выводить на экран короткое сообщение.
Замещение в стеке
Первая версия HotSpot выполняла компиляцию по одному методу в каждый момент времени. Метод считался «горячим», если он выполнял итерации цикла, превышающие определенное количество раз (10000 в первой версии HotSpot). Это определялось назначением каждому методу счетчика и увеличением его значения каждый раз при выполнении обратного перехода. Однако после компиляции метода переключение на компилированную версию не производилось до тех пор, пока не происходил выход из метода и повторный вход — компилированная версия могла использоваться только для последовательных вызовов. В результате этого в некоторых ситуациях компилированная версия вообще никогда не использовалась, например, в случае программы, выполняющей интенсивные вычисления и в которой все вычисления производятся одинарным вызовом метода. В такой ситуации вычисляющий метод может быть скомпилирован, но компилированный код ни разу не использоваться.
Более свежие версии HotSpot используют технологию, называемую on-stack replacement (OSR), для разрешения переключения от интерпретируемого кода к компилированному (или замены одной версии компилированного кода другой) в середине цикла.
Так какое отношение все это имеет к тесту производительности?
Я обещал вам статью о тестах производительности и измерении производительности, но пока вы только получили урок по истории и пересказ белых книг HotSpot от Sun. Причиной такого длительного отступления является то, что без понимания процесса динамической компиляции практически невозможно правильно писать или интерпретировать тесты производительности для Java-классов. (Это сделать довольно непросто даже при глубоком понимании динамической компиляции и JVM-оптимизации.)
Писать микротесты производительности для Java-программы намного сложнее, чем для C-программы.
Традиционным способом определения того, что Вариант А быстрее Варианта Б, является написание маленького теста производительности, часто называемого микротестом производительности. Это очень благоразумно. Научный метод требует независимого исследования! Увы. Написание (и интерпретация) тестов производительности является намного более трудной и запутанной задачей для языков с динамической компиляцией, чем для языков со статической компиляцией. Ничего нет плохого в попытке узнать что-либо о производительности определенной конструкции путем написания программы, ее использующей. Но в большинстве случаев написанные на Java микротесты не покажут вам того, что вы думали получить.
Что касается программ, написанных на C, вы можете многое узнать о характеристиках производительности программы, даже не запуская ее, а просто посмотрев на откомпилированный машинный код. Сгенерированные компилятором инструкции представляют собой реальные машинные команды, которые будут выполняться, а их временные характеристики в общем случае достаточно хорошо понятны. (Существуют патологические примеры, производительность которых намного хуже, чем можно было бы ожидать от просмотра машинного кода, по причине отсутствия постоянного предсказания ветвлений или отсутствия кэша. Но в большинстве случаев вы можете много узнать о производительности C-программы, просмотрев ее машинный код.)
Если компилятор собирается оптимизировать фрагмент кода и удалить его, поскольку считает его несущественным (обычная ситуация с тестами производительности, которые в действительности ничего не делают), вы можете увидеть это в сгенерированном машинном коде — фрагмента не будет. И вы обычно не должны запускать C-программу на длительное время, перед тем как сделать определенные выводы о ее производительности.
С другой стороны, HotSpot JIT периодически перекомпилирует Java-байткод в машинный код при выполнении вашей программы, и эта перекомпиляция может выполняться в неожиданное время после накопления определенного количества данных для анализа, при загрузке новых классов или при выполнении code path, еще не переведенных в разряд загруженных классов. Измерения времени при постоянной перекомпиляции могут быть совершенно не точными и обманчивыми, и очень часто нужно выполнять Java-код довольно продолжительное время (я видел анекдотические ситуации повышения производительности после часов или даже дней, прошедших с момента запуска программы) до получения полезных данных о производительности.
Удаление «мертвого» кода
Одной из проблем написания хорошей тестовой программы является удаление оптимизирующим компилятором «мертвого» кода — кода, не влияющего на результат выполнения программы. Программы тестов производительности часто не выполняют вывод каких-либо результатов на экран; это означает, что некоторый или весь ваш код может быть оптимизирован и удален без вашего ведома, и с этого момента вы будете тестировать не то что думаете. В частности, многие микротесты работают намного лучше, будучи запущенными с параметром -server
вместо -client
, не потому что серверный компилятор быстрее (хотя чаще всего так и есть), а потому что серверный компилятор более приспособлен к оптимизации фрагментов «мертвого» кода. К сожалению, такая оптимизация «мертвого» кода, ускоряющая работу вашей тестовой программы (возможно оптимизируя ее полностью), не работает также хорошо с программой, которая что-то реально делает.
Странный результат
Листинг 1 содержит пример кода теста производительности с ничего не делающим блоком, который был взят из теста для измерения производительности параллельных потоков, но вместо этого измеряет что-то совершенно другое. (Этот пример был позаимствован из отличной презентации JavaOne 2003 «The Black Art of Benchmarking». См. раздел Ресурсы.)
Листинг 1. Тест производительности, искаженный непреднамеренным «мертвым» кодом
public class StupidThreadTest {
public static void doSomeStuff() {
double uselessSum = 0;
for (int i = 0; i < 1000; i++) {
for (int j = 0; j < 1000; j++) {
uselessSum += (double) i + (double) j;
}
}
}
public static void main(String[] args) throws InterruptedException {
doSomeStuff();
int nThreads = Integer. parseInt(args[0]);
Thread[] threads = new Thread[nThreads];
for (int i = 0; i < nThreads; i++)
threads[i] = new Thread(new Runnable() {
public void run() {
doSomeStuff();
}
});
long start = System.currentTimeMillis();
for (int i = 0; i < threads.length; i++)
threads[i].start();
for (int i = 0; i < threads.length; i++)
threads[i].join();
long end = System.currentTimeMillis();
System.out.println("Time: " + (end - start) + "ms");
}
}
Метод doSomeStuff() предназначен для порождения выполняющих какое-то действие потоков. Мы можем оценить планируемые издержки для нескольких потоков времени исполнения StupidThreadBenchmark
. Однако компилятор может определить, что весь код в doSomeStuff
является «мертвым», и оптимизировать его путем удаления, поскольку uselessSum
никогда не используется. Как только код внутри цикла удаляется, может удалиться также и сам цикл, оставив doSomeStuff
полностью пустым. В таблице 1 показана производительность StupidThreadBenchmark
на клиенте и на сервере. Обе JVM показывают линейную зависимость от количества потоков. Легко может быть выдвинуто ошибочное предположение о том, что JVM сервера в 40 раз быстрее JVM клиента. В действительности происходит следующее — серверный компилятор выполняет более полную оптимизацию и может обнаружить, что doSomeStuff
содержит полностью «мертвый» код. В то время как многие программы действительно ускоряются на JVM сервера, ускорение, которое видите вы — это просто измерение плохо написанного теста производительности, а не блестящая производительность JVM сервера. Но если вы были невнимательны, можно довольно легко перепутать одно с другим.
Таблица 1. Производительность StupidThreadBenchmark в клиентской и серверной JVM
Количество потоков | Время работы JVM клиента | Время работы JVM сервера |
10 | 43 | 2 |
100 | 435 | 10 |
1000 | 4142 | 80 |
10000 | 42402 | 1060 |
Оптимизация (т. е. удаление) «мертвого» кода также является проблемой при тестировании компилированных статически приложений. Однако определить удаление компилятором большого фрагмента вашего кода в таких приложениях намного легче. Вы можете посмотреть на сгенерированный машинный код и обнаружить отсутствие части вашей программы. При использовании же динамически компилируемых языков эта информация не является для вас легкодоступной.
Тренировка
При измерении производительности идиомы Х вы, обычно, хотите измерить производительность ее компилированной, а не интерпретируемой, версии. (Вы хотите знать, насколько быстро X будет работать.) Для этого перед началом измерений времени выполнения необходима «тренировка» JVM — выполнение вашего действия достаточное количество раз, для того чтобы компилятор имел достаточно времени и заменил интерпретируемый код компилированным.
Для первых JIT и динамических компиляторов без замещения в стеке существовала простая формула измерения компилированной производительности метода: выполнить определенное количество вызовов метода, запустить таймер, затем выполнить метод определенное дополнительное количество раз. Если число тренировочных вызовов превышало порог срабатывания процедуры компиляции метода, фактически измеренные вызовы относились к компилированному коду, а все издержки компиляции должны были проявиться до начала измерения.
С современными динамическими компиляторами это сделать значительно труднее. Компилятор запускается в менее предсказуемое время, JVM по своему желанию переключает код с интерпретируемой версии на компилированную, а один и тот же code path при выполнении может быть компилирован и перекомпилирован более одного раза. Если вы не определите время этих событий, они могут серьезно исказить результаты ваши х измерений.
На рисунке 1 показаны некоторые из возможных искажений из-за непредсказуемой динамической компиляции. Допустим, вы собираетесь измерить 200000 итераций цикла и компилированный код в 10 раз быстрее интерпретируемого. Если компиляция запускается за 200000 итераций, вы измеряете только интерпретируемую производительность (шкала времени (a)). Если компиляция запускается за 100000 итераций, общее время выполнения состоит из времени выполнения 100000 интерпретируемых итераций плюс время компиляции (которое вы не хотите учитывать) плюс время выполнения компилируемых итераций (шкала времени (b)). Если компиляция запускается за 20000 итераций, общее время состоит из 20000 интерпретируемых итераций плюс время компиляции плюс 180000 компилированных итераций (шкала времени (c)). Поскольку вы не знаете,когда запускается компилятор, а также время компиляции, результат измерений может сильно искажаться. В зависимости от времени компиляции и того, насколько компилируемый код быстрее интерпретируемого, небольшие изменения количества итераций могут привести к большим различиям в измерении «производительности».
Итак, какого объема тренировки достаточно? Вы не знаете. Лучшее, что можно сделать — запустить ваш тест с параметром -XX:+PrintCompilation
, найти причину запуска компилятора, затем реструктуризировать вашу программу измерения производительности так, чтобы вся компиляция происходила до начала измерений и не вызывалась в середине измерительного цикла.
Не забывайте о сборке мусора
Как вы уже видели, для получения точных результатов измерения нужно выполнить тестирующий код даже большее количество раз, чем было бы достаточно для тренировки JVM. С другой стороны, если тестирующий код производит любое размещение объектов (почти каждый код это делает), из них создается мусор и со временем должен будет запуститься сборщик мусора. Это еще один элемент, который может сильно исказить результаты измерений — небольшое изменение количества итераций может означать работу сборщика мусора, который искажает измерение «времени итерации».
Если запустить тест с параметром -verbose:gc
, то можно определить время, затрачиваемое на сборку мусора, и соответственно скорректировать данные измерений. Еще лучше запустить программу на очень длительное время, гарантируя инициирование большого числа сборок мусора для более точной компенсации действий по размещению объектов и сборке мусора.
Динамическая деоптимизация
Многие из стандартных типов оптимизаций могут быть выполнены только в «основном блоке», так что встраивание вызовов методов часто является очень важным для достижения хорошей оптимизации. При встраивании вызовов методов устраняются не только накладные расходы при вызове методов, но и предоставляется больший основной блок компилятору для оптимизации с реальной возможностью оптимизации «мертвого кода».
Листинг 2 представляет пример типа оптимизации, разрешаемой через встраивание. Метод outer()
вызывает inner()
с аргументом null
, который приводит к тому, что inner()
не выполняет никаких действий. Но, встраивая вызов к inner()
, компилятор может обнаружить, что ветвление else
в inner()
является «мертвым» кодом и может оптимизировать проверку и ветвление else
путем удаления, что может в свою очередь дать возможность оптимизировать (удалить) вызов к inner()
полностью. Если бы inner()
не был бы встроен, такая оптимизация не была бы возможна.
Листинг 2. Как встраивание может привести к лучшей оптимизации «мертвого» кодаpublic class Inline {
public final void inner(String s) {
if (s == null)
return;
else {
// выполнить что-то действительно сложное
}
}
public void outer() {
String s = null;
inner(s);
}
}
К сожалению, виртуальные функции ставят препятствие для встраивания, а виртуальные функции в Java больше распространены, чем в C++. Допустим, компилятор пытается оптимизировать вызов doSomething()
в следующем коде:
Foo foo = getFoo();
foo.doSomething();
Из этого фрагмента кода компилятор не может с уверенностью определить, какая версия doSomething()
будет выполнена: реализованная в классе Foo
, или в субклассе Foo
. Есть несколько ситуаций, когда ответ очевиден (например, если Foo
указан как final
или doSomething()
определен как final
-метод в Foo
), но чаще всего компилятор должен догадываться. Со статическим компилятором, который компилирует один класс в данный момент времени, нам бы не повезло. Но динамический компилятор может использовать глобальную информацию для принятия лучшего решения. Допустим, что не существует в данном приложении расширяющих Foo
загруженных классов. Эта ситуация больше похожа на тот случай, когда doSomething()
являлся final
-методом Foo
— компилятор может преобразовать вызов виртуального метода в прямую передачу (уже улучшение) и, далее, может также выполнить встраивание doSomething()
. (Преобразование вызова виртуального метода в прямой вызов метода называется мономорфным преобразованием вызова.)
Подождите секунду — классы могут быть загружены динамически. Что произойдет если компилятор выполнит такую оптимизацию, а затем загрузится класс, расширяющий Foo
? Более того, что если это будет сделано внутри метода генератора getFoo()
, и getFoo()
возвратит экземпляр нового субкласса Foo
? Не будет ли сгенерированный код ошибочным? Да, будет. Но JVM понимает это и сделает недействительным сгенерированный код, основанный на теперь уже неверном предположении, и возвратится к интерпретации (или перекомпилирует неверный code path).
В результате компилятор может принять решение о принудительном встраивании для достижения более высокой производительности, затем отменить это решение, если оно более не основывается на верных предположениях. Фактически эффективность такой оптимизации заключается в том, что добавление ключевого слова final
к не переопределенным методам (совет для повышения производительности из старой литературы) дает очень мало для улучшения реальной производительности.
Странный результат
В листинге 3 приведен шаблон кода, в котором объединены неправильная тренировка, мономорфное преобразование вызова и деоптимизация, что приводит к полностью бессмысленным, но легко интерпретируемым результатам:
Листинг 3. Результаты тестовой программы искажены мономорфным преобразованием вызова и последовательной деоптимизацией
public class StupidMathTest {
public interface Operator {
public double operate(double d);
}
public static class SimpleAdder implements Operator {
public double operate(double d) {
return d + 1. 0;
}
}
public static class DoubleAdder implements Operator {
public double operate(double d) {
return d + 0.5 + 0.5;
}
}
public static class RoundaboutAdder implements Operator {
public double operate(double d) {
return d + 2.0 - 1.0;
}
}
public static void runABunch(Operator op) {
long start = System.currentTimeMillis();
double d = 0.0;
for (int i = 0; i < 5000000; i++)
d = op.operate(d);
long end = System.currentTimeMillis();
System.out.println("Time: " + (end - start) + " ignore:" + d);
}
public static void main(String[] args) {
Operator ra = new RoundaboutAdder();
runABunch(ra); // неправильная попытка тренировки
runABunch(ra);
Operator sa = new SimpleAdder();
Operator da = new DoubleAdder();
runABunch(sa);
runABunch(da);
}
}
StupidMathTest
сначала пытается выполнить тренировку (безуспешно), затем измеряет время выполнения SimpleAdder
, DoubleAdder
и RoundaboutAdder
с результатами, отображенными в таблице 2. Похоже, что намного быстрее добавить 1 к double
, добавив 2 и отняв 1, чем просто добавив 1 сразу же. И немного быстрее добавить 0.5 дважды, чем добавить 1. Возможно ли такое? (Ответ: нет.)
Таблица 2. Бессмысленные и обманчивые результаты StupidMathTest
Method | Runtime |
SimpleAdder | 88ms |
DoubleAdder | 76ms |
RoundaboutAdder | 14ms |
Что здесь произошло? После цикла тренировки RoundaboutAdder
и runABunch()
компилируются, и компилятор выполняет мономорфное преобразование вызова с Operator
и RoundaboutAdder
, так что первый проход выполняется довольно быстро. Во втором проходе (SimpleAdder
) компилятор должен выполнить деоптимизацию и откат к диспетчеризации виртуального метода, так что второй проход выполняется медленнее благодаря неспособности оптимизировать (удалить) вызов виртуальной функции, время тратится на перекомпиляцию. На третьем проходе (DoubleAdder
) выполняется меньше перекомпиляции, поэтому он работает быстрее. (В действительности компилятор собирается провести свертывание констант в RoundaboutAdder
и DoubleAdder
, генерируя точно такой же код, что и SimpleAdder
. То есть, если существует разница во время выполнения, она не зависит от арифметического кода.) Какой проход выполнится первый, он и будет самым быстрым.
Итак, какой мы можем сделать вывод из этого «теста производительности»? Практически никакого, за исключением того, что тестирование производительности динамически компилируемых языков программирования является намного более коварным процессом, чем вы могли бы представить.
Заключение
Результаты приведенных здесь примеров были настолько очевидно ошибочными, что было ясно — должно происходить еще нечто. Даже меньшие эффекты могут легко исказить результаты ваших программ тестирования производительности без включения вашего детектора «Что-то здесь не то». И хотя здесь представлены распространенные источники искажений микротестов производительности, существует множество других источников. Мораль этой истории: вы не всегда измеряете то, что думаете. Фактически, вы обычно не измеряете то, что думаете. Будьте очень осторожны с результатами любых измерений производительности, в которые не были вовлечены реальные программы на длительный период времени.
Ресурсы
Ссылки по теме
Jikes Research Virtual Machine
Overview of the IBM Just-in-Time Compiler
Java theory and practice series
Об авторах
Brian Goetz является профессиональным разработчиком программного обеспечения более чем 17 лет. Он — главный консультант в Quiotix, компании по разработке программного обеспечения и консультациям в Los Altos, Calif., и работает в нескольких экспертных группах JCP. Ищите опубликованные и готовящиеся им статьи в популярных отраслевых публикациях.
Измерение производительности | Parallel Collections
Производительность JVM
При описании модели производительности выполнения кода на JVM иногда ограничиваются несколькими комментариями, и как результат– не всегда становится хорошо понятно, что в силу различных причин написанный код может быть не таким производительным или расширяемым, как можно было бы ожидать. В этой главе будут приведены несколько примеров.
Одной из причин является то, что процесс компиляции выполняющегося на JVM приложения не такой, как у языка со статической компиляцией (как можно увидеть здесь [2]). Компиляторы Java и Scala обходятся минимальной оптимизацией при преобразовании исходных текстов в байткод JVM. При первом запуске на большинстве современных виртуальных Java-машин байткод преобразуется в машинный код той архитектуры, на которой он запущен. Это преобразование называется компиляцией “на лету” или JIT-компиляцией (JIT от just-in-time). Однако из-за того, что компиляция “на лету” должна быть быстрой, уровень оптимизации при такой компиляции остается низким. Более того, чтобы избежать повторной компиляции, компилятор HotSpot оптимизирует только те участки кода, которые выполняются часто. Поэтому тот, кто пишет тест производительности, должен учитывать, что программа может показывать разную производительность каждый раз, когда ее запускают: многократное выполнение одного и того же куска кода (то есть, метода) на одном экземпляре JVM может демонстрировать очень разные результаты замеров производительности в зависимости от того, оптимизировался ли определенный код между запусками. Более того, измеренное время выполнения некоторого участка кода может включать в себя время, за которое произошла сама оптимизация JIT-компилятором, что сделает результат измерения нерепрезентативным.
Кроме этого, результат может включать в себя потраченное на стороне JVM время на осуществление операций автоматического управления памятью. Время от времени выполнение программы прерывается и вызывается сборщик мусора. Если исследуемая программа размещает хоть какие-нибудь данные в куче (а большинство программ JVM размещают), значит сборщик мусора должен запуститься, возможно, исказив при этом результаты измерений. Можно нивелировать влияние сборщика мусора на результат, запустив измеряемую программу множество раз, и тем самым спровоцировав большое количество циклов сборки мусора.
Одной из распространенных причин ухудшения производительности является упаковка и распаковка примитивов, которые неявно происходят в случаях, когда примитивный тип передается аргументом в обобщенный (generic) метод. Чтобы примитивные типы можно было передать в метод с параметром обобщенного типа, они во время выполнения преобразуются в представляющие их объекты. Этот процесс замедляет выполнение, а кроме того порождает необходимость в дополнительном выделении памяти и, соответственно, создает дополнительный мусор в куче.
В качестве распространенной причины ухудшения параллельной производительности можно назвать соперничество за память (memory contention), возникающее из-за того, что программист не может явно указать, где следует размещать объекты. Фактически, из-за влияния сборщика мусора, это соперничество может произойти на более поздней стадии жизни приложения, а именно после того, как объекты начнут перемещаться в памяти. Такие влияния нужно учитывать при написании теста.
Пример микротеста производительности
Существует несколько подходов, позволяющих избежать описанных выше эффектов во время измерений. В первую очередь следует убедиться, что JIT-компилятор преобразовал исходный текст в машинный код (и что последний был оптимизирован), прогнав микротест производительности достаточное количество раз. Этот процесс известен как фаза разогрева (warm-up).
Для того, чтобы уменьшить число помех, вызванных сборкой мусора от объектов, размещенных другими участками программы или несвязанной компиляцией “на лету”, требуется запустить микротест на отдельном экземпляре JVM.
Кроме того, запуск следует производить на серверной версии HotSpot JVM, которая выполняет более агрессивную оптимизацию.
Наконец, чтобы уменьшить вероятность того, что сборка мусора произойдет посреди микротеста, лучше всего добиться выполнения цикла сборки мусора перед началом теста, а следующий цикл отложить настолько, насколько это возможно.
В стандартной библиотеке Scala предопределен трейт scala.testing.Benchmark
, спроектированный с учетом приведенных выше соображений. Ниже приведен пример тестирования производительности операции map
многопоточного префиксного дерева:
import collection.parallel.mutable.ParTrieMap
import collection.parallel.ForkJoinTaskSupport
object Map extends testing. Benchmark {
val length = sys.props("length").toInt
val par = sys.props("par").toInt
val partrie = ParTrieMap((0 until length) zip (0 until length): _*)
partrie.tasksupport = new ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par))
def run = {
partrie map {
kv => kv
}
}
}
Метод run
содержит код микротеста, который будет повторно запускаться для измерения времени своего выполнения. Объект Map
, расширяющий трейт scala.testing.Benchmark
, запрашивает передаваемые системой параметры уровня параллелизма par
и количества элементов дерева length
.
После компиляции программу, приведенную выше, следует запустить так:
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=300000 Map 10
Флаг server
требует использовать серверную VM. Флаг cp
означает “classpath”, то есть указывает, что файлы классов требуется искать в текущем каталоге и в jar-архиве библиотеки Scala. Аргументы -Dpar
и -Dlength
– это количество потоков и количество элементов соответственно. Наконец, 10
означает что тест производительности будет запущен на одной и той же JVM именно 10 раз.
Устанавливая количество потоков par
в 1
, 2
, 4
и 8
, получаем следующее время выполнения на четырехъядерном i7 с поддержкой гиперпоточности:
Map$ 126 57 56 57 54 54 54 53 53 53
Map$ 90 99 28 28 26 26 26 26 26 26
Map$ 201 17 17 16 15 15 16 14 18 15
Map$ 182 12 13 17 16 14 14 12 12 12
Можно заметить, что на первые запуски требуется больше времени, но после оптимизации кода оно уменьшается. Кроме того, мы можем увидеть что гиперпотоковость не дает большого преимущества в нашем примере, это следует из того, что увеличение количества потоков от 4
до 8
не приводит к значительному увеличению производительности.
Насколько большую коллекцию стоит сделать параллельной?
Этот вопрос задается часто, но ответ на него достаточно запутан.
Размер коллекции, при котором оправданы затраты на параллелизацию, в действительности зависит от многих факторов. Некоторые из них (но не все) приведены ниже:
- Архитектура системы. Различные типы CPU имеют различную архитектуру и различные характеристики масштабируемости. Помимо этого, машина может быть многоядерной, а может иметь несколько процессоров, взаимодействующих через материнскую плату.
- Производитель и версия JVM. Различные виртуальные машины применяют различные оптимизации кода во время выполнения и реализуют различные механизмы синхронизации и управления памятью. Некоторые из них не поддерживают
ForkJoinPool
, возвращая нас к использованиюThreadPoolExecutor
, что приводит к увеличению накладных расходов. - Поэлементная нагрузка. Величина нагрузки, оказываемой обработкой одного элемента, зависит от функции или предиката, которые требуется выполнить параллельно. Чем меньше эта нагрузка, тем выше должно быть количество элементов для получения ускорения производительности при параллельном выполнении.
- Выбранная коллекция. Например, разделители
ParArray
иParTrieMap
перебирают элементы коллекции с различными скоростями, а значит разницу количества нагрузки при обработке каждого элемента создает уже сам перебор. - Выбранная операция. Например, у
ParVector
намного медленнее методы трансформации (такие, какfilter
) чем методы получения доступа (какforeach
) - Побочные эффекты. При изменении областей памяти несколькими потоками или при использовании механизмов синхронизации внутри тела
foreach
,map
, и тому подобных, может возникнуть соперничество. - Управление памятью. Размещение большого количества объектов может спровоцировать цикл сборки мусора. В зависимости от способа передачи ссылок на новые объекты, цикл сборки мусора может занимать больше или меньше времени.
Даже рассматривая вышеперечисленные факторы по отдельности, не так-то просто рассуждать о влиянии каждого, а тем более дать точный ответ, каким же должен быть размер коллекции. Чтобы в первом приближении проиллюстрировать, каким же он должен быть, приведем пример выполнения быстрой и не вызывающей побочных эффектов операции сокращения параллельного вектора (в нашем случае– суммированием) на четырехъядерном процессоре i7 (без использования гиперпоточности) на JDK7:
import collection.parallel.immutable.ParVector
object Reduce extends testing.Benchmark {
val length = sys.props("length").toInt
val par = sys.props("par").toInt
val parvector = ParVector((0 until length): _*)
parvector.tasksupport = new collection.parallel.ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par))
def run = {
parvector reduce {
(a, b) => a + b
}
}
}
object ReduceSeq extends testing.Benchmark {
val length = sys.props("length").toInt
val vector = collection.immutable.Vector((0 until length): _*)
def run = {
vector reduce {
(a, b) => a + b
}
}
}
Сначала запустим тест производительности с 250000
элементами и получим следующие результаты для 1
, 2
и 4
потоков:
java -server -cp . :../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=250000 Reduce 10 10
Reduce$ 54 24 18 18 18 19 19 18 19 19
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=250000 Reduce 10 10
Reduce$ 60 19 17 13 13 13 13 14 12 13
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=250000 Reduce 10 10
Reduce$ 62 17 15 14 13 11 11 11 11 9
Затем уменьшим количество элементов до 120000
и будем использовать 4
потока для сравнения со временем сокращения последовательного вектора:
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=120000 Reduce 10 10
Reduce$ 54 10 8 8 8 7 8 7 6 5
java -server -cp .:../../build/pack/lib/scala-library.jar -Dlength=120000 ReduceSeq 10 10
ReduceSeq$ 31 7 8 8 7 7 7 8 7 8
Похоже, что 120000
близко к пограничному значению в этом случае.
В качестве еще одного примера возьмем метод map
(метод трансформации) коллекции mutable.ParHashMap
и запустим следующий тест производительности в той же среде:
import collection.parallel.mutable.ParHashMap
object Map extends testing.Benchmark {
val length = sys.props("length").toInt
val par = sys.props("par").toInt
val phm = ParHashMap((0 until length) zip (0 until length): _*)
phm.tasksupport = new collection.parallel.ForkJoinTaskSupport(new scala.concurrent.forkjoin.ForkJoinPool(par))
def run = {
phm map {
kv => kv
}
}
}
object MapSeq extends testing.Benchmark {
val length = sys.props("length").toInt
val hm = collection.mutable.HashMap((0 until length) zip (0 until length): _*)
def run = {
hm map {
kv => kv
}
}
}
Для 120000
элементов получаем следующие значения времени на количестве потоков от 1
до 4
:
java -server -cp . :../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=120000 Map 10 10
Map$ 187 108 97 96 96 95 95 95 96 95
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=120000 Map 10 10
Map$ 138 68 57 56 57 56 56 55 54 55
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=4 -Dlength=120000 Map 10 10
Map$ 124 54 42 40 38 41 40 40 39 39
Теперь уменьшим число элементов до 15000
и сравним с последовательным хэш-отображением:
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=1 -Dlength=15000 Map 10 10
Map$ 41 13 10 10 10 9 9 9 10 9
java -server -cp .:../../build/pack/lib/scala-library.jar -Dpar=2 -Dlength=15000 Map 10 10
Map$ 48 15 9 8 7 7 6 7 8 6
java -server -cp .:../../build/pack/lib/scala-library.jar -Dlength=15000 MapSeq 10 10
MapSeq$ 39 9 9 9 8 9 9 9 9 9
Для выбранных в этом случае коллекции и операции есть смысл сделать вычисление параллельным при количестве элементов больше 15000
(в общем случае хэш-отображения и хэш-множества возможно делать параллельными на меньших количествах элементов, чем требовалось бы для массивов или векторов).
Ссылки
- Anatomy of a flawed microbenchmark, Brian Goetz
- Dynamic compilation and performance measurement, Brian Goetz
Область памяти Java-область данных времени выполнения
Недавно узнал о JVM. Вначале я видел это быстрее. Существует лишь расплывчатая концепция области данных времени выполнения JVM. Не ясно, какие данные хранятся в разных областях памяти, поэтому я запишу их здесь.
Все мы знаем, что самое большое различие между Java и C, C ++, заключается в области управления памятью (Java имеет динамическое распределение памяти и технологию сбора мусора). Опишите управление памятью на C и C ++ в «Углубленном понимании виртуальной машины Java»: для разработчиков программ, работающих на C и C ++, в области управления памятью они являются «императорами» с наивысшей мощностью и наиболее вовлеченными в большинство «Рабочий» основной работы — не только владеет «собственностью» каждого объекта, но также выполняет задачи по обслуживанию от начала до конца жизни каждого объекта.
Для Java, так как программы Java выполняются JVM, понимание области памяти Java фактически понимает область памяти JVM.
1. Область данных времени выполнения JVM
- Что такое область данных времени выполнения?
Здесь нам нужно понять поток выполнения Java-программы:
Как показано на рисунке выше, сначала файл исходного кода Java (суффикс .java) будет скомпилирован компилятором Java в файл байт-кода (суффикс .class), а затем загрузчик классов в JVM загружает файлы байт-кода каждого класса и загружает После завершения он будет выполнен механизмом исполнения JVM. В течение всего процесса выполнения программы JVM будет использовать пространство для хранения данных и связанной с ними информации, необходимых во время выполнения программы.Это пространство обычно называется областью данных времени выполнения (область данных времени выполнения)
- Посмотрите на формальное описание
Виртуальная машина Java делит память, которой она управляет, на несколько различных областей данных во время выполнения программы Java. Эти области имеют свои собственные цели и время создания и уничтожения. Некоторые области существуют с началом процесса виртуальной машины, а некоторые области создаются и уничтожаются в зависимости от начала и конца пользовательских потоков. Согласно «Спецификации виртуальной машины Java», память, управляемая виртуальной машиной Java, будет содержать следующие области данных времени выполнения.
(синий для частного потока, оранжевый для общего)
2. Введение в каждую область данных
Давайте посмотрим на каждую область данных отдельно:
Счетчик программ
Счетчик программ (Регистр счетчика программ) — это небольшой объем памяти, который можно рассматривать как индикатор номера строки байт-кода, выполняемого текущим потоком (также известный как регистр ПК).В концептуальной модели виртуальной машины интерпретатор байт-кода работает, изменяя значение этого счетчика, чтобы выбрать следующую команду байт-кода, которая должна быть выполнена. Надо полагаться на этот счетчик для завершения.
Объясните, что здесь могут быть маленькие друзья, которые все еще очень невежественны. Ранее мы говорили о процессе выполнения Java-программ.
Файл исходного кода Java (.javaСуффикс) будет скомпилирован в файл байт-кода компилятором Java (.classСуффикс), затем
Загрузчик классов в JVM загружает файлы байт-кода каждого класса, и после завершения загрузки механизм выполнения JVM выполняет его.
Так как многопоточность виртуальной машины Java реализуется путем чередования и выделения времени выполнения процессора, в любой момент процессор будет выполнять инструкции только в одном потоке. Поэтому, чтобы иметь возможность возобновить отправку в правильную позицию после переключения потока, нам нужен счетчик программ, чтобы записать место выполнения программы. Возьмите объяснение процессора: каштан: когда вы читаете кирпичную книгу, ваша мать сказала вам есть, как толстый дом, вы должны быть счастливы выбросить книгу. Но когда вы продолжаете читать книгу, вы не знаете, где вы ее увидели. Как это решить? В следующий раз я добавлю закладку. Это является причиной счетчика программы, который записывает позицию выполнения байт-кода программы и может продолжать выполнять оставшийся код, когда текущий поток восстанавливает ЦП.
Из приведенного выше объяснения становится ясно, что разные потоки не должны совместно использовать счетчик программ,Каждый поток должен иметь независимый программный счетчик. Счетчики каждого потока не влияют друг на друга и хранятся независимо. Мы называем этот тип области памяти «частным потоком». Согласно спецификации JVM, если поток выполняет метод Java, программный счетчик сохраняет адрес байт-кода инструкции, которая должна быть выполнена в данный момент; если поток выполняет собственный метод, значение в программном счетчике пустое ( не определено). Мы упоминали ранее: счетчик программ — это небольшой адрес памяти. Эта область памяти является единственной областью на виртуальной машине Java, в которой не указана ошибка OutOfMemoryError. Размер пространства, занимаемого данными, хранящимися в счетчике программы, не изменится при выполнении программы. Точно так же, как закладка, где бы она ни застряла, она не изменится сама по себе.
2. Стек виртуальных машин Java
Стек виртуальных машин Java (стек виртуальных машин Java) чем-то похож на стек C. Как и программный счетчик, стек виртуальных машин Java также является частным потоком. Стек виртуальной машины описываетJava-методМодель памяти реализована:
Каждый метод создаст кадр стека (Stack
Frame) используется для хранения таблицы локальных переменных, стека операндов, динамической ссылки, выхода метода и другой информации. Процесс каждого метода от вызова до завершения выполнения соответствует процессу вставки кадра стека в стек в виртуальной машине.
Объяснение Чжоу Да можно сказать очень упрощенным. Каждый раз, когда программа выполняет метод, выделяется кадр стека, и установленный кадр стека помещается в стек. После выполнения метода кадр стека будет вытолкнут из стека. Следовательно, можно видеть, что кадр стека, соответствующий методу, выполняемому в данный момент потоком, должен располагаться в верхней части стека Java. Говоря об этом, каждый должен понимать, почему легко вызвать переполнение стековой памяти при использовании рекурсивного метода.
- Таблица локальных переменных
Таблица локальных переменных, как следует из названия, используется для хранения переменных базовых типов данных, известных компилятору (8 базовых типов данных), ссылки на объекты (ссылочные типы, для переменных ссылочного типа, сохраняется ссылка на начальный адрес объекта). Указатель) и тип returnAddress (адрес возврата метода, когда метод выполняется, он должен вернуться в то место, где он был ранее вызван, поэтому адрес возврата метода должен быть сохранен в кадре стека). Размер таблицы локальных переменных может быть определен компилятором, поэтому размер таблицы локальных переменных не изменится во время выполнения программы.
- Стек операндов
Стек операндов, друзья, которые должны были изучить стек в структуре данных, должны быть знакомы с проблемой оценки выражений.Одним из наиболее типичных применений стека является оценка выражений. Подумайте о процессе выполнения метода методом потока, который на самом деле является процессом непрерывного выполнения операторов, а в конечном счете — процессом выполнения вычислений. Таким образом, можно сказать, что все вычисления в программе выполняются с помощью стека операндов.
- Два ненормальных состояния
Исключение StackOverflowError
Если запрашиваемая потоком глубина стека превышает глубину, разрешенную виртуальной машиной, будет выдано исключение StackOverflowError.
Исключение OutOfMemoryError
Если стек виртуальной машины можно динамически расширять, и если для расширения не может быть применено достаточно памяти, будет выдано исключение OutOfMemoryError.
3. Локальный метод стека
Стек локальных методов и стек виртуальных машин играют очень похожие роли. Поток также является частным, разница между ними заключается в том, что стек виртуальных машин обслуживает методы Java, а стек локальных методов используется виртуальной машиной.Native Метод обслуживания. В спецификации JVM нет обязательного положения о конкретном методе реализации и структуре данных, разработанных в этом месте, и виртуальная машина может реализовать его свободно. В виртуальной машине HotSopt локальный стек методов и стек Java напрямую объединены в одну. Как и стек виртуальных машин, стек локальных методов также генерирует исключения StackOverflowError и OutOfMemoryError.
4. куча Java
Java HeapЭто самый большой фрагмент памяти, управляемый виртуальной машиной Java. Куча Java является общей для всех потоков. Единственная цель Java-кучи — хранить экземпляры объектов. Здесь почти все экземпляры объектов и массивы выделяются здесь.Это описано в «Спецификации виртуальной машины Java»:
Все экземпляры объектов и массивы должны быть размещены в куче, но с развитием компилятора JIT и технологии анализа выхода постепенно созревает, распределение в стеке, оптимизация скалярной замены вызовет некоторые тонкие изменения, все объекты в куче Распределение также стало менее «абсолютным».
Куча Java является основной областью сборщика мусора, поэтому ее часто называют кучей «GC».
5. Область метода
Область метода (Method Area), как и куча, является областью памяти, используемой общим потоком. В области методов хранится информация о каждом классе (включая имя класса, информацию о методе и информацию о поле), статические переменные, константы и код, скомпилированный компилятором.
В файле Class, в дополнение к информации описания, такой как поля, методы и интерфейсы класса, есть часть информации — пул констант, который используется для хранения литералов и ссылок на символы, сгенерированных во время компиляции.
Постоянный пул времени выполнения (Runtime Constant Pool) является частью области метода и является представлением времени выполнения постоянного пула каждого класса или интерфейса. После загрузки классов и интерфейсов в JVM создается соответствующий пул времени выполнения. Выходи Конечно, не содержимое пула констант файла Class может входить в пул констант времени выполнения. В процессе работы новые константы также могут быть помещены в пул констант времени выполнения, например, метод String intern.
В спецификации JVM нет обязательного требования, чтобы область метода осуществляла сборку мусора. Многие привыкли называть область метода «постоянным поколением», потому что виртуальная машина HotSpot реализует область метода в постоянном поколении, так что сборщик мусора JVM может управлять этой областью подобно области кучи, поэтому в этой части нет необходимости. Разработать механизм сбора мусора. Однако, начиная с JDK7, виртуальная машина Hotspot удалила пул строковых констант из постоянной генерации.
6. Прямая память
Прямая память не является областью памяти, определенной в спецификации виртуальной машины Java.
Недавно добавленный класс NIO в JDK1.4 вводит форму ввода-вывода, основанную на каналах и буферах, он может использовать собственные функции для непосредственного выделения памяти вне кучи, а затем сохранять ее в куче Java через Объект DirectByteBuffer in работает как ссылка на эту память. Это может значительно улучшить производительность в некоторых местах, поскольку позволяет избежать копирования данных назад и вперед между кучей Java и собственной кучей.
Прямая память этой машины ограничена общей памятью этой машины.
Справочные материалы:
https://www.cnblogs.com/dolphin0520/p/3613043.html
«Углубленное понимание виртуальной машины Java»
runtime — Во время выполнения против компиляции
ИМХО нужно читать много ссылок , ресурсов, чтобы составить представление о разнице между ВС во время выполнения во время компиляции, потому что это очень сложный предмет .
Я перечислим некоторые из этой картинки/ссылки, которые я рекомендую .
Кроме того, что сказано выше хочу добавить, что иногда одна картинка стоит 1000 слов :
- порядок этого две: первая-это время компиляции, а затем запустить
Скомпилированная программа может быть открыта и выполняется пользователем. Когда приложение выполняется, называется время выполнения :
время компиляции, а затем runtime1
;
CLR_diag время компиляции, а затем runtime2
from Wiki
https://en. wikipedia.org/wiki/Run_time
https://en.wikipedia.org/wiki/Runtime(program_lifecycle_phase)
Время выполнения, время выполнения, или выполнения может обратиться к:
Вычислительной
Время выполнения программы (фазы жизненного цикла) период, в течение которого компьютерная программа выполняется
Рантайм библиотеки, программа библиотека предназначена для реализации функций, встроенных в язык программирования
Во время выполнения системы, программа предназначена для поддержки выполнения программ для ЭВМ
Выполнение программного обеспечения, процесс выполнения поручений по одному во время фазы выполнения
Список компиляторов
https://en.wikipedia.org/wiki/List_of_compilers
- поиск в Google и сравнить ошибки во время выполнения против ошибки компиляции:
;
- На мой взгляд очень важно знать :
3.1 разница между строить против компиляции и жизненного цикла сборки
https://maven. apache.org/guides/introduction/introduction-to-the-lifecycle.html
3.2 разница между этим 3 вещи : собрать против строить против выполнения
https://www.quora.com/What-is-the-difference-between-build-run-and-compile
Фернандо Падоан, разработчик, что’s просто немного любопытно на язык дизайна
Ответил 23 Февраля
Я иду назад по отношению к другим ответы:
бег становится некоторым двоичный исполняемый файл (или скрипт, для интерпретируемых языков), ну… выполняется как новый процесс на компьютере;
компиляция-это процесс анализа программы, написанной в некотором языке высокого уровня (выше, если сравнивать с машинным кодом), проверка синтаксиса это, семантика, связываемые библиотеки, возможно, сделать некоторые оптимизации, то создание двоичного исполняемого файла программу в качестве выходного. Этот исполняемый файл может быть в форме машинного кода, или какой-то байт-код — что это, инструкция таргетинг какой-то виртуальной машины;
дом, как правило, включает в себя проверку и предоставлении зависимостей, изучая код, компиляция кода в двоичный, выполнение автоматических тестов и упаковывая полученный двоичный[х] и прочие активы (изображения, файлы конфигурации, библиотеки и т. д.) в некоторых конкретных формате развертывания файла. Обратите внимание, что большинство процессов не являются обязательными, а некоторые зависят от целевой платформы, для которой выполняется сборка. В качестве примера, упаковка Java-приложения для Tomcat будет выводить .файл война. Построение исполняемого файла Win32 из кода C++ можно просто вывести .exe программы, или могли бы также упаковать его внутри .установщик MSI.
Оптимизация производительности Java приложений
Не смотря на то, что описываемые ниже трюки и советы работают не только в J2ME, именно для мобильных приложений они имеют первостепенное значение в силу ограниченности ресурсов платформы.
Техникиоптимизации производительности, как правило, основаны на увеличении объема памяти, необходимой программе для работы. К сожалению, ресурсы платформы Java ME очень ограничены, и программисту приходится постоянно балансировать между производительностью и экономией системных ресурсов. На мой взгляд, рано начатая оптимизация кода ведет к усложнению и замедлению процесса разработки, поэтому большинство приведенных тут советов лучше применять уже на завершающей фазе разработки, когда уже все отлажено и работает.
1. Избегайте синхронизации
Известно, что код, в котором используется механизм синхронизации, примерно в 4 раза медленнее обычного кода. Независимо от конкретной реализации Java VM использование синхронизации требует от виртуальной машины большого количества дополнительных действий: она должна отслеживать блокировки, блокировать контекст при начале работы с ним и разблокировать, когда работа с контекстом закончена. Потоки, которые хотят получить доступ к заблокированному контексту вынуждены стоять в очереди и ждать его освобождения. Думаю, что привел достаточно убедительные доводы, и Вы будете использовать синхронизацию только там, где без нее действительно невозможно обойтись.
2. Используйте предварительные вычисления
Если Вы разрабатываете игру с 3D или 2. 5D графикой, то наверняка используете массу математических вычислений с тригонометрическими функциями. Такие расчеты сильно нагружают процессор, поэтому стоит заранее просчитать наиболее сложные выражения и представить их в виде массива, откуда доставать готовые значения в процессе выполнения программы. Помимо графики существует масса приложений, где предварительные вычисления можно сделать заранее и оформить в виде массивов данных.
3. Вытягивание массивов
Доступ к элементам массива занимает больше времени, чем работа с обычными переменными. Многомерные массивы — еще более медленная история. Избегайте использования многомерных массивов. В большинстве случав они легко заменяются одномерными.
Пример
// До оптимизации int[][] table;// Таблица 4x4 // После оптимизации int[] table;// Таблица 1x16
А еще одномерные массивы потребляют меньше динамической памяти, чем их многомерные собраться.
4.
Разворачивание циклов for
Циклы — это замечательная штука, но они несут в себе дополнительные накладные расходы. Вместе с вызовом тела цикла на каждом шаге выполняется операция увеличения счетчика и проверка условия. Например
void printMsg(){ for(int loop=0; loop<15; loop++){ System.out.println(msg); } }
Выполняется виртуальной машиной следующим образом:
void printMsg(){ int loop=0; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System. out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; System.out.println(msg); if(loop>=15){ return; } loop++; }
Я специально сделал этот список таким длинным, чтобы Вы почувствовали, насколько наша жизнь стала проще с появлением циклов. Однако многие программисты привыкли видеть в цикле только абстракцию и не задумываются о накладных расходах, сопряженных с их использованием.
Если вы на этапе программирования точно знаете число необходимых итераций, Вы можете частично развернуть цикл:
void printMsg(){ for(int loop=0; loop<15; loop+=5){ System.out.println(msg); System.out.println(msg); System.out.println(msg); System.out.println(msg); System.out.println(msg); } }
Эта реализация будет повторяться всего 3 раза, соответственно накладные расходы уменьшатся в 5 раз по сравнению с предыдущим примером.
Не смотря на вполне очевидный выигрыш от использования разворачивания циклов, не слишком увлекаться этой техникой. В результате разворачивания генерируется больший по размеру байт код, и может сложиться ситуация, когда тело цикла не поместится полностью в кеш процессора. В результате будут задействованы механизмы подгрузки-выгрузки частей кеша, и ваш оптимизированный код будет сильно тормозить.
5. Сжатие циклов for
Смысл операции сжатия заключается в вынесении за рамки цикла всего того, что не нуждается в повторном вычислении:
// Плохо оптимизированный код for(int loop=0; loop<10; loop++){ int a=5; int b=10; int c= a* b+ loop; } // А вот здесь - все в порядке int a=5; int b=10; for(int loop=0; loop<10; loop++){ int c= a* b+ loop; }
Во втором примере переменным a и b значение присваивается всего один раз. Таким образом, по сравнению с первым вариантом нам удалось избавиться от 20 лишних операций присваивания значений.
Приведу еще одну менее очевидную технику сжатия циклов. Никогда не вызывайте методы вычисления размеров в заголовке цикла. Сравните:
//Плохой код for(int loop=0; loop< msgs.size(); loop++){ System.out.println(msgs.get(loop)); } // Хороший код int msgCount= msgs.size(); for(int loop=0; loop< msgCount; loop++){ System.out.println(msgs.get(loop)); }
В первом случае на каждом шаге вычисляется размер msgs. Во втором случае — это делается один раз до начала цикла. Конечно, эта оптимизация подразумевает, что тело цикла никак не влияет на размер msgs.
6. Избегайте интерфейсных вызовов методов
В байткоде Java существует 4 типа методов. Ниже они перечислены в порядке уменьшения скорости вызова.
invokestatic
Статические методы не используют экземпляр класса, поэтому им не нужно соблюдать правила полиморфизма и искать подходящий параметрам вызова экземпляр.
invokevirtual
Обычные методы.
invokespecial
Специальные методы — конструкторы, private и super class методы.
invokeinterface
Требуют поиск подходящей реализации интерфейсного метода.
Тип используемых методов влияет на весь дизайн приложения, поэтому помните о скорости вызовов методов в процессе разработки.
Вызов статических методов обеспечивает наилучшую производительность, поскольку Java VM не нужно ничего искать. Вызов интерфейса напротив — самый медленный путь, требующий два поиска.
7. Избегайте ненужных обращений к массиву
Обращение к элементу массива — не самая быстрая операция. Если Вы видите, что какой-то элемент используется в алгоритме многократно — сохраните его в переменную, которую затем и используйте.
8. Избегайте использования аргументов
При вызове нестатических методов вы неявно передаете ссылку this вместе с другими параметрами. В Java VM вызов методов реализован по принципу стека. При каждом вызове аргументы заносятся в стек, а затем извлекаются из него при выполнении метода. В некоторых случаях можно избежать необходимости использования стека, отказавшись от передачи аргументов.
// Плохо return multiply(int a, int b){ return a* b; } // Хорошо int result; int a; int b; void multiply(){ result= a* b; }
С точки зрения высокоуровневого программирования приведенные в примере стили очень похожи, однако второй метод работает быстрее. Еще большей скорости вызова можно добиться, если объявить все переменные и методы как статические. В этом случае при вызове метода стек не будет задействован вообще.
9. Откажитесь от использования локальных переменных
Локальные переменные помещаются и извлекаются из стека при вызове каждого метода. С точки зрения производительности гораздо эффективнее использовать обычные переменные.
10. Не используйте getter-ы/setter-ы
Откажитесь от использования методов, устанавливающих и считывающих значения полей класса и обращайтесь к ним напрямую. Конечно, это немного противоречит базовым принципам ООП, но с точки зрения оптимизации этот шаг вполне оправдан. Обычно я избавляюсь от этих методов на финальной стадии разработки проекта. Это редкий случай, когда оптимизация производительности связана с уменьшением размера программы.
11. Эффективная математика
С точки зрения производительности, не все математические операции равны по скорости выполнения. Быстро работают сложение и вычитание. Умножение, деление, вычисление модуля — заметно медленнее. Самыми быстрыми являются побитывые операции.
// Плохо int a=5*2; // Уже лучше int a=5+5; // Хорошо: побитовый сдвиг эквивалентен умножению и делению на 2 int a=5<<1;// Сдвигаем в 5 (0000 0101) 1-бит влево, получаем 10 (0000 1010). // Еще лучше int a=10;// Подумайте, действительно ли вы хотите // что-то считать во время выполнения программы?
Этими методами оптимизации пользовались еще праотцы, когда писали первые игры под DOS. Для Java они тоже подходят.
12. Пишите кратко
Применяйте операторы типа +=, поскольку они генерируют короткий байткод.
\\Плохо x=x+1; \\Хорошо x+=1;
13. Используйте встроенные методы
Пользуйтесь методами, предоставляемыми платформой. Например, использование System.arraycopy для копирования элементов массива будет более эффективным, чем аналогичная собственная реализация.
14. Используйте StringBuffer вместо String
Если Вы работаете со строками, значения которых могут меняться по ходу выполнения программы, используйте класс StringBuffer вместо String. Любое изменение объекта String приводит к созданию нового объекта.
Заключение
Выше были приведены простейшие методы оптимизации, которые могут с успехом использоваться при разработке приложений для мобильной платформы J2ME. Эти методы основаны на анализе байт-кода и позволяют выработать стиль программирования, обеспечивающий оптимальный результат.
Источники:j2medevcorne
javajazzup.com
Перевод:Александр Ледков
Как повысить быстродействие вашей программы | GeekBrains
Элементарные приемы.
https://d2xzmw6cctk25h.cloudfront.net/post/853/og_cover_image/4affef55e59a548052479e0a0a247845
Данный текст будет посвящён в большей степени тем, кто только делает свои первые шаги в программировании и о производительности совершенно не задумывается. А между тем, практически все основные шаги по увеличению скорости выполнения вашей программы непосредственно связаны с компьютерной грамотностью и пониманием происходящих процессов. Итак, оставим общие фразы и перейдём непосредственно к сути.
Не навреди
На скорость выполнения вашей программы влияет 2 непосредственных фактора:
- Скорость исполнения кода
- Объём выделяемой памяти
Как вы понимаете, минимизировать одновременно влияние обоих этих факторов невозможно: заигрывания с памятью неизбежно приводят к раздуванию кода, а «малобуквенный» текст к повышенному потреблению ресурсов исполняемой машиной. Поэтому главным правилом оптимизации является исключение излишеств в вашей программе. Это означает никакого лишнего кода и строго ограниченное использование памяти. На словах это легко, а на деле?
Операции по оптимизации
Если задача оптимизации встала перед вами уже после написания кода, то наиболее разумным решением будет предварительная очистка и разбиение на части с последующим изучением времени выполнения отдельных блоков. Очистка предполагает удаление неиспользуемых участков, переменных, избыточных заходов циклов.
Анализ скорости выполнения после разбиения чистого кода даст вам объективную оценку того, в какую сторону направить свои усилия по оптимизации. Логично, что если одна операция выполняется 40% времени, а другая – 2%, то и разработчик должен куда больше усилий приложить к уменьшению производственных потерь в первом случае.
Так как мы предполагаем, что код уже чистый, то необходимо в первую очередь рассмотреть выполнение следующих правил:
- Минимизировано количество используемых переменных. Например, ваша программа изобилует различными циклами. По правилам красивого кода, вы можете создать для каждого цикла свой уникальный счётчик, но с точки зрения оптимизации – это трата драгоценных ресурсов. Если в одной части программы переменная “отработала”, то её вполне можно применить в другой.
- Правильно выбраны типы данных. Как известно, каждый тип данных имеет свой используемый диапазон, то есть собственно тот размер памяти, который резервируется под его использование. Например, создавая код в Java и имея переменную, способную принимать только два значения (например, «on» и «off»), лучше использовать boolean с созданием последующего соответствия, но никак не char.
- Минимизировано количество присваиваний. Опять-таки, руководствуясь принципами красоты кода, вы можете разбить длинное арифметическое выражение на несколько более мелких. Это чревато появлением избыточных переменных и лишними операциями присваивания, что позитивно на быстродействии точно не скажется.
- Переменные инициируются при объявлении. Это правило хорошего тона в программировании не только повысит производительность, исключая лишние операции, но и избавит от невынужденных ошибок.
- Однотипные повторяющиеся операции объединены в процедуру или функцию. Еще один приём из основ программирования, который, тем не менее, часто игнорируется новичками.
- Однотипные циклы объединены. Допустим, у вас есть несколько массивов одинаковой размерности, которые надо заполнить в цикле. Вы можете создать несколько циклов и повысить читабельность или запихнуть все операции в общий цикл и повысить быстродействие. Решать вам.
- Использованная память немедленно очищается. Безусловно, не стоит удалять каждую переменную сразу после окончания её использования, но когда речь идет о работе с существенным объемом памяти (например, с большими массивами), контролировать потребление ресурсов просто необходимо.
Когда оптимизация не имеет смысла
Безусловно, подобные приемы не являются универсальными, а в случае с рядом языков и компиляторов (как в случае Dart в JavaScript с помощью dart2js), вы и вовсе получите настолько оптимизированный код, что собственными руками вряд ли создадите что-то лучше.
Однако, в любом случае, опыт применения описанных выше приемов поможет новичку перейти из разряда хороших программистов в разряд эффективных, ведь так вы сэкономите не только ресурсы машины, но и свое личное время.
А какие приёмы для оптимизации кода используете вы?
Как измерить прошедшее время выполнения в Java
Существует двумя способами измерения прошедшего времени выполнения в Java : с помощью System.currentTimeinMillis () или с помощью System.nanoTime (). Эти два метода могут использоваться для измерения прошедшего времени или времени выполнения между двумя вызовами метода или событиями в Java. Вычисление прошедшего времени — это одна из первых вещей, которую делает Java-программист, чтобы узнать, сколько секунд или миллисекунд требует выполнение метода или сколько времени занимает конкретный блок кода.Большинство программистов на Java знакомы с System.currentTimeInMillis (), которая присутствует с самого начала, в то время как новая версия утилиты для более точного измерения времени System. nanoTime представлена в Java 1.5 вместе с несколькими новыми функциями на таких языках, как Generics, Enum types, автобокс и переменные аргументы или varargs.
Вы можете использовать любой из них для измерения времени выполнения метода в Java. Хотя для более точного измерения временных интервалов лучше использовать System.nanoTime ().
Между прочим, если вы новичок в среде Spring, я также предлагаю вам присоединиться к всестороннему и актуальному курсу для более глубокого изучения Spring. Если вам нужны рекомендации, я настоятельно рекомендую вам взглянуть на Spring Framework 5: от новичка до Guru , один из всеобъемлющих и практических курсов по изучению современной Spring. Он также наиболее актуален и охватывает весну 5.
Это также очень доступно, и вы можете купить его всего за 10 долларов на распродажах Udemy, которые время от времени происходят.
Пример программы Java для измерения времени выполнения в Java
Вот пример кода для измерения времени, прошедшего между двумя блоками кода с с использованием System. nanoTime. Многие библиотеки Java с открытым исходным кодом, такие как Apache commons-lang, Google commons и Spring, также предоставляют служебный класс StopWatch, который можно использовать для измеряет прошедшее время в Java .
StopWatch улучшает читаемость, чтобы минимизировать ошибку вычислений при вычислении прошедшего времени выполнения, но будьте осторожны, что StopWatch не является поточно-ориентированным и не должен использоваться совместно в многопоточной среде, и его документация четко говорит, что это больше подходит в среде разработки и тестирования для основных измерение производительности, а не вычисление времени в производственной среде.
import org.springframework.util.StopWatch;
/ **
* Простая программа на Java для измерения прошедшего времени выполнения в Java
* В этой программе на Java показаны два способа измерения времени на Java с использованием System.nanoTime (), который был добавлен в
* в Java 5, и StopWatch, который это служебный класс из Spring Framework.
* /
public class MeasureTimeExampleJava {
public static void main ( String args []) {
// измерение прошедшего времени с использованием системы.nanoTime
long startTime = System .nanoTime ();
для ( int i = 0; i <1000000; i ++) {
Object obj = new Object ();
}
long elapsedTime = Система .nanoTime () — startTime;
System .out.println («Общее время выполнения для создания 1000K объектов в Java в миллисекундах:»
+ elapsedTime / 1000000);
// измерение прошедшего времени с помощью Spring StopWatch
StopWatch watch = new StopWatch ();
часы.Начните();
для ( int i = 0; i <1000000; i ++) {
Object obj = new Object ();
}
watch.stop ();
System .out.println («Общее время выполнения для создания 1000K объектов в Java с использованием StopWatch в миллисекундах:»
+ watch. getTotalTimeMillis ());
}
}
Вывод:
Общее время выполнения для создания 1000K объектов в Java в миллисекундах: 18
Общее время выполнения для создания 1000K объектов в Java с использованием StopWatch в миллисекундах: 15
Какой из них следует использовать для измерения времени выполнения в Java
Это зависит от того, какие параметры доступны вам, если вы работаете с JDK 1 ниже.5, чем System.currentTimeInMillis (), является лучшим вариантом с точки зрения доступности, тогда как после JDK 1.5 System.nanoTime отлично подходит для измерения прошедшего времени, поскольку он более точен и использует точные системные часы и может измерять с точностью до наносекунды .
Хотя, если вы используете какую-либо из упомянутых выше библиотек с открытым исходным кодом Java, в основном Spring, чем StopWatch, также является лучшим вариантом, но, как я уже сказал ранее, StopWatch не является поточно-ориентированным и должен использоваться только в среде разработки и тестирования.
Подобно тому, как SimpleDateFormat не является потокобезопасным, и вы можете использовать ThreadLocal для создания SimpleDateFormat для каждого потока, вы можете сделать то же самое и с StopWatch. Но я не думаю, что секундомер — это такой тяжелый объект, как SimpleDateFormat.
Вот и все, что касается , как измерить прошедшее время или время выполнения в Java . Возьмите за привычку измерять производительность и время выполнения важной части кода, особенно методов или циклов, которые выполняются большую часть времени. Это те места, где небольшая оптимизация кода приводит к большему увеличению производительности.
Дополнительная литература
Spring Framework 5: от новичка до гуру
Полный Java MasterClass
Core Java, Volume II — Advanced Features
Spring Fundamentals
Другой Руководство по программированию на Java , которое может вам понравиться:
Сбор и анализ данных о времени выполнения — Java Enterprise Performance
Существует два широко используемых метода сбора данных о времени выполнения: измерение на основе времени и измерение на основе событий. Оба стали стандартными частями продуктов для управления производительностью, и оба обычно представляют данные в виде стека, поэтому внешне выглядят одинаково. Мы подробно рассмотрим каждый из них, указав на фундаментальные различия в их подходах к измерению данных и в том, что они означают с точки зрения интерпретации данных.
Измерение по времени
При измерении на основе времени стеки всех выполняющихся потоков приложения захватываются с постоянным интервалом (частотой дискретизации).Затем проверяется стек вызовов, и для верхнего метода устанавливается состояние потока — выполнение или ожидание. Чем чаще метод появляется в выборке, тем больше время его выполнения или ожидания.
Каждая выборка показывает, какой метод активно выполнялся, но нет никаких мер продолжительности выполнения или того, когда метод завершает выполнение. Это имеет тенденцию пропускать более короткие вызовы методов, как если бы они не выполнялись активно. В то же время другие методы могут иметь завышенный вес, потому что они активны во время, но не между периодами выборки. По обеим причинам это затрудняет сбор точных данных о времени выполнения для отдельного метода.
Выбранный временной интервал определяет степень детализации и качество измерения. Более длительное время измерения также увеличивает вероятность того, что данные будут более репрезентативными и что упомянутые ошибки будут иметь меньшее влияние. Это простая статистика. Чем больше у нас выборок, тем больше вероятность того, что методы, которые выполняются чаще, появятся как выполняющиеся методы в стеке вызовов.
Рисунок 1.12: Стеки каждого потока анализируются каждые 20 мс.
Этот подход создает собственные ошибки измерения, но обеспечивает простой способ отслеживания проблем с производительностью. Возможно, наиболее важным преимуществом является то, что накладные расходы на измерения можно контролировать с помощью интервала выборки; чем длиннее интервал, тем меньше накладные расходы. Эти низкие накладные расходы особенно важны в производственной среде при полной нагрузке. Вот почему:
Мы видим, что конкретный пользовательский запрос выполняется медленно, но мы не можем видеть используемые входные данные.В то же время у нас нет сведений о фактическом количестве выполнений для конкретных методов. Временные меры не могут предоставить эту информацию. Собранные данные не дают нам возможности определить, когда выполнение отдельного метода выполняется медленно или когда метод выполняется слишком часто. Анализ проблем такого рода требует использования измерения на основе событий.
Измерение на основе событий
Вместо выборок резьбы измерение на основе событий анализирует выполнение отдельных методов.Мы регистрируем временную метку в начале и в конце каждого вызова метода, что позволяет подсчитать количество вызовов отдельных методов и определить точное время выполнения каждого вызова. Это простой вопрос вычисления разницы между собранными отметками времени.
Рисунок 1.13: Для измерения на основе событий регистрируется время начала и окончания каждого вызова метода.
Java Runtime предлагает специальные обратные вызовы через JVMPI / JVMTI, которые вызываются в начале и в конце выполнения метода.Однако этот подход сопряжен с большими накладными расходами, которые могут существенно повлиять на поведение приложения во время выполнения. Современные методы измерения производительности используют инструменты байт-кода, требующие очень небольших накладных расходов. Также есть дополнительная функциональность инструментария только наиболее подходящих методов и возможность захвата контекстной информации, такой как параметры метода.
Транзакционные измерения
Для сложных приложений мы используем более сложные формы измерения на основе событий.Таким образом, в дополнение к хранению показателей выполнения метода, измерение транзакций хранит контекст, что позволяет изучать методы различных транзакций по отдельности. Таким образом мы можем узнать, что конкретная медленная инструкция SQL выполняется только для определенного веб-запроса.
Выполнения метода также сохраняются в индивидуальном хронологическом порядке вместо агрегирования на основе стека вызовов, что может выявить динамическое поведение логики обработки транзакции — например, запроса сервлета.Таким образом, изолированное изучение отдельных транзакций позволяет анализировать проблемы и выбросы, зависящие от данных.
Подробная диагностика, обеспечиваемая транзакционными измерениями, особенно хорошо подходит для работы с распределенными приложениями. Существует несколько способов корреляции межуровневых транзакций:
Подходы на основе типа транзакции передают идентификатор типа транзакции на следующий уровень и дополнительно используют корреляцию на основе времени. Это дает более подробную информацию не только о времени, но и о типе транзакции (например,г. URL) учитывается. Корреляция на основе идентификатора транзакции передает идентификаторы отдельных транзакций на следующий уровень. Это позволяет отслеживать отдельные транзакции на разных уровнях и является наиболее точным подходом к межуровневой корреляции.
Рисунок 1.14: Каждый метод регистрируется с использованием времени начала и окончания для расчета общего времени выполнения.
Гибридные подходы
Гибридные методы сочетают измерение производительности на основе времени и событий, чтобы использовать преимущества обоих и избежать отдельных недостатков.Основное преимущество сбора данных на основе событий заключается в захвате каждого вызова и его контекста, например параметров метода. Основное преимущество подхода, основанного на времени, заключается в постоянных и независимых от нагрузки накладных расходах.
Гибридное измерение объединяет данные на основе транзакционных событий со снимками потоков, которые могут быть назначены для конкретной транзакции. Инструментарий гарантирует, что мы можем фиксировать контекст транзакции, а также параметры метода и другие детали, в то время как данные на основе времени обеспечивают подход с низкими накладными расходами для сбора информации о времени выполнения.
Этот подход предлагает наиболее точное представление о поведении приложения во время выполнения, а почти постоянные накладные расходы хорошо подходят для мониторинга производства. На рисунке показана трассировка транзакции, объединяющая оба источника данных друг с другом. Данные на основе событий показаны синим цветом, а данные на основе времени — зеленым.
Рисунок 1.15: Гибридная трассировка транзакций (зеленые участки были добавлены через моментальные снимки потоков)
Java 14 повышает производительность труда разработчиков за счет новых функций производительности
Java 14 дебютировал 17 марта, став первым крупным обновлением языка программирования Java в 2020 году.
Java 14 выходит через шесть месяцев после выпуска Java 13 в сентябре 2019 года, что соответствует плану Oracle по выпуску как минимум двух крупных обновлений Java в год. Среди новых функций в Java 14 — возможности повышения производительности труда разработчиков и видимости кода, а также повышения общей производительности.
Одна из новых функций, Java Enhancement Proposal 361 ( JEP 361 ), делает Switch Expressions стандартной функцией Java. Тем временем производительность повышается за счет использования JEP 345 для выделения памяти с учетом NUMA для сборщика мусора Java G1.
Java Flight Recorder Потоковая передача событий
Ключевой частью Java Development Kit является функция Java Flight Recorder (JFR), которая предоставляет инструмент, помогающий профилировать приложения Java в диагностических целях. В Java 14 в Java Flight Recorder появилась новая возможность потоковой передачи данных.
«Потоковая передача событий JFR ( JEP 349 ) — это новая функция в Java 14, которую, я думаю, люди смогут использовать сразу», — сказал в интервью ITPro Today Жорж Сааб, вице-президент по разработке платформы Java в Oracle. .«Это меняет исторический способ использования данных Java Flight Recorder, который является своего рода пакетным форматом, на то, что теперь он доступен как своего рода поток событий, с которым вы можете работать в режиме реального времени».
Согласно Saab, поток событий
JFR полезен везде, где работает виртуальная машина Java (JVM), будь то локально или в облаке. Он добавил, что новая функция поможет разработчикам легче понять, что происходит во время выполнения приложения во время выполнения программы.
Полезные исключения NullPointerExceptions
Еще одна новая функция в Java 14 — JEP 358 для полезных исключений NullPointerExceptions. Исключение нулевого указателя — это распространенный тип ошибки кодирования, при которой указатель в коде не указывает на допустимую переменную.
«Речь идет о том, чтобы люди могли понять, когда они сталкиваются с исключением с нулевым указателем, что они точно знают, откуда они пришли и как они могут справиться с этим быстрее и эффективнее», — сказал Сааб.
Java 15 и более поздние версии
Java 14 также включает ряд функций, которые будут находиться на стадии предварительного просмотра или инкубации, пока они не станут достаточно зрелыми для полной стандартной доступности. Одной из таких функций является Packaging Tool ( JEP 343 ), которая сейчас находится на стадии инкубации.
Инструмент упаковки помогает разработчикам создавать автономные приложения Java. По словам Saab, вместо того, чтобы предоставлять среду выполнения, такую как Java Runtime Environment (JRE), отдельно от кода приложения, который может предоставить разработчик, JEP 343 дает людям способ объединить все это вместе.
«Таким образом, вы можете использовать jlink для создания упрощенной среды выполнения, которая настраивается только для вашего приложения и включает только то, что нужно вашему приложению», — сказал он. «Затем используйте упаковочный инструмент, чтобы собрать все это в формат, который можно будет доставить как единое целое».
Что такое виртуальная машина Java и ее архитектура
Что такое JVM?
Виртуальная машина Java (JVM) — это механизм, который обеспечивает среду выполнения для управления кодом Java или приложениями. Он преобразует байт-код Java в машинный язык. JVM является частью Java Run Environment (JRE). В других языках программирования компилятор создает машинный код для конкретной системы. Однако компилятор Java создает код для виртуальной машины, известной как виртуальная машина Java.
Вот как работает JVM
Во-первых, код Java преобразуется в байт-код. Этот байт-код интерпретируется на разных машинах.
Между хост-системой и источником Java байт-код является промежуточным языком.
JVM в Java отвечает за выделение пространства памяти.
Работа виртуальной машины Java (JVM)
В этом руководстве по JVM вы узнаете:
Архитектура JVM
Теперь в этом руководстве по JVM давайте разберемся с архитектурой JVM. Архитектура JVM в Java содержит загрузчик классов, область памяти, механизм выполнения и т. Д. Архитектура виртуальной машины Java
1) ClassLoader
Загрузчик классов — это подсистема, используемая для загрузки файлов классов. Он выполняет три основные функции, а именно.Загрузка, связывание и инициализация.
2) Область методов
Область методов JVM хранит структуры классов, такие как метаданные, постоянный пул времени выполнения и код для методов.
3) Куча
Все объекты, связанные с ними переменные экземпляра и массивы хранятся в куче. Эта память является общей и разделяется между несколькими потоками.
4) Языковые стеки JVM
Языковые стеки Java хранят локальные переменные и их частичные результаты.Каждый поток имеет свой собственный стек JVM, создаваемый одновременно с созданием потока. Новый фрейм создается всякий раз, когда вызывается метод, и удаляется, когда процесс вызова метода завершен.
5) Регистры ПК
Регистры ПК хранят адрес инструкции виртуальной машины Java, которая выполняется в данный момент. В Java каждый поток имеет свой отдельный регистр ПК.
6) Стеки собственных методов
Стеки собственных методов содержат инструкции собственного кода, в зависимости от собственной библиотеки. Он написан на другом языке вместо Java.
7) Механизм выполнения
Это тип программного обеспечения, используемого для тестирования оборудования, программного обеспечения или целых систем. Механизм выполнения теста никогда не несет никакой информации о тестируемом продукте.
8) Интерфейс собственных методов
Интерфейс собственных методов — это среда программирования. Он позволяет Java-коду, который выполняется в JVM, вызывать библиотеки и собственные приложения.
9) Библиотеки собственных методов
Собственные библиотеки — это набор собственных библиотек (C, C ++), которые необходимы механизму выполнения.
Процесс компиляции и выполнения программного кода
Для того, чтобы написать и выполнить программу, вам потребуется следующее:
1) Редактор — Для ввода вашей программы можно использовать блокнот
2) Компилятор — для преобразования вашей многоязыковой программы в машинный код
3) Компоновщик — для объединения ссылок на различные программные файлы в вашей основной программе вместе.
4) Загрузчик — для загрузки файлов с вашего вторичного запоминающего устройства, такого как жесткий диск, флэш-накопитель, компакт-диск, в оперативную память для выполнения. Загрузка выполняется автоматически, когда вы выполняете свой код.
5) Выполнение — Фактическое выполнение кода, который обрабатывается вашей ОС и процессором.
Исходя из этого, просмотрите следующее видео и изучите внутреннюю работу JVM и архитектуру JVM (виртуальная машина Java).
Нажмите здесь, если видео недоступно.
Процесс компиляции и выполнения кода C
Чтобы понять процесс компиляции Java в Java.Давайте сначала кратко рассмотрим процесс компиляции и компоновки в C.
Предположим, что в основном вы вызвали две функции f1 и f2. Основная функция хранится в файле a1.c.
Функция f1 хранится в файле a2. c
Функция f2 хранится в файле a3.c
Все эти файлы, то есть a1.c, a2.c и a3.c, передаются компилятору . Выходные данные — соответствующие объектные файлы, являющиеся машинным кодом.
Следующим шагом будет объединение всех этих объектных файлов в один.exe-файл с помощью компоновщика. Компоновщик объединит все эти файлы вместе и создаст файл .exe.
Во время выполнения программы программа-загрузчик загружает файл .exe в оперативную память для выполнения.
Компиляция и выполнение кода Java в Java VM
Теперь в этом руководстве по JVM давайте посмотрим на процесс для JAVA. В вашем основном у вас есть два метода f1 и f2.
- Основной метод хранится в файле a1.java
- f1 хранится в файле как a2.java
- f2 хранится в файле как a3.java
Компилятор скомпилирует три файла и создаст 3 соответствующих файла .class, состоящих из кода BYTE. В отличие от C, связывание не выполняется .
Виртуальная машина Java или виртуальная машина Java находится в ОЗУ. Во время выполнения с использованием загрузчика классов файлы классов переносятся в ОЗУ. БАЙТ-код проверяется на наличие нарушений безопасности.
Затем механизм выполнения преобразует байт-код в собственный машинный код. Это как раз вовремя компиляции.Это одна из основных причин, почему Java работает сравнительно медленно.
ПРИМЕЧАНИЕ. JIT или своевременный компилятор является частью виртуальной машины Java (JVM). Он интерпретирует часть байтового кода, которая одновременно выполняет аналогичные функции.
Почему Java является одновременно интерпретируемым и компилируемым языком?
Языки программирования классифицируются как
- Язык высокого уровня Пример. C ++, Java
- Языки среднего уровня Пр. C
- Язык низкого уровня Ex Assembly
- , наконец, самый низкий уровень машинного языка.
Компилятор — это программа, которая преобразует программу с одного уровня языка на другой. Пример преобразования программы на C ++ в машинный код.
Компилятор java преобразует высокоуровневый код java в байт-код (который также является типом машинного кода).
Интерпретатор — это программа, которая преобразует программу на одном уровне в другой язык программирования на том же уровне . Пример преобразования программы Java в C ++.
В Java генератор Just In Time Code преобразует байт-код в собственный машинный код, который находится на тех же уровнях программирования.
Следовательно, Java является как компилируемым, так и интерпретируемым языком.
Почему Java работает медленно?
Две основные причины медлительности Java:
- Динамическое связывание: В отличие от C, связывание выполняется во время выполнения, каждый раз, когда программа запускается на Java.
- Интерпретатор времени выполнения: Преобразование байтового кода в собственный машинный код выполняется во время выполнения в Java, что еще больше снижает скорость
Однако последняя версия Java решила проблему узких мест в производительности. степень.
Сводка :
- Полная форма JVM — это виртуальная машина Java. JVM в Java — это движок, который управляет кодом Java. Он преобразует байт-код Java в машинный язык.
- Архитектура JVM в Java содержит загрузчик классов, область памяти, механизм выполнения и т. Д.
- В JVM код Java компилируется в байт-код. Этот байт-код интерпретируется на разных машинах.
- JIT — это компилятор Just-in-time. JIT является частью виртуальной машины Java (JVM).Он используется для ускорения времени выполнения.
- По сравнению с другими компиляторами, JVM в Java может работать медленнее.
JIVE: Java Interactive Visualization Environment
JIVE — это интерактивная среда выполнения для
Затмение, что
обеспечивает визуализацию выполнения программы Java
на разных уровнях детализации.
Как начинающие, так и продвинутые Java-программисты извлекут выгоду из
Богатые визуализации объектных структур и методов JIVE
взаимодействие, а также умение шагать вперед
и наоборот в исполнении.
JIVE обеспечивает поисковую систему по структурам времени выполнения и помогает
выявлять ошибки, возникающие в любой момент истории выполнения
без необходимости вручную выполнять код.
JIVE помогает разработчикам программного обеспечения, обеспечивая понимание
работа корректных программ на Java. Поддерживает визуализацию
через определенные промежутки времени, чтобы специалист по сопровождению программного обеспечения мог сосредоточиться только на
части кода, которые изменяются.
JIVE поддерживает большие выполнения с помощью фильтров исключения, визуализации по интервалам и динамического среза.
JIVE можно использовать со стандартным JDK или Android SDK.
Интерактивные визуализации
JIVE отображает как состояние выполнения, так и историю вызовов программы в визуальном
манера. Состояние выполнения визуализируется как расширенная диаграмма объектов,
отображение структуры объекта, а также активации методов в соответствующих контекстах объекта.
История звонков представлена в виде расширенной схемы последовательности, где каждый
исполнение резьбы показано другим цветом,
разъяснение взаимодействий с объектами, которые происходят во время выполнения.Диаграммы
масштабируемый и может быть отфильтрован для отображения только информации, относящейся к задаче, на
рука. Расширение плагина JIVE поддерживает
компактные диаграммы последовательности для абстрактного представления истории выполнения.
Отладка на основе запросов
Традиционная отладка — это процедурный процесс , в котором
программист должен продолжить
шаг за шагом и объект за объектом, чтобы найти причину ошибки. В
напротив, JIVE поддерживает декларативный подход к отладке с помощью
предоставление расширяемого набора запросов по всему
история выполнения программы, а не только стек невыполненных вызовов.Запросы формулируются с использованием исходного кода или диаграмм, а результаты
отображаются в табличном формате, а также в виде аннотаций к диаграммам. JIVE также
поддерживает динамическое нарезание для уменьшения визуализации
и сосредоточьтесь на первопричине ошибок.
Обратный шаг
JIVE поддерживает как прямое, так и обратное пошаговое выполнение программ Java.
Часто программист может обнаружить, что ошибка произошла только после
ошибочная инструкция была выполнена.Предоставление возможности отступить назад экономит
программист время и усилия повторного выполнения программы до точки
ошибка. JIVE также дает возможность сразу вернуться к любому предыдущему
точку в истории выполнения, чтобы наблюдать диаграмму объекта в этой точке. Обеспечить регресс
пошаговое выполнение и прыжок тесно взаимодействуют с отладкой на основе запросов, чтобы сузить
причина ошибок программы.
Диаграммы состояния и проверка имущества
В
Расширение плагина диаграммы состояний позволяет пользователю извлекать модель с конечным состоянием
из трассировки выполнения программы, запущенной под JIVE, и проверьте свойства около
эта модель.Это расширение поддерживает два представления: представление диаграммы состояний,
и просмотр проверки свойств. В представлении диаграммы состояний пользователь вводит
Отслеживает выполнение JIVE и выбирает поля программы Java, которые должны
составляют вектор состояния. Отображаемая диаграмма состояний показывает прогресс
изменений состояния для этих полей. Диаграмму состояний можно просмотреть в уменьшенном
форма, указав критерии абстракции модели через простое текстовое поле. в
Просмотр Property Checker, свойства модели можно указать с помощью
текстовое поле.Глобальные свойства, которым должны удовлетворять все государства, наличие
состояния, удовлетворяющие определенным свойствам, а также пути между состояниями могут
можно указать с помощью простого языка спецификации свойств.
областей данных времени выполнения Java
Области данных среды выполнения Java
Примечание: — Поскольку хакерские заметки будут закрыты, вы можете найти эту статью здесь: — Статья
Во время выполнения программы JVM выделяет память.Некоторые области данных времени выполнения относятся только к потоку. Ниже приведен список различных областей данных времени выполнения: —
1. Heap
2. Stack
3. Pc Register
4. Method Area
5. Собственный стек
1. HEAP: —
Память кучи создается JVM при запуске программы и используется для хранения объектов. Память кучи может быть доступна для любого потока, который дополнительно разделен на три поколения Молодое поколение
, Старое
и PermGen
(постоянное поколение).Когда объект создается, он сначала переходит в Молодое поколение (особенно в пространство Эдема), когда объекты стареют, затем он перемещается в Старое / существующее поколение. В пространстве PermGen хранятся все статические переменные и переменные экземпляра пар имя-значение (ссылки на имя объекта). Ниже приведено изображение, показывающее структуру кучи java.
Вы можете вручную увеличить размер кучи с помощью некоторых параметров JVM, как показано на изображениях. Предположим, у нас есть простой класс HackTheJava, который затем увеличивает свою память следующими параметрами: —
java -Xms = 1M -XmX = 2M «Имя класса»
2.Стек: —
Стек создается с каждым потоком, созданным программой. Он связан потоком. У каждого потока есть свой стек. Все локальные переменные и вызовы функций хранятся в стеке. Его жизнь зависит от жизни потока, так как поток будет живым, и наоборот. Его также можно увеличить вручную: —
java -Xss = 512M «Имя класса»
При заполнении стека выдает ошибку StackOverFlow
.
3. Регистр ПК
Он также связан своим потоком.В основном это адрес текущей инструкции, которая выполняется. Поскольку для каждого потока некоторые наборы методов, которые будут выполняться, зависят от регистра ПК. Он имеет значение для каждой инструкции и не определено для собственных методов. Обычно это делается для отслеживания инструкций.
4. Область метода
Это память, которая совместно используется всеми потоками, например, Heap. Он создается при запуске виртуальной машины Java. Он содержит код, фактически скомпилированный код, методы и его данные и поля.Пул констант времени выполнения также является частью области методов. Память для него по умолчанию выделяется JVM и при необходимости может быть увеличена. Пул констант времени выполнения — это представление констант для каждого класса. Он содержит все литералы, определенные во время компиляции, и ссылки, которые будут решены во время выполнения.
5. Стек собственных методов
Собственные методы — это методы, написанные на языках, отличных от java. Реализации JVM не могут загружать собственные методы и не могут полагаться на обычные стеки.Он также связан с каждым потоком. Короче говоря, он такой же, как стек, но используется для собственных методов.
класс Пример
{
int instance_variable = 0;
статический int static_variable = 0;
void func (int a)
{
int local_variable = 0;
возвращаться ;
}
}
Надеюсь, вам понравилась эта статья. Спасибо
Среда выполнения Java 8
С помощью App Engine вы можете создавать веб-приложения, использующие Google
масштабируемая инфраструктура и сервисы.App Engine запускает ваше веб-приложение
с использованием Java 8 JVM. Приложение
Движок вызывает классы сервлетов вашего приложения для обработки запросов и подготовки
ответы в этой среде.
Платформа App Engine предоставляет множество встроенных
API-сервисы, которые
ваш код может позвонить. Ваше приложение также может настроить
запланированные задачи, которые выполняются в
заданные интервалы.
Указание среды выполнения Java 8 для вашего приложения
Чтобы ваше приложение использовало среду выполнения Java 8, добавьте следующую строку
в ваш appengine-web.xml
файл:
java8
Обратите внимание, что существующие приложения App Engine, которые ранее использовали
среда выполнения Java 7 будет работать в среде выполнения Java 8, просто сделав это изменение.
API App Engine для Java представлен appengine-api - *. Jar
включен в SDK App Engine как часть Cloud SDK
(где *
представляет версию API и SDK App Engine).
Предупреждение. Автономная версия SDK App Engine, доступная как
отдельная загрузка, теперь не рекомендуется.Вам следует использовать инструменты на основе Cloud SDK.
Вы выбираете версию API, которую использует ваше приложение, включив этот JAR в
каталог приложения WEB-INF / lib /
или используйте Maven для обработки зависимостей. Если новый
выпущена версия среды выполнения Java, в которой внесены изменения,
несовместимы с существующими приложениями, эта среда будет иметь новую
номер основной версии.
Использование Maven для обработки зависимостей
Вы можете использовать Maven для управления всеми зависимостями.Например, это
pom.xml
запись включает последнюю версию App Engine API
( Rappengine-api-1.0-sdk
), доступный в Maven Central:
<зависимость>com.google.appengine appengine-api-1.0-sdk 1.9.86
ОК
Песочница
Среда выполнения Java App Engine распределяет запросы приложений по
несколько веб-серверов и предотвращает взаимодействие одного приложения с другим.Приложение App Engine не должно медленно реагировать. Веб-запрос к
приложение должно быть обработано в пределах лимита времени ожидания запроса. Процессы, которые превышают этот предел для ответа, прекращаются, чтобы избежать
перегрузка веб-сервера.
Обратите внимание, что единственное место, где пользователи могут записывать файлы, — это каталог / tmp
.
Файлы в / tmp
будут занимать память, выделенную вашему экземпляру. Файлы
хранящиеся в этом месте доступны только для этого экземпляра и только для
время жизни этого конкретного экземпляра.
Обычный способ для вашего приложения получить файлы ресурсов — это упаковать файлы
вы полагаетесь на свое приложение под WEB-INF
, а затем загружаете их из своего приложения
используя Class.getResource ()
, ServletContext.getResource ()
или аналогичные методы.
По умолчанию все файлы в WAR являются «файлами ресурсов». Вы можете исключить файлы из
для этого набора используется файл appengine-web.xml
.
Класс погрузчика JAR, заказ
Иногда может потребоваться переопределить порядок, в котором файлы JAR
просканированы на предмет классов, чтобы разрешить конфликты между именами классов.В
В этих случаях приоритет загрузки может быть предоставлен конкретным файлам JAR путем добавления
элемент, содержащий
элементов в
файл appengine-web.xml
. Например:
<конфигурация-загрузчика классов>
<спецификатор-приоритета filename = "mailapi.jar" />
Это помещает « mailapi.jar
» как первый файл JAR, в котором будет выполняться поиск классов,
кроме тех, что находятся в каталоге war / WEB-INF / classes /
.
Если приоритетным является несколько файлов JAR, их исходный порядок загрузки (с
по отношению друг к другу) будут использоваться. Другими словами, порядок
сами элементы значения не имеют.
Нитки
В среде выполнения Java 8 вы можете создавать потоки с помощью App Engine
ThreadManager API
и встроенные API Java, например new Thread ()
.
В настоящее время, если вы хотите вызывать API-интерфейсы App Engine ( com.google.appengine.api. *
),
вы должны вызывать эти API из потока запроса или из потока, созданного с помощью
API ThreadManager.
Примечание: Если вы используете API создания потоков Java,
Cloud Trace не будет отображаться
ID запроса правильно. В настоящее время Google работает над решением этой проблемы.
Приложение может
Если вы создаете ThreadPoolExecutor
с currentRequestThreadFactory ()
, тогда
выключение ()
должен быть явно вызван до завершения запроса сервлета.В противном случае
вызовет невыполнение запроса и сбой сервера приложений.
Обратите внимание, что некоторые библиотеки могут создавать для вас ThreadPoolExecutors.
Приложение может выполнять операции с текущим потоком, например
thread.interrupt ()
.
Каждый запрос ограничен 50 параллельными потоками запросов API App Engine.
При использовании потоков используйте параллелизм высокого уровня
объекты,
такие как Executor
и Runnable
.Они заботятся о многих тонких, но
важные детали параллелизма, такие как
Прерывания
и планирование и
бухгалтерия.
Максимальное количество одновременных фоновых потоков, созданных
App Engine API — 10 на экземпляр. (Это ограничение не распространяется на обычные
Потоки Java, не связанные с API App Engine.)
Примечание. Google рекомендует во всех новых проектах использовать
Инструменты на основе Cloud SDK, которые поддерживают
Maven,
Gradle,
Intellij и
Затмение.
SDK App Engine для Java (с
Плагин Maven и
Плагин Gradle
support) также полностью поддерживается и рекомендуется для существующих проектов, которые
не готовы к переходу на инструменты на основе Cloud SDK.
Поддерживаемые IDE
Cloud Tools for Eclipse добавляет новые мастера проектов и отладку
конфигурации вашей Eclipse IDE для
Проекты App Engine. Вы можете развернуть свои проекты App Engine
жить в производство изнутри Eclipse.
Cloud Tools for IntelliJ позволяет запускать и отлаживать
Приложения App Engine внутри IntelliJ IDEA.
Вы можете развернуть свои проекты App Engine в рабочем режиме без
выходя из IDE.
Поддерживаемые инструменты сборки
Чтобы ускорить процесс разработки, вы можете использовать приложение
Плагины движка для Apache Maven или Gradle:
Локальный сервер разработки
Сервер разработки
запускает ваше приложение на локальном компьютере для разработки и
тестирование.Сервер имитирует службы хранилища данных. В
сервер разработки также может генерировать конфигурацию для Datastore
индексы на основе запросов, которые приложение выполняет во время тестирования.
AppCfg
Предупреждение: Инструмент appcfg
теперь
устарело. Ты
следует использовать инструмент командной строки Cloud SDK gcloud
и
Maven и
Плагины Gradle, которые предоставляют
те же функции, что и у инструментов SDK App Engine, таких как AppCfg.
AppCfg — это
входит в состав автономного SDK App Engine для Java.Это
многоцелевой инструмент, который обрабатывает взаимодействие с вашим приложением из командной строки
работает на App Engine. AppCfg может загрузить ваше приложение в
App Engine или просто обновите индекс Datastore
конфигурации, чтобы вы могли создавать новые индексы перед обновлением кода. Он также может
загрузить данные журнала приложения, чтобы вы могли анализировать производительность своего приложения, используя
ваши собственные инструменты.
Параллелизм и задержка
Задержка вашего приложения больше всего влияет на количество экземпляров.
необходимо для обслуживания вашего трафика.Если вы обрабатываете запросы быстро, единственный экземпляр
может обрабатывать множество запросов.
Однопоточные экземпляры могут обрабатывать один одновременный запрос.
Следовательно, существует прямая зависимость между задержкой и количеством
запросы, которые могут обрабатываться на экземпляре в секунду. Например, 10 мс
задержка равна 100 запросам в секунду на экземпляр.
Многопоточные экземпляры могут обрабатывать множество одновременных запросов. Поэтому там
это прямая зависимость между потребляемым процессором и количеством
запросов / сек.
приложений Java
поддержка одновременных
запросы, так что
один экземпляр может обрабатывать новые запросы, ожидая других запросов к
полный. Параллелизм значительно сокращает количество экземпляров вашего приложения
требуется, но вам необходимо разработать приложение для многопоточности.
Например, если B4
пример
(приблизительно 2,4 ГГц) потребляет 10 млн циклов на запрос, вы можете обработать 240
запросов / сек / экземпляр. Если он потребляет 100 млн циклов / запрос, вы можете обработать 24
запросов / сек / экземпляр. Эти числа являются идеальным случаем, но достаточно
реалистично с точки зрения того, что вы можете сделать на инстансе.
Переменные среды
Среда выполнения устанавливает следующие переменные среды:
Переменная среды | Описание |
---|---|
GAE_APPLICATION | Идентификатор вашего приложения App Engine. Этот идентификатор имеет префикс « код региона ~». например, «e ~» для приложений, развернутых в Европе. |
GAE_DEPLOYMENT_ID | Идентификатор текущего развертывания. |
GAE_ENV | Среда App Engine. Установить на стандарт . |
GAE_INSTANCE | Идентификатор экземпляра, на котором в настоящее время работает ваша служба. |
GAE_RUNTIME | Среда выполнения, указанная в файле app.yaml . |
GAE_SERVICE | Имя службы, указанное в вашем приложении .yaml файл. Если имя службы не указано, устанавливается значение по умолчанию . |
GAE_VERSION | Метка текущей версии вашей службы. |
GOOGLE_CLOUD_PROJECT | Идентификатор облачного проекта, связанный с вашим приложением. |
ПОРТ | Порт, который принимает HTTP-запросы. |
В приложении можно определить дополнительные переменные среды.yaml
файл,
но указанные выше значения изменить нельзя.
.