Низкоуровневая оптимизация

Основные принципы

Так как современные процессоры используют весьма сложный набор команд, большинство операций можно выполнить на низком уровне очень многими способами. При этом иногда оказывается, что наиболее очевидный способ – не самый быстрый. Часто простыми перестановками команд, зная механизм выполнения команд на современных процессорах, можно заставить ту же процедуру выполняться на 50–200% быстрее. Разумеется, переходить к этому уровню оптимизации можно только после того, как текст программы окончательно написан и максимально оптимизирован на среднем уровне.

Перечислим основные рекомендации.

  • Используйте регистр ЕАХ всюду, где возможно. Команды с непосредственным операндом, с операндом – абсолютным адресом переменной и команды XCHG с регистрами занимают на один байт меньше, если другой операнд – регистр ЕАХ.
  • Если к переменной в памяти, адресуемой со смещением, выполняется несколько обращений – загрузите её в регистр.
  • Не используйте сложные команды – ENTER, LEAVE, LOOP, строковые команды, если аналогичное действие можно выполнить небольшой последовательностью простых команд.
  • Не используйте умножение или деление на константу – его можно заменить другими командами (см. раздел 6.3).
  • Старайтесь программировать условия и переходы так, чтобы переход выполнялся по менее вероятному событию.
  • Следующее эмпирическое правило, относящееся к переходам и вызовам, очень простое: избавляться от них везде, где только можно. Для этого организуйте программу так, чтобы она исполнялась прямым, последовательным образом, с минимальным числом точек принятия решения. В результате очередь команд будет почти всегда заполнена, а вашу программу будет легче читать, сопровождать и отлаживать. Процедуры, особенно небольшие, нужно не вызывать, а встраивать. Это, конечно, увеличивает размер программы, но даёт существенный выигрыш во времени её исполнения.
  • Используйте короткую форму команды JMP, где возможно (jmp short <метка>).
  • Команда LEA быстро выполняется и имеет много неожиданных применений (см. раздел 8.3.2).
  • Многие одиночные команды, как это ни странно, выполняются дольше, чем две или три команды, приводящие к тому же результату. Это может быть связано с различными особенностями выполнения команд, в том числе, с возможностью/невозможность попарного выполнения команд в разных конвейерах (см. раздел 8.3.3).
  • Старайтесь выравнивать данные и метки по адресам, кратным 2/4/8/16 (см. раздел 8.3.4).
  • Если команда обращается к 32-битному регистру, например ЕАХ, сразу после команды, выполнявшей запись в соответствующий частичный регистр (АХ, AL, АН), может происходить пауза в один или несколько тактов.

8.3.2. Использование команды LEA

  • Команда LEA может использоваться для трёхоперандного сложения (но только сложения, а не вычитания).

lea eax, [ebx + edx]

  • Команда LEA может использоваться для сложения значения регистра с константой или вычитания константы из значения регистра. В данном случае вычитание возможно, т.к. оно рассматривается как сложение с отрицательной константой. Результат может быть помещён в тот же или другой регистр (кроме регистра ESP). Такой способ используется для сохранения флагов, т.к. команда LEA, в отличие от команд ADD, SUB, INC и DEC, не меняет флаги.

lea eax, [eax + 1]; Сохраняем флаги

lea eax, [ebx – 4]

  • Команда LEA может использоваться для быстрого умножения на константы 2, 3, 4, 5, 7(?), 8, 9. Адрес, загружаемый командой LEA, может быть суммой двух регистров, один из которых может быть умножен на константу 2, 4 или 8. Поэтому комбинируя умножение и сложение можно получить вышеперечисленные константы. Третье слагаемое может быть константой.

lea eax, [eax * 4 + eax]; EAX = EAX * 5

lea eax, [ebx * 8 + ecx – 32]

Замена команд

  • Вместо команды AND лучше использовать команду TEST, если нужен не результат, а проверка. Команда TEST лучше спаривается. Команда TEST также может быть использована для проверки на равенство нулю.

test eax, eax

jz <метка>; Переход, если EAX = 0

  • Если за командой CALL сразу же следует команда RET, замените эти команды командой JMP. Вызываемая процедура осуществит возврат по адресу возврата, переданному вызывающей процедуре.

call dest jmp dest

