Давай сделаем это медленно

Декабрь 8, 2012 — Шарахов А.П.

Внимательный читатель наверняка заметил, что я еще ни разу не упомянул об ассемблерных вставках. Почему? На мой взгляд, в их применении нет никакой выгоды.

Во-первых, практически все необходимые программисту команды процессора либо доступны на языках высокого уровня, либо могут быть успешно сэмулированы (см., например, Эмуляция операторов SAR, ROR, ROL в Delphi).

Во-вторых, ассемблерные вставки затрудняют сопровождение и анализ кода. Уж лучше изменить алгоритм или, в крайнем случае, полностью переписать нагруженные процедуры на ассемблере.

В-третьих, ассемблерные вставки уменьшают возможности оптимизации кода компилятором. Например, из-за них компилятор может разместить часто используемые переменные в оперативной памяти, а не в регистрах, и вместо прироста производительности будет получен совершенно противоположный результат.

Рассмотрим это подробнее на примере функции MulRol, которая в цикле выполняет умножение (имитация полезной работы) и вращение данных (имитация необходимости ассемблерной вставки). Ниже приведены четыре различных варианта функции: полностью ассемблерный вариант, вариант на Pascal с эмуляцией, вариант на Pascal с вызовом подпрограммы на ассемблере, вариант на Pascal с ассемблерной вставкой.

function MulRolAsm(count, mul, rol: integer): integer;
asm
  test eax, eax
  jle @done
@loop:
  imul edx, edx, $BCEBCAD
  rol edx, cl
  sub eax, 1
  jg @loop
@done:
  mov eax, edx
  end;
 
function MulRolPas(count, mul, rol: integer): integer;
var
  rol2, temp: integer;
begin;
  rol2:=-rol;
  while count>0 do begin;
    mul:=mul * $BCEBCAD;
    temp:=mul shr rol2; mul:=mul shl rol; mul:=mul or temp; //mul:=mul ROL rol;
    dec(count);
    end;
  Result:=mul;
  end;
 
function MulRolMix(count, mul, rol: integer): integer;
begin;
  while count>0 do begin;
    mul:=mul * $BCEBCAD;
    asm
      mov edx, mul
      mov ecx, rol
      rol edx, cl
      mov mul, edx
      end;
    dec(count);
    end;
  Result:=mul;
  end;
 
function rol32(value, shift: longint): longint;
asm
  mov ecx, edx
  rol eax, cl
  end;
 
function MulRolCall(count, mul, rol: integer): integer;
begin;
  while count>0 do begin;
    mul:=mul * $BCEBCAD;
    mul:=rol32(mul, rol);
    dec(count);
    end;
  Result:=mul;
  end;
 
procedure TForm1.Button1Click(Sender: TObject);
type
  TMulRol= function(count, mul, rol: integer): integer;
const
  f: array[1..4] of TMulRol= (MulRolAsm, MulRolPas, MulRolCall, MulRolMix);
  n: array[low(f)..high(f)] of string= ('Asm', 'Pas', 'Call', 'Mix');
var
  r: array[low(f)..high(f)] of integer;
  t: array[low(f)-1..high(f)] of cardinal;
  i: integer;
begin;
  t[low(f)-1]:=GetTickCount;
  for i:=low(f) to high(f) do begin;
    r[i]:=f[i](1234567890, 123, 8);
    t[i]:=GetTickCount;
    end;
  for i:=low(f) to high(f) do
    Memo1.Lines.Add(Format('%d   %d   %s', [r[i], t[i]-t[i-1], n[i]]));
  end;

Мне пришлось немного изменить код эмуляции вращения по сравнению с кодом, приведенным в указанной выше статье, для того, чтобы время работы этого варианта заметно отличалось от варианта с вызовом подпрограммы. После этого места распределились следующим образом:

  Result     Time   Function
=============================
-277667881   1641   Asm
-277667881   2578   Pas
-277667881   2875   Call
-277667881   5765   Mix
=============================

на главную

Comments (4)

Некорректно

Если в MulRolMix перенести
  mul:=mul * $BCEBCAD;
в ассемблерную вставку:

  while count>0 do begin
    //mul:=mul * $BCEBCAD;  — убрать
    asm
      //mov edx, mul        — более не нужно
      imul edx, edx, $BCEBCAD
      //mov ecx, rol        — более не нужно
      rol edx, cl
      mov mul, edx
    end;
    dec(count);
  end;
  Result:=mul;

то MulRolMix будет работать ненамного медленнее, чем MulRolAsm.

Вывод — нужно быть очень осторожным при использовании ассемблерных вставок, т.к. компилятор/оптимизатор могут давать крайне непредсказуемый код...

rxt

Поддерживаю данную корректировку. Извините, но статья не интересна.
1. Если пишите на асме вставки, обязаны знать способ передачи параметров
1-ый EAX, 2-ой EDX, 3-ий ECX, дальше стек, а MulRolMix по кругу гоняет mov edx, edx.
2. Если пишите на асме, обязаны знать, как располагаются переменные в стеке, где база, и по какому смещению (-/+) обращаться к переменным.
Если игнорировать эти обязательства, то блок asm .. end; не более, чем для красоты. В ином случае, совсем не трудно будет решать такие задачи, как обратные вызовы с вложенными процедурами. Которые компилятор не в состоянии решить, а у некоторых гуру вызывает разрыв шаблона.

Автор поставил целью сделать это медленно, задачу выполнил, нареканий нет.

Вот результат из цели "Давайте сделаем это быстро" (кстати, неплохое название для Вашей следующей статьи ;)):

function Compare1(A,B: Integer): Integer;
begin
 if A > B then
  Result := 0 else
  Result := -1;
end;
 
 
function Compare2(A,B: Integer): Integer;
asm
  cmp EAX, EDX
  sbb EAX, EAX
end;

2rxt

По пунктам.
1. Позвольте спросить: вы чувствуете разницу между ассемблерной вставкой и ассемблерной процедурой? А между статьей и банкнотой? )
2. Тут вы, по существу, излагаете позицию автора.
3. В функциях сравнения часто нужно иметь не двоичный, а троичный результат, чтобы можно было их объединять. В простейшем случае, разумеется, достаточно функции IsLess(a,b).

Некорректно

Предполагается, что MulRolMix имитирует некоторую полезную работу на Pascal. Если ее перенести в ассемблерную вставку, то в процедуре фактически не останется никакой полезной работы. И в этом случае целесообразно процедуру переписать полностью на ассемблере, о чем и говорит автор.