| 15.08.2010 10:00 | |
Аннотация
Из статьи вы узнаете о проекте «Оптимизация доступа к загрузчикам классов, определяемых пользователем JVM», которым мы занимались в рамках Летней школы Intel 2010. Цель проекта заключалась в оптимизации механизма определения загрузчика класса в Java Virtual Machine, который стал «бутылочным горлышком», негативно влияющим на производительность приложений, активно использующих механизм сериализации при работе на многоядерных системах.
Введение
Язык Java позволяет пользователю переопределять загрузчики классов, что является аспектом одной из основных особенностей этого языка – динамической загрузки классов. Эта возможность достаточно популярна у программистов и, в частности, занимает существенное место в популярном тесте производительности SPECjvm2008. Однако инфраструктура HotSpot JVM, предоставляющая доступ к пользовательским загрузчикам классов, оказалась не готова к современным архитектурам процессоров, когда количество ядер в одиночном сервере может достигать 64-х и более. Она стала «бутылочным горлышком», существенно ограничивая производительность приложений, интенсивно использующих этот механизм. Задачей является оптимизация доступа к пользовательским загрузчикам классов и как следствие радикальное увеличение произодительности теста SPECjvm2008.serial на платформе Nehalem-EX.
Успешная реализация проекта обеспечит повышение производительности процессоров Intel в популярном тесте SPECjvm2008 и поможет упрочить лидерство Intel в сегменте серверных платформ.
Моим напарником на данной задаче был Владимир Костюков. Менеджером – Вячеслав Шакин, а менторами – Алексей Шпиталёв и Николай Сидельников.
Немного истории...
Впервые проблема была замечена при сравнении результатов запуска теста SPECjvm2008.serial на платформе Dunnington для различного количества потоков:
Таблица 1. Зависимость количества очков на тесте от количества потоков
| Количество потоков | Количество "очков" |
| 6 | 136 |
| 12 | 244 |
| 16 | 275 |
| 18 | 281 |
| 24 | 282 |
Из таблицы 1 видно, что по достижении некоторого максимума производительности (280 очков) дальнейшее увеличение количества потоков почти не оказывает влияния на производительность.
При просмотре верхушки стека одного из рабочих потоков подозрение вызвал native метод, использующий достаточно емкие по времени функции JVM. Этим методом оказался private static native ClassLoader latestUserDefinedLoader() класса ObjectInputStream служащий для определения объекта-загрузчика, необходимого для загрузки класса десериализуемого объекта. Замечание: Под "емкими по времени" подразумеваются методы, в которых система проводит большое количество времени. Временные затраты на каждую функцию были определены с помощью Intel® VTune™. Для проверки гипотезы о том, что данный метод оказывает отрицательное влияние на производительность и масштабируемость, был проведен эксперимент. Значение, возвращаемое методом, было закэшировано, что позволило вызывать метод только один раз. Экспериментальное API показало прирост производительности.
Причины проблемы
Для понимания причин проблем с производительностью посмотрим внимательнее на наш «проблемный» метод:
JVM_ENTRY(jobject, JVM_LatestUserDefinedLoader(JNIEnv *env))
for (vframeStream vfst(thread); !vfst.at_end(); vfst.next()) {
// UseNewReflection
vfst.skip_reflection_related_frames(); // Only needed for 1.4 reflection
klassOop holder = vfst.method()->method_holder();
oop loader = instanceKlass::cast(holder)->class_loader();
if (loader != NULL) {
return JNIHandles::make_local(env, loader);
}
}
return NULL;
JVM_END
Данный метод служит для определения загрузчика, которым следует загружать десериализуемый класс. Самой затратной частью в данном методе оказался вызов vfst.next(), который используется для итерирования по фреймам стека исполнения. Подробное описание проблемы можно прочитать в статье моего напарника Владимира Костюкова Оптимизация доступа к загрузчикам классов, определенных пользователем JVM. Часть 1.
Пути решения проблемы
Существует, как минимум, два узких места/проблемы в описанном выше механизме получения последнего ненулевого загрузчика на стеке исполнения. Во-первых, сам по себе нативный вызов JVM достаточно дорогой по отношению к вызовам в JRE. Во-вторых - высота стека, а следовательно и количество итераций цикла - относительно большие величины на момент вызова метода JVM_LatestUserDefinedLoader(). Первая проблема решается с помощью кеширования загрузчика в JRE. Вторая - переносом точки вызова метода JVM_LatestUserDefinedLoader() в метод readObject().
Реализацией решения стал следующий патч класса ObjectInputStream:
…
private ClassLoader cachedUserDefinedLoader;
…
public final Object readObject() throws IOException, ClassNotFoundException
{
+ // caching caller class loader if and only if is non-recursive calling
+ if (curContext == null) {
+ cachedUserDefinedLoader = latestUserDefinedLoader();
+ }
…
}
protected Class<?> resolveClass(String classname) throws IOException, ClassNotFoundException
{
- return Class.forName(name, false, latestUserDefinedLoader());
+ return Class.forName(name, false, cachedUserDefinedLoader == null
+ ? (cachedUserDefinedLoader = latestUserDefinedLoader())
+ : cachedUserDefinedLoader);
}
Результаты применения патча API
Было произведено измерение производительности HotSpot JVM c оригинальным и измененным API на платформах Nehalem-EX и Dunnington следующих конфигураций:
Nehalem-EX Server: 2.27 GHz, 8 процессоров по 8 ядер в каждом, 128 GB DDR3-1067
Dunnington Server : 2.27 GHz, 4 процессора по 6 ядер в каждом, 32 GB DDR2-667
Для проведения тестирования были использованы параметры, соответствующие конфигурациям платформ:
- Максимальное количество потоков было равным количеству ядер соответствующей машины.
- Операционная система: RHEL5.3 2.6.18-128.el5 x86_64 GNU/Linux
- Параметры запуска теста SPECjvm2008: 120 секунд на warmup ("разогрев" виртуальной машины), и измерение производительности в течение 240 секунд
- Версия JVM: Sun JDK6u20P, x64 Linux X64
- Дополнительные параметры JVM, касающиеся оперативной памяти:
- Nehalem-EX: -Xmn56g –Xms64g –Xmx64g
- Dunnington: -Xmn12g -Xms16g -Xmx16g
Запуски теста производились по три раза, и, затем, из трех результатов выбирался наибольший. Для расчета ускорения (% Boost) была использована следующая формула:
% Boost = (|OriginalScores - PatchedScores|)/OriginalScores
где OriginalScores - результаты теста на оригинальном Java API
PatchedScores - результаты теста на патченном Java API
На платформе Nehalem-EX тесты запускались на различном количестве потоков: от 8 до 64 с шагом 4. На графике мы видим значительное увеличение производительности, по сравнению с оригинальным API при увеличении количества потоков. Итоговое ускорение составило 28 % на максимальном количестве – 64 потока. Максимальное же ускорение, было получено на 32 потоках и составило 48% !
Таблица 2. Результаты тестирования на платформе Nehalem
| Количество потоков |
Оригинальное API | Измененное API | Ускорение, % |
| 8 | 234 | 264 | 13 |
| 12 | 349 | 393 | 13 |
| 16 | 469 | 523 | 12 |
| 20 | 532 | 629 | 18 |
| 24 | 606 | 734 | 21 |
| 28 | 620 | 838 | 35 |
| 32 | 634 | 941 | 48 |
| 36 | 674 | 992 | 47 |
| 40 | 714 | 1032 | 45 |
| 44 | 740 | 1045 | 41 |
| 48 | 772 | 1079 | 40 |
| 52 | 794 | 1127 | 42 |
| 56 | 894 | 1166 | 30 |
| 60 | 924 | 1184 | 28 |
| 64 | 939 | 1201 | 28 |
Рисунок 1. График показателей теста на платформе Nehalem-EX
На платформе Dunnington был получен максимальный прирост производительности 11%, но на максимальном количестве потоков прирост был 2% (Таблица 3).
| Количество потоков |
Оригинальное API | Измененное API | Ускорение, % |
| 24 | 284 | 289 | 2 |
Была проанализирована статистика вызовов при выполнении теста serial, в результате чего было определено, что:
Во-первых, количество вызовов функции сократилось в 2.5 раза (40000 против 16000);
Во-вторых, высота стека, вглубь которого необходимо пройти функции, уменьшилась в 4-6 раз (12-20 против 3).
Рисунок 2. Различия количества вызовов для оригинального и измененного API
Рисунок 3. Различия высоты стека для оригинального и измененного API
Дополнительно запускался тест protobuf-compare, который сравнивает производительность различных библиотек Java для сериализации. На диаграммах (Рисунок 4, 5) показано общее время выполнения теста на различных библиотеках. Причем меньшее значение соответствует большей скорости. Данный тест запускался на платформе Nehalem.
Рисунок 4. Результаты запуска protobuf-compare на оригинальном API
Рисунок 5. Результаты запуска protobuf-compare на измененном API
Мы видим, что время выполнения теста стандартной сериализации (самая нижняя строка на диаграммах) сократилось на 18 % (Таблица 4), на результаты остальных тестов изменение API не оказало значительного влияния.
Таблица 4. Результаты выполнения теста protobuf-compare
| Оригинальное API |
Измененное API | Ускорение, % API |
| 82479 сек. | 67437 сек. | 18 |
Для обеспечения некоторой уверенности, что патч не нарушает функционирование платформы Java был использован Java Compatibility Kit 6b. Конечно, нельзя сказать, что проверка на данной группе тестов гарантирует полную корректность предложенного изменения API, но задача гарантирования корректности не входила в наши планы.
Результаты работы
В результате проделанной работы было достигнуто увеличение производительности теста SPECjvm2008 на платформе Nehalem на 28% при 64 потоках, максимальный прирост составил 48%. Патч Java API был предложен для рассмотрения в Oracle.
Все это свидетельствует о том, что поставленные цели были достигнуты, и задача выполнена на 100%.
Впечатления от стажировки
И, напоследок, конечно же впечатления от Intel Summer School 2010!
Я уже второй раз участвую в Летней Школе Intel, и надо сказать, что эти две летние школы кардинально различаются по своей организации. В прошлом году мы работали в лаборатории Intel НГУ и вся корпоративная жизнь обошла нас, что говорится, стороной. В этом же году нам посчастливилось посмотреть на Intel изнутри. Работа внутри корпорации оставила только позитивные впечатления. Организация работы в офисе находится на очень высоком уровне, а дружелюбный коллектив и удобные рабочие места способствовали плодотворной работе. В начале нас «напугали» строгостью корпоративной политики безопасности, но на самом деле выполнять эти требования оказалось достаточно просто. Конечно же, мне не хватало моих любимых сервисов Google и операционной системы Ubuntu Linux, но к неудобствам в виде Microsoft Outlook и Microsoft Windows быстро привыкаешь :)
В рамках Летней Школы мы как работали над проектами, так и посещали образовательные тренинги и вебинары, благодаря которым я открыл для себя много нового и значительно повысил свой профессиональный уровень. Проведение вебинаров – это совершенно новый опыт образовательных мероприятий, и, конечно, не сразу все проходило гладко, но организаторы успешно справились со всеми сложностями. Интересно было побывать на телефонном совещании с сотрудниками из зарубежных офисов, на котором мы с Владимиром представляли результаты нашей работы. Конечно, было сложно воспринимать совещание на английском языке в обстановке постоянных шумов, но все завершилось хорошо и зарубежные коллеги оценили наши достижения. Также, во время стажировки, мне (уже во второй раз) посчастливилось побывать в Центре компетенции СО РАН – Intel по высокопроизводительным вычислениям. Мне уже была знакома программа экскурсии с прошлого года, но в этом году нам показали реальные результаты научных вычислений на суперкомпьютерах.
Не могу не упомянуть и про развлекательную программу. За эти короткие 2 месяца мы поучаствовали в праздновании шестилетия офиса, и даже победили на импровизированных олимпийских играх:) Успели побывать на множестве Дней Рождений сотрудников, на которые принято приглашать всех друзей. И, в общем, замечательно провели время.
Благодарю всех организаторов Летней Школы, и, конечно же,выражаю особую благодарность Николаю Куртову как директору Новосибирской школы. А также нашим с Владимиром менторам – Алексею и Николаю, постоянно помогавшим нам с проектом.
Пожалуйста, обратитесь к странице Уведомление об оптимизации для более подробной информации относительно производительности и оптимизации в программных продуктах компании Intel.
Комментарии (10) 
| 05.10.2010 13:34
Василий
|
И еще. "На диаграммах показано общее время выполнения теста на различных библиотеках." -К сожалению не нашел в статье диаграмм, кроме рисунка два. "Конечно же, мне не хватало моих любимых сервисов Google" -Каких например? Вроде все были доступны, со всеми можно было работать |
| 06.10.2010 09:03
danil_skachkov
|
>на самом деле такой вывод по данной таблице не очевиден. Нужно было бы посмотреть на большем количестве потоков. Количество потоков для проведения тестирования выбиралось исходя из общего количества ядер на конкретной машине. В данном случае на машине с процессорами Dunnington 6 x 4 = 24 ядра. Мы пробовали запускать тесты и на большем количестве потоков, но после данной отметки количество "очков" идет на снижение (что и понятно, т. к. происходят переключения потоков), поэтому результаты данных тестов мы не учитывали. >А почему на нехалеме нету такого поведения, когда по достижению определенного максимума дальнейшее увеличение потоков почти не оказывает влияние? Так может проблема в Dunnington, может он просто не умеет разбивать на больше 20 потоков? На Nehalem зависимость количество очков/количество потоков ведет себя немного по другому. На графике (Рисунок 1) видно, что прирост производительности при увеличении количества потоков постепенно уменьшается (график более "пологий" к концу). Поэтому негативное влияние количества потоков все-таки есть. Результаты показали, что наше изменение не привело к кардинальному увеличению производительности на процессоре Dunnington, возможно, есть именно в нем какая-то проблема. Но именно на основе результатов тестов на Dunnington возникла идея исследовать поглубже механизм сериализации, приведшая к достижению хороших результатов на Nehalem. |
| 06.10.2010 09:15
danil_skachkov
|
>А в чем разница между Dunnington и Nehalem? Какие у них параметры? Это процессоры различных архитектур. Nehalem новее чем Dunnington со всеми вытекающими. А если писать подробнее, то, боюсь, это тема для отдельной статьи :) Параметры, которые я знаю, я упомянул в описании конфигураций тестовых машин. |
| 06.10.2010 09:19
danil_skachkov
|
>"На диаграммах показано общее время выполнения теста на различных библиотеках." >К сожалению не нашел в статье диаграмм, кроме рисунка два. Ой, ошибочка. Извиняюсь. Сейчас добавлю :) >Каких например? Вроде все были доступны, со всеми можно было работать В частности не хватало Gmail и Google Docs, которыми нельзя было пользоваться для рабочих целей из соображений безопасности. |
| 11.10.2010 02:20
danil_skachkov
| Эх... Что-то никак не обновят статью со вставленными диаграммами. |
| 11.10.2010 02:21
danil_skachkov
| *всмысле не отмодерируют |
| 13.10.2010 04:56
danil_skachkov
| Ура, обновили:) |
| 13.10.2010 12:25
Vitaly Slobodskoy (Intel)
| Судя по всему, характер проделанных изменений влияет на масштабируемость лишь косвенно: уменьшается количество времени, затрачиваемое на выполнение функции + (что стало ясно лишь из комментария Владимира к части 1) уменьшается время на синхронизацию (хотя там же, в ч.1, указывается на "single-threaded использование"). Если задача стояла как "поднять производительность", а не "повысить масштабируемость", то не очень понятен смысл тестирования на столь многоядерных системах.. |
| 14.10.2010 11:13
Vladimir Kostyukov |
Виталий, все не совсем так. На самом деле, single-threaded использование применимо исключительно к классу ObjectInputStream над кодом которого мы работали. Однако, нативный вызов, который и стал узким местом масштабируемости, использовал общие участи памяти - ссылки на рашаренный байт-код классов. Тут важно понимать, что речь идет о разных уровнях платформы джава. О JRE, в которой ObjectInputStream класс - однопоточный и о самой JVM, в которой метод JVM_LatestUserDefinedLoader() является по сути критической секцией (хоть и не явно). Поэтому, сократив количество итераций, мы добились сокращения локов при генерации следующего виртуального фрейма (операция next()), тем самым действительно решив как проблему производительности так и масштабируемости, что и доказывают графики. Виталий, если вам требуются дополнительные коментарии по поводу критической секции дайте мне знать - я попробую прояснить ситуацию. |



Василий
789
-на самом деле такой вывод по данной таблице не очевиден. Нужно было бы посмотреть на большем количестве потоков.
А почему на нехалеме нету такого поведения, когда по достижению определенного максимума дальнейшее увеличение потоков почти не оказывает влияние?
Так может проблема в Dunnington, может он просто не умеет разбивать на больше 20 потоков?
Одним словом, из таблицы один сказанного вами выше не видно...
А в чем разница между Dunnington и Nehalem? Какие у них параметры?