ret

  • Команду CBW можно заменить засылкой нуля, если расширяемое число положительное. Команду CDQ можно заменить засылкой нуля, если расширяемое число положительное, или парой команд MOV + SAR, если знак расширяемого числа не известен. Недостаток – команды XOR и SAR меняют флаги.

cdq xor edx, edx

cdq mov edx, eax

sar eax, 31

  • Вместо команд инкремента и декремента можно использовать команду LEA.
  • Сложение и вычитание с константой можно заменить командой LEA.
  • Вместо умножения и деления на степень числа 2 используйте сдвиги.
  • Умножение и деление на константу можно заменить командой LEA или сочетанием команд сдвига и команд сложения и вычитания.
  • Деление на константу можно заменить умножением на константу.
  • Обнуление регистров производится с помощью команды XOR.

xor eax, eax; EAX = 0 при любом значении EAX, которое было до этой команды

  • Не используйте команду MOVZX для чтения байта – это требует 3 тактов для выполнения. Заменой может служить такая пара команд, выполняющаяся за 2 такта:

xor еах, еах

mov al, <источник>

  • Засылку непосредственного операнда в ячейку памяти можно производить через регистр – такие команды лучше спариваются.

mov x, 1 mov eax, 1

mov x, eax

mov [ebx], 1 mov eax, 1

mov [ebx], eax

  • Аналогично команды PUSH и POP, работающие с ячейкой памяти, можно заменить парой команд MOV + PUSH или POP + MOV.

push x mov eax, x

push eax

pop x pop eax

mov x, eax

Выравнивание

  • 80-битные данные должны быть выравнены по 16-байтным границам (то есть четыре младших бита адреса должны быть равны нулю).
  • Восьмибайтные данные должны быть выравнены по восьмибайтным границам (то есть три младших бита адреса должны быть равны нулю).
  • Четырёхбайтные данные должны быть выравнены по границе двойного слова (то есть два младших бита адреса должны быть равны нулю).
  • Двухбайтные данные должны быть выравнены по границе слова.
  • Метки для переходов, особенно метки, отмечающие начало цикла, должны быть выравнены по 16-байтным границам.

Каждое невыравненное обращение к данным означает потерю тактов процессора.

Для выравнивания данных и кода используется директива ALIGN:

ALIGN <число>

Число должно быть степенью двойки. Данные и команда, расположенные после директивы ALIGN, будут размещены по адресу, кратному указанному числу.

Примеры

1. Процедура вычисления наибольшего общего делителя двух беззнаковых чисел. Для нахождения НОД используется алгоритм Евклида: пока числа не равны, надо вычитать из большего числа меньшее. Процедура получает параметры через регистры EAX и EDX и возвращает результат через регистр EAX.

NOD proc

N1: cmp eax, edx; Сравниваем числа

je N3; Если числа равны, завершаем работу процедуры

ja N2; Если первое число больше, обходим обмен

; Поскольку команды перехода не меняют флаги, оба перехода

; выполняются или не выполняются по результатам одного сравнения

xchg eax, edx; Если первое число было меньше, выполняем обмен

N2: sub eax, edx; Вычитаем из большего числа меньшее

jmp N1; Переход к началу цикла

N3: ret

NOD endp

2. Ввод и вывод в консольном приложении. В программе используются следующие функции Win32 API.

  • SetConsoleTitle – меняет заголовок окна консоли. Получает один параметр – указатель на строку, которая будет выведена в заголовке. Строка должна заканчиваться нулём.
  • GetStrHandle – возвращает идентификатор устройства ввода, устройства вывода или устройства отчёта об ошибках. Для консольного приложения всё три устройства являются консолью, но идентификаторы будут разными. Функция получает один параметр – указание, идентификатор какого устройства нужно вернуть. Чтобы получить идентификатор устройства ввода, надо передать функции число -10, чтобы получить идентификатор устройства вывода – число -11, а чтобы получить идентификатор устройства отчёта об ошибках – число -12. Функция возвращает требуемый идентификатор через регистр EAX.
  • WriteConsole – выводит строку в консоль. Получает следующие параметры – идентификатор устройства вывода, адрес выводимой строки, количество символов для вывода, адрес переменной, куда будет записано количество выведенных символов, зарезервированный указатель.
  • ReadConsole – вводит строку из консоли. Получает следующие параметры – идентификатор устройства ввода, адрес памяти, куда будет записана введённая строка, максимальное количество читаемых символов, адрес переменной, куда будет записано реальное количество введённых символов, зарезервированный указатель.

