Страница 5 из 7 Разбор ассемблерного кода неких базовых операций Для анализа использовался достаточно простой код на С++: 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 тактов. |