Смотрим на ассемблерный код работающего Java приложения

Nov 23, 2011 12:52


Проводя эксперименты над производительностью разного кода иногда становиться не понятно, почему то или иное изменение кода тем или иным образом отражается на производительности, есть ли возможность дальнейшего увеличения производительности или всё - предел достигнут. В этом случае часто помогает взгляд на ассемблерный код тех кусков кода, которые потребляют больше всего времени.
В предыдущей записи я показал замеры времени работы простой итерации со сложением элементов по массиву целых чисел на Java и на C++. Время работы получается очень похожим, что как бы намекает. Но случайность ли это или действительно компилятор C++ и компилятор Java HotSpot Server выдают похожий ассемблерный код? Я отвечу на этот вопрос, а заодно расскажу как подсмотреть ассемблерный код, который создает JVM.
На C++ это не составляет труда. Компилятору, gcc, например, можно передать ключик, "-S", который заставляет его скопилировать исходные файлы в ассемблер и остановиться. Запустив "g++ -O3 -funroll-loops -S IntVectorIterationTiming.cpp" и посмотрев на получившися ".s" файл, легко найди то, во что превратилась основная итерация суммирования элементов вектора целых числ. GCC использует AT&T синтаксис ассеблера (приемник результата записывается в последнем аргументе).
L3: addl (%ecx,%edx,4), %eax addl 4(%ecx,%edx,4), %eax addl 8(%ecx,%edx,4), %eax addl 12(%ecx,%edx,4), %eax addl 16(%ecx,%edx,4), %eax addl 20(%ecx,%edx,4), %eax addl 24(%ecx,%edx,4), %eax addl 28(%ecx,%edx,4), %eax addl $8, %edx cmpl %ebx, %edx jne L3
Цикл развернут так, чтобы выполнять по 8-м операций за раз. Код инициалиции, который обрабатывает случаи, когда количество итераций не кратно 8 я не показал (но он, естественно, там тоже есть). Каждая итерация представлена одной инструкцией add, которая добавляет очередное целое число к регистру eax.
Для того, чтобы посмотреть ассемблерный код создаваемый HotSpot-ом, первым делом немного модифицируем тестовую программу IntListIterationTiming.java. Надо сделать так, чтобы при её запуске она работала продолжительное время и выполняла в основном только интересующий нас код. Поэтому мы научим её принимать в качестве аргументов количество итераций и список реализций классов, которые мы будем сравнивать, а так же выделять память и заполнять массивы только один раз при запуске. Убедимся что это не влияет на получаемые результаты - около 0.75 нс на итерацию. Наш основной цикл будет выглядет вот так:
int sum = 0; for (int i = 0; i < size; i++) sum += list.getInt(i);
Теперь запустим её для продолжительного выполнения (не забыв передать в JVM ключик "-server") и подключимся отладчиком от Miscrosoft Visual Studio (я использую бесплатный Express Edition от 2008-ой версии): Tools -> Attach to Process (Ctrl+Alt+P); потом ставим приложение на паузу через Tools -> Break All (Ctrl+Alt+Break). Скорей всего мы попадем именно в нужное место кода. Visual Studio сразу покажет ассемблерный код в Intel синтаксисе ассемблера (приемник результата записывается в первом аргументе):
01C29870 add ebx,dword ptr [edx+edi*4+0Ch] 01C29874 add ebx,dword ptr [edx+edi*4+10h] 01C29878 add ebx,dword ptr [edx+edi*4+14h] 01C2987C add ebx,dword ptr [edx+edi*4+18h] 01C29880 add ebx,dword ptr [edx+edi*4+1Ch] 01C29884 add ebx,dword ptr [edx+edi*4+20h] 01C29888 add ebx,dword ptr [edx+edi*4+24h] 01C2988C add ebx,dword ptr [edx+edi*4+28h] 01C29890 add ebx,dword ptr [edx+edi*4+2Ch] 01C29894 add ebx,dword ptr [edx+edi*4+30h] 01C29898 add ebx,dword ptr [edx+edi*4+34h] 01C2989C add ebx,dword ptr [edx+edi*4+38h] 01C298A0 add ebx,dword ptr [edx+edi*4+3Ch] 01C298A4 add ebx,dword ptr [edx+edi*4+40h] 01C298A8 add ebx,dword ptr [edx+edi*4+44h] 01C298AC add ebx,dword ptr [edx+edi*4+48h] 01C298B0 add edi,10h 01C298B3 cmp edi,esi 01C298B5 jl 01C29870
Видно, что ассемблерный код в сущности такой же - по одной операции на итерацию, что и объясняет в целом одинаковую скорость работы. Различия в виде выбранных регистров и разной глубины разворачивания цикла (HotSpot развернул 16 итераций) существенной роли не играют.
Использая классические скалярные иснтрукции x86 этот код нельзя сделать более быстрым. В этом смысле компиляторы C++ и Java сделали оптимальный код. Однако, используя SIMD инструкции из SSE2, доступные начиная с Pentium 4, можно складывать до 4-х 32-битных целых чисел одной инстукцией. Даст ли это заметный прирост в скорости работы? Мы посмотрим на это подробней в ближайшее время.
UPDATE: А справится ли HotSpot Server, если код будет более сложный, можно почитать в продолжении.

programming, cpp, performance, java, assembler

Previous post Next post
Up