Не забывайте, что параметры кладутся в стек, начиная с последнего, и что введённая строка всегда будет содержать в конце символы с кодами 13 и 10, которые появляются при нажатии на клавишу ВВОД (без чего, однако, ввод не завершится).

.686

.model flat, c

option casemap: none

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc

includelib \masm32\lib\kernel32.lib

.data

str db 256 dup(0)

hStdIn dd 0

hStdOut dd 0

slength dd 0

.const

sConsoleTitle db 'Input and Output',0; Заголовок окна консоли. Заканчивается нулём

prompt db 'Input a string', 13,10; Приглашение для ввода. Символы с кодами 13 и 10

; обеспечивают перевод курсора на следующую строку

STD_INPUT_HANDLE equ -10d; Определяем символические имена для констант,

STD_OUTPUT_HANDLE equ -11d; указывающих требуемое устройство

.code

program:

; Вывод заголовка консоли

push offset sConsoleTitle; Кладём в стек адрес начала строки заголовка консоли

call SetConsoleTitle; Вызываем функцию

; Получаем идентификатор устройства ввода

push STD_INPUT_HANDLE; Кладём в стек параметр функции GetStdHandle

call GetStdHandle; Вызываем функцию

mov hStdIn, eax; Сохраняем полученный идентификатор

; Получаем идентификатор устройства вывода

push STD_OUTPUT_HANDLE

call GetStdHandle

mov hStdOut, eax

; Выводим приглашение

push 0; Зарезервированный параметр, в стек кладём 0

push 0; Указатель на переменную для записи количества выведенных символов,

; в данном случае не нужен, поэтому в стек кладём 0

push 10h; Количество выводимых символов

push offset prompt; Адрес выводимой строки

push hStdOut; Идентификатор устройства вывода

call WriteConsole; Вызываем функцию

; Вводим строку

push 0; Зарезервированный параметр, в стек кладём 0

push offset slength; Адрес переменной, куда будет записано количество введённых символов

push 256; Максимальное количество вводимых символов

push offset str; Адрес для записи введённой строки

push hStdIn; Идентификатор устройства ввода

call ReadConsole; Вызываем функцию

; Выводим строку

push 0

push 0

push slength

push offset str

push hStdOut

call WriteConsole

; Задержка

push 1800h

call Sleep

push 0

call ExitProcess

end program

3. Процедура ввода целого числа в 16-ричной системе счисления. Процедура предназначена для использования в консольном приложении и предполагает, что идентификатор устройства ввода был получен основной программой и сохранён в переменной hStdIn.

InputNumber proc

push ebp; Сохраняем в стеке значение регистра EBP

mov ebp, esp; Заносим в регистр EBP текущее значение вершины стека

sub esp, 16; Резервируем 16 байт. Вводимая строка может содержать до 8 цифр.

; 2 байта требуются для символов с кодами 13 и 10. Итого 10 байт.

; 4 байта нужно для целочисленной переменной, куда будет записываться количество

; введённых символов. Итого 14 байт. Но выделим 16 байт, т.е. 4 двойных слова

push ebx; Сохраняем значения важных регистров

push esi

; Вводим строку

push 0

lea eax, [ebp - 16]; 4 байта по адресу [EBP – 16] предназначены для хранения количества введённых символов

push eax

push 10d

lea eax, [ebp - 12]; По адресу [EBP – 12] начинается память для вводимой строки

push eax

push hStdIn

call ReadConsole

; Преобразуем строку в число

xor eax, eax; Обнуляем регистр EAX ...

xor ebx, ebx; ... и регистр EBX

mov ecx, [ebp - 16]; Заносим в регистр ECX количество введённых символов

sub ecx, 2; Символы с кодами 13 и 10 обрабатывать не надо

lea esi, [ebp - 12]; Заносим в регистр ESI адрес начала строки

test ecx, ecx; Используем команду TEST для сравнения с нулём

jz L2; Если ECX = 0, то завершаем работу процедуры

L1: mov bl, [esi]; Заносим в регистр BL текущий символ (три старших байта EBX

; содержат 0, т.к. ранее была команда XOR EBX, EBX)

lea edx, [ebx - '0']; Заносим в регистр EDX разность между кодом текущего символа и кодом символа '0'

