mov esp,0
устанавливает указатель стека на 0. Любые инструкции стека, такие как push / pop или call / ret, вылетят после того, как вы это сделаете.
Выберите другой регистр для временного подсчета массива, а не указатель стека! У вас есть еще 7 вариантов выбора, похоже, что EDX по-прежнему не используется.
В обычном соглашении о вызовах только EAX, ECX и EDX блокируются вызовом (так что вы можетеиспользовать их без сохранения значения вызывающего абонента).Но вы звоните ExitProcess
вместо возврата из main
, чтобы вы могли уничтожить все регистры.Но ESP
должен быть действителен, когда вы call
.
call
работают, помещая адрес возврата в стек, как sub esp,4
/ mov [esp], next_instruction
/ jmp ExitProcess
.См. https://www.felixcloutier.com/x86/CALL.html. Как показывает ваш дамп регистра, ESP = 8 перед call
, поэтому он пытается сохранить по абсолютному адресу 4
.
Ваш код имеет 2разделы: цикл по массиву и затем нахождение среднего. Вы можете повторно использовать регистр для разных вещей в 2 разделах , что часто значительно снижает нагрузку на регистр.(т. е. у вас не заканчиваются регистры.)
Использование массивов неявной длины (оканчивающихся элементом стража, таким как 0
), необычно вне строк.Гораздо более распространенной является передача функции указатель + длина вместо простого указателя.
Но в любом случае, у вас есть массив неявной длины, поэтому вы должны найти его длину и запомнить это при вычислении среднего значения.Вместо того, чтобы увеличивать счетчик размера внутри цикла, вы можете вычислить его по указателю, который вы также увеличиваете.(Или используйте счетчик в качестве индекса массива, например ary[ecx*4]
, но приращение указателя часто более эффективно.)
Вот как может выглядеть эффективная (скалярная) реализация .(С SSE2 для SIMD вы можете добавить 4 элемента с одной инструкцией ...)
Используется всего 3 регистра .Я мог бы использовать ECX вместо ESI (чтобы main
мог ret
, не уничтожив ни одного из регистров, которые, как ожидал вызывающий объект, он сохранит, только EAX, ECX и EDX), но я сохранил ESI для совместимости с вашей версией.
.data
;ary dword 100, -30, 25, 14, 35, -92, 82, 134, 193, 99, 0
ary dword -24, 1, -5, 30, 35, 81, 94, 143, 0
.code
main PROC
;; inputs: static ary of signed dword integers
;; outputs: EAX = array average, EDX = remainder of sum/size
;; ESI = array count (in elements)
;; clobbers: none (other than the outputs)
; EAX = sum accumulator
; ESI = array pointer
; EDX = array element temporary
xor eax, eax ; sum = 0
mov esi, OFFSET ary ; incrementing a pointer is usually efficient, vs. ary[ecx*4] inside a loop or something. So this is good.
sumloop: ; do {
mov edx, [esi]
add edx, 4
add eax, edx ; sum += *p++ without checking for 0, because + 0 is a no-op
test edx, edx ; sets FLAGS the same as cmp edx,0
jnz sumloop ; }while(array element != 0);
;;; fall through if the element is 0.
;;; esi points to one past the terminator, i.e. two past the last real element we want to count for the average
sub esi, OFFSET ary + 4 ; (end+4) - (start+4) = array size in bytes
shr esi, 2 ; esi = array length = (end-start)/element_size
cdq ; sign-extend sum into EDX:EAX as an input for idiv
idiv esi ; EAX = sum/length EDX = sum%length
call ExitProcess
main ENDP
Я использовал инструкцию аппаратного деления x86 вместо цикла вычитания.Ваш цикл повторного вычитания выглядел довольно сложным, но ручное деление со знаком может быть сложным. Я не понимаю, где вы обрабатываете вероятность того, что сумма будет отрицательной. Если ваш массив имел отрицательную сумму, повторное вычитание заставило бы его расти до тех пор, пока оно не переполнилось.Или в вашем случае вы выходите из цикла, если sum < count
, что будет истинно на первой итерации для отрицательной суммы.
Обратите внимание, что комментарии типа Set EAX register to 0
бесполезны.Мы уже знаем это из чтения mov eax,0
.sum = 0
описывает смысл семантики , а не архитектурный эффект.Есть некоторые хитрые инструкции для x86, в которых есть смысл прокомментировать то, что он даже делает в этом конкретном случае, но mov
не является одним из них.
Если вы просто хотели сделать повторное вычитание с помощьюПредположим, что sum
неотрицателен для начала, это так просто:
;; UNSIGNED division (or signed with non-negative dividend and positive divisor)
; Inputs: sum(dividend) in EAX, count(divisor) in ECX
; Outputs: quotient in EDX, remainder in EAX (reverse of the DIV instruction)
xor edx, edx ; quotient counter = 0
cmp eax, ecx
jb subloop_end ; the quotient = 0 case
repeat_subtraction: ; do {
inc edx ; quotient++
sub eax, ecx ; dividend -= divisor
cmp eax, ecx
jae repeat_subtraction ; while( dividend >= divisor );
; fall through when eax < ecx (unsigned), leaving EAX = remainder
subloop_end:
Обратите внимание, как проверка особых случаев перед входом в цикл позволяет упростить его.См. Также Почему циклы всегда компилируются в стиле "do ... while" (прыжок с хвоста)?
sub eax, ecx
и cmp eax, ecx
в одном цикле кажется избыточным: мы могли бы простоиспользуйте sub для установки флагов и исправьте перерегулирование.
xor edx, edx ; quotient counter = 0
cmp eax, ecx
jb division_done ; the quotient = 0 case
repeat_subtraction: ; do {
inc edx ; quotient++
sub eax, ecx ; dividend -= divisor
jnc repeat_subtraction ; while( dividend -= divisor doesn't wrap (carry) );
add eax, ecx ; correct for the overshoot
dec edx
division_done:
(Но на самом деле это не так быстро в большинстве случаев на большинстве современных процессоров x86; они могут запускать inc, cmp и sub параллельнодаже если входные данные не совпадают. Это может помочь в семействе AMD Bulldozer, где целочисленные ядра довольно узкие.)
Очевидно, что повторное вычитание является полным мусором для производительности с большими числами. Возможно реализовать более совершенные алгоритмы, такие как одноразрядное длинное деление, но инструкция idiv
будет быстрее для всего, кроме случая, когда вы знаете, что частное равно 0 или 1, поэтому требуется не более 1 вычитания.(div
/ idiv
довольно медленный по сравнению с любой другой целочисленной операцией, но выделенное аппаратное обеспечение намного быстрее, чем зацикливание.)
Если вам нужно реализовать деление со знаком вручную, обычно вызапишите знаки, примите абсолютное значение без знака, затем выполните деление без знака.
Например, xor eax, ecx
/ sets dl
дает вам dl = 0, если EAX и ECX имели одинаковый знак, или 1, если онибыли разные (и, следовательно, частное будет отрицательным).(SF устанавливается в соответствии со знаковым битом результата, и XOR выдает 1 для разных входов, 0 для одинаковых входов.)