C - статьи

       

Разбор ассемблерного кода неких базовых операций


Для анализа использовался достаточно простой код на С++:

void dummyFn1(unsigned);
void dummyFn2(unsigned aa) {
for (unsigned i=0;i<16;i++) dummyFn1(aa);
}

А теперь посмотрим, во что этот кусок кода компилирует MSVC++ (приводится только текст необходимой функции):

?dummyFn2@@YAXI@Z PROC NEAR
push esi
push edi
mov edi, DWORD PTR _aa$[esp+4]
mov esi, 16
$L271:
push edi
call?dummyFn1@@YAXI@Z

add esp, 4
dec esi
jne SHORT $L271
pop edi
pop esi
ret 0
?dummyFn@@YAXI@Z ENDP

Как видно, MSVC++ инвертировал цикл и for (unsigned i=0;i<16;i++) у него превратился в unsigned i=16;while (i--);, что очень правильно с точки зрения оптимизации - мы экономим на одной операции сравнения (см. следующий листинг), которая занимает, как минимум, 5 байт, и нарушает выравнивание. Конечно, компилятор по своему усмотрению поменял порядок изменения переменной i, но в данном примере мы ее используем просто как счетчик цикла, поэтому такая замена вполне допустима.

А вот что выдал Intel Compiler (вообще-то, он сначала вообще полностью развернул цикл, но после увеличения количества итераций на порядок прекратил заниматься такой самодеятельностью):

?dummyFn2@@YAXI@Z PROC NEAR
$B1$1:
push ebp
push ebx
mov ebp, DWORD PTR [esp+12]
sub esp, 20
xor ebx, ebx
$B1$2:
mov DWORD PTR [esp], ebp
call?dummyFn1@@YAXI@Z
$B1$3:
inc ebx
cmp ebx, 16
jb $B1$2
$B1$4:
add esp, 20
pop ebx
pop ebp
ret
?dummyFn2@@YAXI@Z ENDP

Во-первых, используется прямой порядок цикла for, поэтому появилась дополнительная команда сравнения "cmp ebx, 16". А вот и очень интересный момент -перед началом цикла мы выделили на стеке необходимое количество памяти плюс некий запас ("sub esp, 20"), а потом вместо пары push reg;..;add esp, 4;, как это делает MSVC++, использовали одну команду копирования. Кроме того, использование регистра общего назначения ebx для счетчика цикла вместо индексного esi, как в MSVC++, дополнительно уменьшает время выполнения и размер кода.

Borland Builder сгенерировал следующую конструкцию:

@@dummyFn2$qui proc near
?live16385@0:
@1:
push ebp
mov ebp,esp
push ebx
push esi
mov esi,dword ptr [ebp+8]
?live16385@16:
@2:
xor ebx,ebx
@3:
push esi
call @@dummyFn1$qui
pop ecx
@5:
inc ebx
cmp ebx,16
jb short @3
?live16385@32:
@7:
pop esi
pop ebx
pop ebp
ret
@@dummyFn2$qui endp

Если не считать большего количества подготовительных операций, то блок вызова собственно функции является чем-то средним между MSVC++ и Intel Compiler: цикл используется прямой и передача параметров осуществляется с помощью push reg;. Правда, есть интересный момент: вместо add esp, 4 используется pop ecx; что экономит, как минимум, 4 байта,- правда, из-за дополнительного обращения к памяти команда "pop" может работать медленнее, чем сложение.

Ну и, наконец, gcc (обратите внимание, gcc для ассемблера использует синтаксис AT&T):

__Z7dummy2Fnj:
LFB1:
pushl %ebp
LCFI0:
movl %esp, %ebp
LCFI1:
pushl %esi
LCFI2:
pushl %ebx
LCFI3:
xorl %ebx, %ebx
movl 8(%ebp), %esi
.p2align 4,,7
L6:
subl $12, %esp
incl %ebx
pushl %esi
LCFI4:
call __Z2dummyFn1j
addl $16, %esp
cmpl $15, %ebx
jbe L6
leal -8(%ebp), %esp
popl %ebx
popl %esi
popl %ebp
ret

Данный код является самым плохим из всех приведенных выше - gcc использует прямой цикл плюс пару push esi;..;add esp, 4 (это происходит неявно в команде "addl $16, %esp") для передачи параметров; кроме того, резервирует место на стеке прямо в цикле, а не вне его, как это делает Intel Compiler. Кроме того, совершенно непонятно, зачем резервировать место на стеке, а потом использовать команду push reg;. Единственный приятный момент - это явное выравнивание начала цикла по границе, чего не делают остальные компиляторы - поскольку линейка кэша сегмента кода достигает 32-х байт, то метки начала циклов должны быть выровнены по границе 16 байт. На каждый байт, выходящий за пределы кэша, процессор семейства P2 тратит 9-12 тактов.



Содержание раздела