cmp edx, 9; Сравниваем значение в регистре EDX с 9

ja M1; Если выше, то переходим к следующему сравнению

sub bl, '0'; Иначе получаем число из кода символа

jmp M3; Переходим к действиям, учитывающим текущую цифру

M1: lea edx, [ebx - 'a']; Заносим в регистр EDX разность между кодом текущего символа и кодом символа 'a'

cmp edx, 'f' - 'a'; Сравниваем значение в регистре EDX с 5

ja M2; Если выше, то переходим к следующему сравнению

sub bl, 'a' - 10d; Иначе получаем число из кода символа

jmp M3; Переходим к действиям, учитывающим текущую цифру

M2: lea edx, [ebx - 'A']; Заносим в регистр EDX разность между кодом текущего символа и кодом символа 'A'

cmp edx, 'F' - 'A'; Сравниваем значение в регистре EDX с 5

ja L2; Если выше, то завершаем процедуру. Результат не определён,

; т.к. был введён некорректный символ

sub bl, 'A' - 10d; Иначе получаем число из кода символа

M3: sal eax, 4; Умножаем EAX на 16

add eax, ebx; Прибавляем текущую цифру

inc esi; Переходим к следующему символу

dec ecx; Уменьшаем ECX на 1

jnz L1; Если ECX не равно 0, продолжаем цикл

L2: pop esi; Восстанавливаем значения использовавшихся регистров

pop ebx

mov esp, ebp; Освобождаем стек

pop ebp; Восстанавливаем значение регистра EBP

ret

InputNumber endp

4. Процедура вывода числа в 16-ричной системе счисления. Процедура получает один параметр – выводимое число. Для вывода всегда формируется строка из 8-ми шестнадцатеричных цифр с лидирующими нулями. Поскольку количество символов заранее известно, они будут сразу же записываться в строку с конца, и инвертировать строку не придётся. Процедура предназначена для использования в консольном приложении и предполагает, что идентификатор устройства ввода был получен основной программой и сохранён в переменной hStdOut.

digits db '0123456789abcdef'; Массив шестнадцатеричных цифр

OutputNumber proc

push ebp; Сохраняем в стеке значение регистра EBP

mov ebp, esp; Заносим в регистр EBP текущее значение вершины стека

sub esp, 12; Выделяем в стеке место под формируемую строку

push esi

; Преобразуем число в строку

mov eax, [ebp + 8]; Заносим в регистр EAX переданный параметр

mov ecx, 8; Заносим в регистр ECX количество символов строки

mov byte ptr [ebp - 1], 10; Добавляем в конец строки символы с кодами 13 и 10 для перевода курсора

mov byte ptr [ebp - 2], 13

lea esi, [ebp - 3]; Начиная с адреса [EBP - 3] будут заносится цифры

L3: mov edx, eax; Копируем значение регистра EAX в регистр EDX

and edx, 1111b; Получаем остаток от деления на 16

shr eax, 4; Делим исходное число на 16

mov dl, digits[edx]; По полученному остатку от деления берём цифру ...

mov [esi], dl; ... и записываем её в строку

dec esi; Уменьшаем адрес, т.к. строка формируется с конца

dec ecx; Уменьшаем ECX на 1

jnz L3; Если ECX не равно 0, продолжаем цикл

; Выводим строку

inc esi; Регистр ESI указывает на начало строки

push 0

push 0

push 10

push esi

push hStdOut

call WriteConsole

pop esi

mov esp, ebp; Освобождаем стек

pop ebp; Восстанавливаем значение регистра EBP

ret 4; Удаляем из стека переданный параметр и возвращаемся

OutputNumber endp

5. Функция, находящая в одномерном массиве x сумму значений f(x[i]), где f – некоторая функция одного целочисленного аргумента, адрес которой передаётся через параметры. Функции используют соглашение о вызовах cdecl.

Sum proc

push ebp

mov ebp, esp

push esi

push edi

mov ecx, [ebp + 8]; Заносим в ECX первый параметр – количество элементов массива

mov esi, [ebp + 12]; Заносим в ESI второй параметр – адрес начала массива

mov edi, [ebp + 16]; Заносим в EDI третий параметр – адрес функции

xor edx, edx; Обнуляем регистр EDX

L: push [esi]; Кладём в стек элемент массива

call edi; Вызываем функцию, адрес которой находится в регистре EDI

