Простая программа на Си
#include<stdio.h>
float
power(float x,int n)
{ int i;
float s;
s=1;
for (i=0;i<n;i++) s=s*x;
return s;
}
int
main()
{ float
y;
y=power(2.125,10);
printf("Result: %f\n",y);
return 0;
}
Ассемблерный листинг,
сгенерированный командой: icc -O3 -march=pentiumiii -S test.c
# -- Machine type PA
# mark_description "Intel(R) C++ Compiler for
32-bit applications, Version 7.1 Build
20030307Z";
# mark_description "-long_double -Xlinker -rpath
-Xlinker /opt/intel/compiler70/ia32/lib -O3 -march=pentiumiii -S";
.ident
"Intel(R) C++ Compiler for 32-bit applications, Version 7.1 Build 20030307Z"
.ident
"-long_double -Xlinker -rpath -Xlinker /opt/intel/compiler70/ia32/lib -O3
-march=pentiumiii -S"
.file
"test.c"
.text
.data
.align 4
.bss
.align 4
.data
.align 16
_2il0floatpacket.3:
.long
0x3f800000,0x3f800000,0x3f800000,0x3f800000 #
ms128
.type _2il0floatpacket.3,@object
.size _2il0floatpacket.3,16
_2il0floatpacket.1:
.long
0x3f800000 # xf32
.type _2il0floatpacket.1,@object
.size _2il0floatpacket.1,4
.data
.text
С символа # видимо
начинаются комментарии.
# -- Begin
power
# mark_begin;
.align 4,0x90 #
Это директива выравнивания. Она указывает, чтобы следующая команда в памяти
была выровнена по 4 байтам.
.globl power
power:
# parameter 1: 8 + %ebx
# parameter 2: 12 + %ebx
..B1.1: # Preds ..B1.0
pushl %ebx #3.1 Начинаются всякие операции с указателями стека и кадра
movl %esp, %ebx #3.1
andl $-16, %esp #3.1
subl $16, %esp #3.1
movl 12(%ebx), %ecx #2.7 Записать с регистр ecx значение по адресу ebx+12 (похоже, это параметр n)
testl %ecx, %ecx #6.3 Проверить значение ecx (установить соответствующие флаги)
flds
8(%ebx) #2.7 Загрузить в вершину FPU-стека число по адресу
ebx+8 (видимо, это переменная x)
flds _2il0floatpacket.1 #5.3 Загрузить в вершину FPU-стека число по указанному адресу (какая-то константа)
flds
_2il0floatpacket.1 #5.3 Загрузить в вершину FPU-стека число по указанному
адресу (та же константа)
jle
..B1.12 # Prob 2% #6.3 Если "меньше или равно" (ecx<=0),
то переходим в конец функции. Т.е. если n<=0, то ничего делать не надо.
Заметим, что здесь установка флагов (test) и проверка (jle)
разделены командами сопроцессора, которые могут выполняться независимо и
параллельно (одновременно).
# LOE ecx
ebp esi edi f1 f2 f3
..B1.2: # Preds ..B1.1
xorl %edx, %edx #6.3 Обнуляем edx. Видимо, это i - счетчик цикла.
cmpl
$8, %ecx #6.3 Сравниваем ecx со значением 8
jl
..B1.6 # Prob 50% #6.3 Переход, если меньше (ecx<8)
# LOE edx ecx
ebp esi edi f1 f2 f3
..B1.3: # Preds ..B1.2
fstp %st(0) # Удаляем значение из вершины FPU-стека
movl %ecx, %eax #6.3 Записать значение ecx в eax
(присваивание)
andl
$7, %eax #6.3 В eax биты 0,1,2 без изменений, более
старшие биты обнуляются (7=00000111b). Получаем в eax остаток от деления его на 8.
negl %eax
#6.3 Меняем знак: eax = -eax
addl %ecx, %eax #6.3 Прибавляем: eax = eax + ecx
# Результат: eax - это теперь округленный до 8 ecx (см. комментарии после
листинга).
movaps _2il0floatpacket.3, %xmm2 #6.21 Записываем в регистр xmm2 значения из памяти (похоже, четыре единицы). Ух ты, используются регистры xmm! Неужели он что-то векторизовал?
fxch %st(1) #6.25 Меняем местами значения st(1) и st(0) (st(0) - вершина FPU-стека)
fsts (%esp) #6.25 Записываем в память по адресу esp значение из вершины FPU-стека
movss (%esp), %xmm0 #6.25 Записать float
значение из памяти по адресу esp в регистр xmm0
shufps $0, %xmm0, %xmm0 #6.25 Как-то перераспределяем значения в регистре xmm0 (наверно, первое float-число из регистра xmm0 размножаем по остальным позициям в регистра).
.align 4,0x90
# Вот такой векторизованный цикл получился:
# LOE eax
edx ecx ebp esi edi f1 f3 xmm0 xmm2
..B1.4: # Preds ..B1.4 ..B1.3
mulps %xmm0, %xmm2 #6.25 Векторное умножение: xmm2
*= xmm0 (4 числа сразу!)
mulps %xmm0, %xmm2 #6.25 Векторное умножение: xmm2 *= xmm0 (4 числа сразу!)
addl
$8, %edx #6.3 edx += 8 ( i+=8,
т.к. сразу 8 умножений за одну итерацию)
cmpl %eax, %edx #6.3 Сравниваем eax и edx
jl ..B1.4 # Prob 97% #6.3 Если меньше (edx<eax), то переход
# LOE eax
edx ecx ebp esi edi f1 f3 xmm0 xmm2
..B1.5: # Preds ..B1.4
movaps %xmm2, %xmm0 #6.3 Векторное присваивание: xmm0 = xmm2
movhlps %xmm0, %xmm0 #6.3 Перемещение двух нижних float-чисел регистра xmm0 в верхние
mulps %xmm0, %xmm2 #6.21 Векторное умножение: xmm2 *= xmm1
movaps %xmm2,
%xmm1 #6.3 Векторное присваивание: xmm1 = xmm2
shufps $1,
%xmm1, %xmm1 #6.3 Как-то перераспределяем значения в
регистре xmm1
mulss %xmm1, %xmm2 #6.21 Скалярное умножение (одного самого младшего float-числа): xmm2* = xmm1
cmpl %ecx, %edx #6.3 Сравниваем ecx и edx (опять заметим:
сравнение делается намного раньше, чем понадобится его результат; это потому,
что эту и последующие команды он может делать независимо и, возможно,
параллельно; такая вот оптимизация).
movss %xmm2, 8(%esp) #6.3 Записываем младшее число float из
регистра xmm2 в память по адресу esp+8
flds
8(%esp) #6.3 А потом его заносим в вершину
FPU-стека
fmulp %st, %st(2) #6.3 Умножаем: st(0)*st(2) (они при этом выталкиваются из стека). Результат помещаем в вершину стека.
fxch %st(1) #6.3 Меняем местами st(1) и вершину FPU-стека st(0)
fsts
8(%esp) #6.3 Записываем значение из вершины
FPU-стека в память по адресу esp+8
flds
8(%esp) #6.3 И помещаем его же в FPU-стек (теперь это новая вершина)
jl
..B1.6 # Prob 50% #6.3 Переход, если меньше (edx<ecx)
# LOE edx
ecx ebp esi edi f1 f2 f3
..B1.13: # Preds ..B1.5
fstp %st(2) # Выталкиваем из FPU-стека значение в st(2)
fstp %st(0) # Выталкиваем из FPU-стека вершину
jmp
..B1.9 # Prob 100% # Переход на ..B1.9
# LOE ebp
esi edi f2
..B1.6: # Preds ..B1.5 ..B1.2
fstp %st(1) # Выталкиваем из FPU-стека значение в st(1)
.align 4,0x90
# В этом цикле доделываются умножения:
# LOE edx
ecx ebp esi edi f1 f2
..B1.7:
# Preds ..B1.6 ..B1.7
fmul %st(1), %st #6.25 Умножение: st(1)*st(0). Результат - в вершину FPU-стека.
incl %edx #6.3 Увеличение edx на 1
fstps
8(%esp) #6.21 Выталкиваем значение из вершины
FPU-стека в память по адресу esp+8
cmpl %ecx, %edx #6.3 Сравнение ecx и edx
flds
8(%esp) #6.21 Заносим в FPU-стек значение по адресу esp+8
jl
..B1.7 # Prob 50% #6.3 Переход, если ecx<edx
# LOE edx
ecx ebp esi edi f1 f2
..B1.8: # Preds ..B1.7
fstp %st(1) # Выталкиваем из FPU-стека значение st(1)
jmp
..B1.9 # Prob 100% # Переход на ..B1.9
# LOE ebp
esi edi f2
..B1.12: # Preds ..B1.1
fstp %st(2) # Выталкиваем из FPU-стека значение st(2)
fstp %st(0) # Выталкиваем из FPU-стека вершину
# LOE ebp
esi edi f2
..B1.9: # Preds ..B1.13 ..B1.8
..B1.12
movl %ebx, %esp #7.10 Восстанавливаем указатель вершины
стека
popl %ebx
#7.10 Восстанавливаем указатель кадра
ret
#7.10
.align 4,0x90
# LOE
# mark_end;
.type power,@function
.size power,.-power
.data
# -- End power
.data
.align 16
__STRING.0:
.byte 82 # s8
.byte 101 # s8
.byte 115 # s8
.byte 117 # s8
.byte 108 # s8
.byte 116 # s8
.byte 58 # s8
.byte 32 # s8
.byte 37 # s8
.byte 102 # s8
.byte 10 # s8
.byte 0 # s8
.type __STRING.0,@object
.size __STRING.0,12
_2il0floatpacket.5:
.long
0x40080000 # xf32
.type _2il0floatpacket.5,@object
.size _2il0floatpacket.5,4
.data
.text
# -- Begin main
# mark_begin;
.align 4,0x90
.globl main
main:
..B2.1:
# Preds ..B2.0
pushl %ebp #11.1
movl %esp, %ebp #11.1
subl $3, %esp #11.1
andl $-8, %esp #11.1
addl $4, %esp #11.1
subl $16, %esp #11.1
pushl %eax #11.1
pushl %eax #11.1
stmxcsr (%esp) #11.1
popl %eax #11.1
orl $32768, %eax #11.1
pushl %eax #11.1
ldmxcsr (%esp) #11.1
popl %eax #11.1
popl %eax #11.1
push $10
#12.5
push $1074266112 #12.5
call power #12.5
# LOE ebx esi
edi f1
..B2.6: # Preds ..B2.1
addl $8, %esp #12.5
# LOE ebx esi
edi f1
..B2.2: # Preds ..B2.6
movl $__STRING.0, (%esp) #13.25
fstpl 4(%esp) #13.25
call printf #13.25
# LOE ebx esi edi
..B2.3: # Preds ..B2.2
xorl %eax, %eax #14.3
addl $16, %esp #14.3
movl %ebp, %esp #14.3
popl %ebp #14.3
ret
#14.3
.align 4,0x90
# LOE
# mark_end;
.type main,@function
.size main,.-main
.data
# -- End main
.data
# End
Комментарии к ассемблерному листингу вычислительной функции:
- переменная n (параметр функции) хранится в регистре ecx,
- переменная i (счетчик цикла) хранится в регистре edx,
- в регистр eax записывается значение eax=n - n%8, оно кратно 8,
- векторизованный цикл выполняет n/8 итераций по 8 умножений за итерацию, результаты в xmm2,
- затем перемножаются числа-компоненты регистра xmm2; получается, выполнено n√n%8 умножений,
- остальные n%8 умножений выполняются на FPU (причем все время лазит в память).
- иногда для временного хранения значений происходит обращение в память; но если одна и та же ячейка памяти сначала пишется, а потом сразу читается, то задержки не происходит (значение еще находится в буфере записи и в память еще не ушло).
Замечания
по синтаксису ассемблера AT&T
- К названиям команд, имеющим операнды, добавляются суффиксы, отражающие размер операндов:
b - байт
w - слово
l - двойное слово
q - учетверенное слово
s - 32-битное число с
плавающей точкой
l - 64-битное число с
плавающей точкой
t - 80-битное число с
плавающей точкой
-
Если команда имеет несколько операндов,
операнд-источник записывается первым, а операнд-приемник - последним.
-
Способы адресации:
Регистровый операнд всегда начинается с символа "%":
xorl %eax, %eax // обнулить регистр eax
Непосредственный операнд всегда начинается с символа "$":
movl $variable, %edx // записать в edx адрес переменной variable (variable - переменная в памяти)
Косвенная адресация использует немодифицированное имя переменной:
pushl variable // записать значение переменной variable в стек (variable - переменная в памяти, стек также занимает область памяти)
Примеры более сложных способов адресации памяти:
addr(%ebx,%edi,4) - адрес: addr + ebx + edi * 4
(%ebx,%eax,4) - адрес: ebx + eax * 4
-2(%ebp) - адрес:
-2 + ebp
(,%edi,2) - адрес: edi * 2
(%ebx) - адрес: ebx
Следует обратить внимание на такой пример, касающийся обращений в память:
Здесь обращение в память происходит:
mov -8(%ebx), %eax // поместить значение из памяти по адресу ebx-8 в регистр eax
push %ebx // поместить в стек значение регистра ebx (стек находится в памяти)
flds 8(%esp) // загрузить значение из памяти по адресу esp+8 в регистр сопроцессора
Здесь обращения в память не происходит:
lea -8(%ebx), %eax // загрузить адрес ebx-8 в регистр eax (к значению по этому адресу обращения нет).
С помощью команды lea процессор может выполнять различные арифметические операции, такие как сложение и умножение. Например:
lea 24(%ebx,%ecx,4),
%eax // eax = 24 + ebx + ecx * 4