elizarov


Блог Романа Елизарова


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

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

В предыдущей записи я показал замеры времени работы простой итерации со сложением элементов по массиву целых чисел на 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, если код будет более сложный, можно почитать в продолжении.


  • 1
"Использая классические скалярные иснтрукции x86 этот код нельзя сделать более быстрым."

Рома, попробуй "руками" раскрутить гнусёвый ассемблер до 64 байт на итерацию, и воткни в начале цикла "prefetcht0 256(%ecx,%ebx,4)"
Можно даже поиграться сдвигом, только оставляй его кратным 64 байт.

Векторизация в SSE2 скорее всего не шибко и поможет в этом тесте, т.к. он уж очень от памяти зависит. Только iCache'у поможешь меньшей длиной цикла, но и это не сильно скажется.

Тут фикус в том, что автопрефетч - это конечно хорошо, но он таки префетчит в L2, т.е. на каждые 64 байта ты получишь лейтенси между L2 и L1.

Тем не менее, Intel optimization manual сообщает: "In cases where memory accesses are in long, regular data patterns; the automatic hardware prefetcher should be favored over software prefetches."

Угу. Они нас долго в этом убеждали, пока не увидели результаты. Теперь их компилятор тоже пытается префетчи вставлять ;)

  • 1
?

Log in

No account? Create an account