add esp, 4; Освобождаем стек

add edx, eax; Прибавляем результат функции к общей сумме

add esi, 4; Переходим к следующему элементу массива

dec ecx; Уменьшаем значение регистра ECX на 1

jnz L; Если ECX не равно 0, продолжаем цикл

mov eax, edx; Записываем полученную сумму в регистр EAX,

; через который должен возвращаться результат функции

pop edi

pop esi

mov esp, ebp

pop ebp

ret

Sum endp

Sqr proc

mov eax, [esp + 4]

imul eax

ret

Sqr endp

Negation proc

mov eax, [esp + 4]

neg eax

ret

Negation endp

Для вызова функции Sum будет использовать следующая последовательность команд.

push Sqr

push offset a

push na

call Sum

add esp, 12

mov sa, eax

push Negation

push offset a

push na

call Sum

add esp, 12

mov sa, eax

6. Процедура, проверяющая сбалансированность круглых и квадратных скобок в строке. Строка должна заканчиваться нулём. Для проверки сбалансированности открывающие скобки будем класть в стек, а при нахождении в строке закрывающей скобки будем извлекать из стека последнюю положенную туда открывающую скобку и проверять, что она соответствует закрывающей скобке. Будем считать, что скобок в тексте меньше, чем других символов, поэтому после сравнения делаем переход «если равно», считая, что это событие менее вероятно. При любом выходе из процедуры нужно очистить стек. Поскольку мы не можем заранее знать, сколько скобок будет туда положено и сколько извлечено, восстановление значения регистра ESP можно сделать только с помощью регистра EBP. Процедура возвращает значение через регистр EAX: если скобки сбалансированы, регистр EAX будет содержать значение истина (-1), в противном случае регистр EAX будет содержать значение ложь (0).

Brackets proc

push ebx; Сохраняем регистры

push ebp

mov ebp, esp; Сохраняем начальное значение регистра ESP

mov ebx, [ebp + 12]; Заносим в регистр EBX адрес начала строки

mov eax, -1; Заносим в регистр EAX предварительное значение результата

xor edx, edx; Обнуляем регистр EDX

L1: mov dl, [ebx]; Заносим в регистр DL очередной символ

test edx, edx; Проверяем значение в регистре EDX

jz E1; Если EDX = 0, выходим из цикла

inc ebx; Меняем адрес символа

cmp dl, '('; Сравниваем символ с открывающей круглой скобкой

je L2; Если равно, ...

cmp dl, '['; Сравниваем символ с открывающей квадратной скобкой

je L2; Если равно, ...

cmp dl, ')'; Сравниваем символ с закрывающей круглой скобкой

je L3; Если равно, переходим к сравнению со скобкой из стека

cmp dl, ']'; Сравниваем символ с закрывающей квадратной скобкой

je L4; Если равно, переходим к сравнению с другой скобкой из стека

jmp L1; Если символ – не скобка, возвращаемся к началу цикла

L2: push dx; ... заносим открывающую скобку в стек (один байт записать в стек нельзя)

jmp L1; Возвращаемся к началу цикла

L3: cmp ebp, esp; Если была закрывающая скобка, прежде всего проверяем, есть ли скобки в стеке –

; если мы положили что-то в стек, значение регистра ESP будет отличаться от регистра EBP

je E2; Если значения регистров равны, выходим из процедуры

pop cx; Извлекаем из стека последнюю открывающую скобку

cmp cl, '('; Сравниваем

jne E2; Если скобки не равны, выходим из процедуры

jmp L1; Иначе возвращаемся к началу цикла

L4: cmp ebp, esp; При нахождении закрывающей квадратной скобки,

je E2; выполняем те же действия, что и при нахождении закрывающей круглой скобки,

pop cx; только скобку из стека сравниваем с другим значением

cmp cl, '['; Дублирование сделано для того, чтобы уменьшить

jne E2; количество переходов

jmp L1

E1: cmp ebp, esp; При достижении конца строки, сравниваем регистры ESP и EBP

je E3; Если значения равны, обходим обнуление регистра EAX

E2: xor eax, eax; Если была несбалансированность, обнуляем регистр EAX

E3: mov esp, ebp; Восстанавливаем значение регистра ESP

pop ebp

pop ebx

ret

Brackets endp

1 В защищённом режиме программе выделяется один сегмент размером 4 Гб для кода и один сегмент размером 4 Гб для данных (физически они обычно совпадают). Виртуальный адрес состоит из 16-битного значения, хранящегося в сегментном регистре, и 32-битного смещения. Однако преобразование виртуального адреса в физический осуществляется не путём сложения, а по более сложной схеме. Сначала процессор преобразует виртуальный адрес в линейный. При этом он обращается к таблицам дескрипторов, которые заранее строятся операционной системой. На втором этапе по линейному адресу определяется физический. В этом преобразовании участвует другой набор системных таблиц – таблицы страничной трансляции, которые также составляются операционной системой. Оба набора таблиц могут динамически меняться, обеспечивая максимальное использование оперативной памяти.

В сегментные регистры записываются не адреса сегментов, а селекторы, которые представляют собой номера ячеек специальной таблицы, содержащей дескрипторы сегментов программ. Каждый дескриптор хранит все характеристики, необходимые для обслуживания сегмента: базовый линейный адрес сегмента, границу сегмента (номер последнего байта), а также атрибуты сегмента, определяющие его свойства. Процессор с помощью селектора определяет индекс дескриптора адресуемого сегмента, извлекает из него базовый линейный 32-битный адрес сегмента и, сложив его с 32-битным смещением, получает линейный адрес адресуемой ячейки памяти. Получив линейный адрес адресуемого байта, процессор с помощью таблиц трансляции преобразует его в 32-битный физический адрес. Этот адрес зависит от объёма оперативной памяти, установленной на компьютере.

В 32-битной модели Windows предоставляет всем запущенным приложениям один и тот же селектор для сегмента кода и один и тот же селектор для сегмента данных. Базы обоих сегментов равны 0, а границы – FFFFFFFF. Другими слова, каждому приложению как бы предоставляется всё линейное пространство. Поскольку базовые линейные адреса сегментов программы равны 0, виртуальные смещения, с которыми работают приложения, совпадают с линейными адресами. Другими словами, плоское виртуальное адресное пространство программы совпадает с плоским линейным адресным пространством. При этом все приложения используют один и тот же диапазон линейных адресов. Для того чтобы при одинаковых линейных адресах приложения занимали различные участки физической памяти и не затирали друг друга, Windows при смене приложения изменяет таблицы страничной трансляции, с помощью которых как раз и происходит преобразование линейных адресов в физические.

2 Если говорить точнее, то относительный адрес перехода отсчитывается не от самой команды перехода, а от следующей за ней команды. Дело в том, что выполнение любой команды начинается с засылки в регистр EIP адреса следующей по порядку команды и только затем выполняется собственно команда. Поэтому в команде перехода относительный адрес будет прибавляться к значению регистра EIP, которое уже указывает на следующую команду, а потому от этой следующей команды и приходится отсчитывать относительный адрес перехода. Однако, в любом случае, программисту нет необходимости самому высчитывать относительный адрес перехода, это делает компилятор языка ассемблера.

3 Компьютерная программа в целом или её отдельная процедура называется реентерабельной (от англ. reentrant – повторно входимый), если она разработана таким образом, что одна и та же копия инструкций программы в памяти может быть совместно использована несколькими пользователями или процессами. При этом второй пользователь может вызвать реентерабельный код до того, как с ним завершит работу первый пользователь и это как минимум не должно привести к ошибке, а в лучшем случае не должно вызвать потери вычислений (то есть не должно появиться необходимости выполнять уже выполненные фрагменты кода).

Для обеспечения реентерабельности необходимо выполнение нескольких условий:

  • никакая часть вызываемого кода не должна модифицироваться;
  • вызываемая процедура не должна сохранять информацию между вызовами;
  • если процедура изменяет какие-либо данные, то они должны быть уникальными для каждого пользователя;
  • процедура не должна возвращать указатели на объекты, общие для разных пользователей.

В общем случае, для обеспечения реентерабельности необходимо, чтобы вызывающий процесс или функция каждый раз передавал вызываемому процессу все необходимые данные. Таким образом, функция, которая зависит только от своих параметров, не использует глобальные и статические переменные и вызывает только реентерабельные функции, будет реентерабельной. Если функция использует глобальные или статические переменные, необходимо обеспечить, чтобы каждый пользователь хранил свою локальную копию этих переменных.

http://natalia.appmat.ru/c&c++/assembler.html


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: