Подтвердить что ты не робот

Методы оптимизации сборки Intel x86 в выборочной задаче

Я изучаю ассемблер довольно долго, и я пытаюсь переписать некоторые простые процедуры\функции, чтобы увидеть преимущества производительности (если они есть). Моим основным инструментом разработки является Delphi 2007, и первые примеры будут на этом языке, но их можно легко перевести на другие языки.

Проблема заключается в следующем:

Мы дали значение без знака, в котором каждый из восьми бит представляет пиксель в одной строке экрана. Каждый отдельный пиксель может быть сплошным (1) или прозрачным (0). Таким образом, мы имеем 8 пикселей, упакованных в одно байтовое значение. Я хочу распаковать эти пиксели в массив из восьми байтов таким образом, что младший пиксель (бит) будет располагаться под самым низким индексом массива и так далее. Вот пример:

One byte value -----------> eight byte array

10011011 -----------------> [1][1][0][1][1][0][0][1]

Array index number ------->  0  1  2  3  4  5  6  7

Ниже представлены пять методов, которые решают проблему. Затем я покажу свое сравнение времени и то, как я это делал.

Мои вопросы состоят из двух частей:

1.

Я прошу вас дать подробный ответ относительно методов DecodePixels4a и DecodePixels4b. Почему метод 4b несколько медленнее, чем 4a?

Если, например, он медленнее, потому что мой код не выровнен правильно, тогда покажите мне, какие команды в данном методе могут быть лучше выровнены и как это сделать, чтобы не нарушить метод.

Я хотел бы видеть реальные примеры теории. Пожалуйста, имейте в виду, что я изучаю сборку, и я хочу получить знания из ваших ответов, что позволяет мне в будущем писать улучшенный оптимизированный код.

2.

Можете ли вы написать более быструю рутину, чем DecodePixels4a? Если да, представьте его и опишите шаги по оптимизации, которые вы предприняли. Посредством более быстрой подпрограммы я подразумеваю подпрограмму, которая выполняется в кратчайшие сроки в тестовой среде среди всех предложенных здесь процедур.

Разрешены все процессоры семейства Intel и совместимые с ними.

Ниже вы найдете написанные мной подпрограммы:

procedure DecodePixels1(EncPixels: Byte; var DecPixels: TDecodedPixels);
var
  i3: Integer;
begin
  DecPixels[0] := EncPixels and $01;
  for i3 := 1 to 7 do
  begin
    EncPixels := EncPixels shr 1;
    DecPixels[i3] := EncPixels and $01;
    //DecPixels[i3] := (EncPixels shr i3) and $01;  //this is even slower if you replace above 2 lines with it
  end;
end;


//Lets unroll the loop and see if it will be faster.
procedure DecodePixels2(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels[0] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[1] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[2] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[3] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[4] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[5] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[6] := EncPixels and $01;
  EncPixels := EncPixels shr 1;
  DecPixels[7] := EncPixels and $01;
end;


procedure DecodePixels3(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push eax;
    push ebx;
    push ecx;
    mov bl, al;
    and bl, $01;
    mov [edx], bl;
    mov ecx, $00;
@@Decode:
    inc ecx;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + ecx], bl;
    cmp ecx, $07;
    jnz @@Decode;
    pop ecx;
    pop ebx;
    pop eax;
  end;
end;


//Unrolled assembly loop
procedure DecodePixels4a(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push eax;
    push ebx;
    mov bl, al;
    and bl, $01;
    mov  [edx], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $01], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $02], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $03], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $04], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $05], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $06], bl;
    shr al, $01;
    mov bl, al;
    and bl, $01;
    mov [edx + $07], bl;
    pop ebx;
    pop eax;
  end;
end;


// it differs compared to 4a only in switching two instructions (but seven times)
procedure DecodePixels4b(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push eax;
    push ebx;
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx], bl;        //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $01], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $02], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $03], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $04], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $05], bl;  //
    mov bl, al;
    and bl, $01;
    shr al, $01;          //
    mov [edx + $06], bl;  //
    mov bl, al;
    and bl, $01;
    mov [edx + $07], bl;
    pop ebx;
    pop eax;
  end;
end;

И вот как я их тестирую:

program Test;

{$APPTYPE CONSOLE}

uses
  SysUtils, Windows;

type
  TDecodedPixels = array[0..7] of Byte;
var
  Pixels: TDecodedPixels;
  Freq, TimeStart, TimeEnd :Int64;
  Time1, Time2, Time3, Time4a, Time4b: Extended;
  i, i2: Integer;

begin
  if QueryPerformanceFrequency(Freq) then
  begin
    for i2 := 1 to 100 do
    begin
      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels1(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time1 := Time1 + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels2(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time2 := Time2 + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels3(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time3 := Time3 + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels4a(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time4a := Time4a + ((TimeEnd - TimeStart) / Freq * 1000);

      QueryPerformanceCounter(TimeStart);
      for i := 1 to 100000 do
        DecodePixels4b(155, Pixels);
      QueryPerformanceCounter(TimeEnd);
      Time4b := Time4b + ((TimeEnd - TimeStart) / Freq * 1000);

    end;
    Writeln('Time1 : ' + FloatToStr(Time1 / 100) + ' ms.    <- Delphi loop.');
    Writeln('Time2 : ' + FloatToStr(Time2 / 100) + ' ms.    <- Delphi unrolled loop.');
    Writeln('Time3 : ' + FloatToStr(Time3/ 100) + ' ms.    <- BASM loop.');
    Writeln('Time4a : ' + FloatToStr(Time4a / 100) + ' ms.    <- BASM unrolled loop.');
    Writeln('Time4b : ' + FloatToStr(Time4b / 100) + ' ms.    <- BASM unrolled loop instruction switch.');
  end;
  Readln;
end.

Ниже приведены результаты моей машины (Intel® Pentium® E2180 на Win32 XP):

Time1  : 1,68443549919493 ms.     <- Delphi loop.
Time2  : 1,33773024572211 ms.     <- Delphi unrolled loop.
Time3  : 1,37015271374424 ms.     <- BASM loop.
Time4a : 0,822916962526627 ms.    <- BASM unrolled loop.
Time4b : 0,862914462301607 ms.    <- BASM unrolled loop instruction switch.

Результаты довольно стабильны - время варьируется только на несколько процентов между каждым тестом, который я сделал. И это всегда верно: Time1 > Time3 > Time 2 > Time4b > Time4a

Итак, я думаю, что разница между Time4a и Time4b зависит от того, что инструкции переключаются в методе DecodePixels4b. Иногда это 4%, иногда это до 10%, но 4b всегда медленнее, чем 4a.

Я думал о другом методе с использованием инструкций MMX для записи в память восьми байтов за один раз, но я не могу найти быстрый способ распаковать байт в 64-разрядный регистр.

Спасибо за ваше время.


Спасибо, ребята, за ценный вклад. Если бы я мог ответить всем вам в одно и то же время, к сожалению, по сравнению с современным процессором у меня есть только один "труба" и может выполнять только одну инструкцию "ответ" в то время;-) Итак, я попытаюсь подытожить некоторые вещи здесь и написать дополнительные комментарии по вашим ответам.

Прежде всего, я хотел сказать, что перед отправкой моего вопроса я придумал решение, представленное Wouter van Nifterick, и это было фактически медленнее, чем мой код сборки. Поэтому я решил не публиковать эту процедуру здесь, но вы можете заметить, что я применил тот же подход и в моей программе Delphi для этой программы. Это прокомментировано там, потому что это давало мне более серьезные результаты.

Это загадка для меня. Я снова запустил свой код с помощью процедур Wouter и PhilS и вот результаты:

Time1  : 1,66535493194387 ms.     <- Delphi loop.
Time2  : 1,29115785420688 ms.     <- Delphi unrolled loop.
Time3  : 1,33716934524107 ms.     <- BASM loop.
Time4a : 0,795041753757838 ms.    <- BASM unrolled loop.
Time4b : 0,843520166815013 ms.    <- BASM unrolled loop instruction switch.
Time5  : 1,49457681191307 ms.     <- Wouter van Nifterick, Delphi unrolled
Time6  : 0,400587402866258 ms.    <- PhiS, table lookup Delphi
Time7  : 0,325472442519827 ms.    <- PhiS, table lookup Delphi inline
Time8  : 0,37350491544239 ms.     <- PhiS, table lookup BASM

Посмотрите на результат Time5, довольно странный, не так ли? Я предполагаю, что у меня есть другая версия Delphi, так как мой сгенерированный код сборки отличается от того, что предоставляется Wouter.

Второе главное изменение:


Я знаю, почему обычная 5 была медленнее на моей машине. Я проверил "Проверка диапазона" и "Проверка переполнения" в моих параметрах компилятора. Я добавил директиву assembler в подпрограмму 9, чтобы узнать, помогает ли она. Похоже, что с этой директивной процедурой сборки так же хорошо, как в Delphi встроенный вариант или даже немного лучше.

Вот окончательные результаты:

Time1  : 1,22508325749317 ms.     <- Delphi loop.
Time2  : 1,33004145373084 ms.     <- Delphi unrolled loop.
Time3  : 1,1473583622526 ms.      <- BASM loop.
Time4a : 0,77322594033463 ms.     <- BASM unrolled loop.
Time4b : 0,846033593023372 ms.    <- BASM unrolled loop instruction switch.
Time5  : 0,688689382044384 ms.    <- Wouter van Nifterick, Delphi unrolled
Time6  : 0,503233741036693 ms.    <- PhiS, table lookup Delphi
Time7  : 0,385254722925063 ms.    <- PhiS, table lookup Delphi inline
Time8  : 0,432993919452751 ms.    <- PhiS, table lookup BASM
Time9  : 0,362680491244212 ms.    <- PhiS, table lookup BASM with assembler directive

Третье главное изменение:


По мнению @Pascal Cuoq и @j_random_hacker, разница в времени выполнения между подпрограммами 4a, 4b и 5 обусловлена ​​зависимостью данных. Однако я должен не согласиться с этим мнением, основываясь на дальнейших тестах, которые я сделал.

Я также изобрел новую процедуру 4c на основе 4a. Вот он:

procedure DecodePixels4c(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  asm
    push ebx;
    mov bl, al;
    and bl, 1;
    mov [edx], bl;
    mov bl, al;
    shr bl, 1;
    and bl, 1;
    mov [edx + $01], bl;
    mov bl, al;
    shr bl, 2;
    and bl, 1;
    mov [edx + $02], bl;
    mov bl, al;
    shr bl, 3;
    and bl, 1;
    mov [edx + $03], bl;
    mov bl, al;
    shr bl, 4;
    and bl, 1;
    mov [edx + $04], bl;
    mov bl, al;
    shr bl, 5;
    and bl, 1;
    mov [edx + $05], bl;
    mov bl, al;
    shr bl, 6;
    and bl, 1;
    mov [edx + $06], bl;
    shr al, 7;
    and al, 1;
    mov [edx + $07], al;
    pop ebx;
  end;
end;

Я бы сказал, что он довольно зависим от данных.

И вот тесты и результаты. Я сделал четыре теста, чтобы убедиться, что это не случайность. Я также добавил новые времена для подпрограмм, предложенных GJ (Time10a, Time10b).

          Test1  Test2  Test3  Test4

Time1   : 1,211  1,210  1,220  1,213
Time2   : 1,280  1,258  1,253  1,332
Time3   : 1,129  1,138  1,130  1,160

Time4a  : 0,690  0,682  0,617  0,635
Time4b  : 0,707  0,698  0,706  0,659
Time4c  : 0,679  0,685  0,626  0,625
Time5   : 0,715  0,682  0,686  0,679

Time6   : 0,490  0,485  0,522  0,514
Time7   : 0,323  0,333  0,336  0,318
Time8   : 0,407  0,403  0,373  0,354
Time9   : 0,352  0,378  0,355  0,355
Time10a : 1,823  1,812  1,807  1,813
Time10b : 1,113  1,120  1,115  1,118
Time10c : 0,652  0,630  0,653  0,633
Time10d : 0,156  0,155  0,172  0,160  <-- current winner!

Как вы можете видеть результаты 4a, 4b, 4c и 5 очень близки друг к другу. Почему это? Поскольку я удален из 4a, 4b (4c уже не имеет его), две инструкции: push eax и pop eax. Поскольку я знаю, что я не буду использовать где-либо еще в своем коде значение под eax, мне не нужно его защищать. Теперь мой код имеет только одну пару push/pop, а также процедуру 5. Обычная 5 пресервирует значение eax beacause, прежде всего, сделав ее копию под ecx, но не deser't preereserve ecx.

Итак, я пришел к выводу, что разница во времени исполнения 5 и 4a и 4b (до третьего редактирования) не относилась к данным зависимым, но была вызвана дополнительной парой инструкций push/pop.

Я очень заинтересован в ваших комментариях.

Через несколько дней GJ изобрел даже более быструю рутину (Time 10d), чем PhiS. Хорошая работа GJ!

4b9b3361

Ответ 1

Ваш код asm является относительной медленностью, потому что использовать конец стека, 8 раз записывать в память. Проверьте это...

procedure DecodePixels(EncPixels: Byte; var DecPixels: TDecodedPixels);
asm
  xor   ecx, ecx
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 1
  mov   [DecPixels + 4], ecx
  xor   ecx, ecx
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 8
  add   al, al
  rcl   ecx, 1
  mov   [DecPixels], ecx
end;

Возможно, даже быстрее, чем код с помощью таблицы поиска!

Улучшенная версия:

procedure DecodePixelsI(EncPixels: Byte; var DecPixels: TDecodedPixels);
asm
  mov   ecx, 0    //Faster than: xor   ecx, ecx
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  ror   ecx, 16
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  mov   [DecPixels + 4], ecx
  mov   ecx, 0    //Faster than: xor   ecx, ecx
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  ror   ecx, 16
  add   al, al
  rcl   ch, 1
  add   al, al
  rcl   cl, 1
  mov   [DecPixels], ecx
end;

Версия 3:

procedure DecodePixelsX(EncPixels: Byte; var DecPixels: TDecodedPixels);
asm
  add   al, al
  setc  byte ptr[DecPixels + 7]
  add   al, al
  setc  byte ptr[DecPixels + 6]
  add   al, al
  setc  byte ptr[DecPixels + 5]
  add   al, al
  setc  byte ptr[DecPixels + 4]
  add   al, al
  setc  byte ptr[DecPixels + 3]
  add   al, al
  setc  byte ptr[DecPixels + 2]
  add   al, al
  setc  byte ptr[DecPixels + 1]
  setnz byte ptr[DecPixels]
end;

Версия 4:

const Uint32DecPix : array [0..15] of cardinal = (
  $00000000, $00000001, $00000100, $00000101,
  $00010000, $00010001, $00010100, $00010101,
  $01000000, $01000001, $01000100, $01000101,
  $01010000, $01010001, $01010100, $01010101
  );

procedure DecodePixelsY(EncPixels: byte; var DecPixels: TDecodedPixels); inline;
begin
  pcardinal(@DecPixels)^ := Uint32DecPix[EncPixels and $0F];
  pcardinal(cardinal(@DecPixels) + 4)^ := Uint32DecPix[(EncPixels and $F0) shr 4];
end;

Ответ 2

В общем, я лично не буду пытаться оптимизировать код, используя трюки на уровне ассемблера, , если вам действительно не нужны дополнительные 2 или 3% скорости, и вы готовы заплатить цену за код, который сложнее читать, обслуживать и порт.

Чтобы сжать последние 1%, вам даже придется поддерживать несколько версий, оптимизированных для каждого процессора, и если появятся более новые процессоры и улучшенный компилятор pascal, вы не выиграете от него.

Этот код Delphi быстрее, чем ваш самый быстрый код ассемблера:

procedure DecodePixels5(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels[0] := (EncPixels shr 0) and $01;
  DecPixels[1] := (EncPixels shr 1) and $01;
  DecPixels[2] := (EncPixels shr 2) and $01;
  DecPixels[3] := (EncPixels shr 3) and $01;
  DecPixels[4] := (EncPixels shr 4) and $01;
  DecPixels[5] := (EncPixels shr 5) and $01;
  DecPixels[6] := (EncPixels shr 6) and $01;
  DecPixels[7] := (EncPixels shr 7) and $01;
end;


Results:

Time1  : 1,03096806151283 ms.    <- Delphi loop.
Time2  : 0,740308641141395 ms.   <- Delphi unrolled loop.
Time3  : 0,996602425688886 ms.   <- BASM loop.
Time4a : 0,608267951561275 ms.   <- BASM unrolled loop.
Time4b : 0,574162510648039 ms.   <- BASM unrolled loop instruction switch.
Time5  : 0,499628206138524 ms. !!!  <- Delphi unrolled loop 5.

Это быстро, потому что операции могут выполняться только с помощью регистров, а не для хранения и извлечения памяти. Современные процессоры выполняют это частично параллельно (новая операция может быть запущена до предыдущего завершения), потому что результаты последовательных инструкций не зависят друг от друга.

Код машины выглядит следующим образом:

  push ebx;
  // DecPixels[0] := (EncPixels shr 0) and 1;
  movzx ecx,al
  mov ebx,ecx
  //  shr ebx,$00
  and bl,$01
  mov [edx],bl
  // DecPixels[1] := (EncPixels shr 1) and 1;
  mov ebx,ecx
  shr ebx,1
  and bl,$01
  mov [edx+$01],bl
  // DecPixels[2] := (EncPixels shr 2) and 1;
  mov ebx,ecx
  shr ebx,$02
  and bl,$01
  mov [edx+$02],bl
  // DecPixels[3] := (EncPixels shr 3) and 1;
  mov ebx,ecx
  shr ebx,$03
  and bl,$01
  mov [edx+$03],bl
  // DecPixels[4] := (EncPixels shr 4) and 1;
  mov ebx,ecx
  shr ebx,$04
  and bl,$01
  mov [edx+$04],bl
  // DecPixels[5] := (EncPixels shr 5) and 1;
  mov ebx,ecx
  shr ebx,$05
  and bl,$01
  mov [edx+$05],bl
  // DecPixels[6] := (EncPixels shr 6) and 1;
  mov ebx,ecx
  shr ebx,$06
  and bl,$01
  mov [edx+$06],bl
  // DecPixels[7] := (EncPixels shr 7) and 1;
  shr ecx,$07
  and cl,$01
  mov [edx+$07],cl
  pop ebx;

Изменить: как было предложено, поиск в таблице действительно быстрее.

var
  PixelLookup:Array[byte] of TDecodedPixels;

// You could precalculate, but the performance gain would hardly be worth it because you call this once only.
for I := 0 to 255 do
  DecodePixels5b(I, PixelLookup[I]);


procedure DecodePixels7(EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels := PixelLookup[EncPixels];
end;

Results:

Time1  : 1,03096806151283 ms.    <- Delphi loop.
Time2  : 0,740308641141395 ms.   <- Delphi unrolled loop.
Time3  : 0,996602425688886 ms.   <- BASM loop.
Time4a : 0,608267951561275 ms.   <- BASM unrolled loop.
Time4b : 0,574162510648039 ms.   <- BASM unrolled loop instruction switch.
Time5  : 0,499628206138524 ms. !!!  <- Delphi unrolled loop 5.
Time7 : 0,251533475182096 ms.    <- simple table lookup

Ответ 3

Развернувшись на ответе Nick D, я попробовал следующие версии на основе табличного поиска, все , которые быстрее, чем реализации, которые вы даете (и быстрее, чем код Wouter van Nifterick).

Учитывая следующий упакованный массив:


      const Uint64DecPix : PACKED ARRAY [0..255] OF UINT64 =
  ( $0000000000000000, $0000000000000001, $0000000000000100, $0000000000000101, $0000000000010000, $0000000000010001, $0000000000010100, $0000000000010101, $0000000001000000, $0000000001000001, $0000000001000100, $0000000001000101, $0000000001010000, $0000000001010001, $0000000001010100, $0000000001010101,
    $0000000100000000, $0000000100000001, $0000000100000100, $0000000100000101, $0000000100010000, $0000000100010001, $0000000100010100, $0000000100010101, $0000000101000000, $0000000101000001, $0000000101000100, $0000000101000101, $0000000101010000, $0000000101010001, $0000000101010100, $0000000101010101,
    $0000010000000000, $0000010000000001, $0000010000000100, $0000010000000101, $0000010000010000, $0000010000010001, $0000010000010100, $0000010000010101, $0000010001000000, $0000010001000001, $0000010001000100, $0000010001000101, $0000010001010000, $0000010001010001, $0000010001010100, $0000010001010101,
    $0000010100000000, $0000010100000001, $0000010100000100, $0000010100000101, $0000010100010000, $0000010100010001, $0000010100010100, $0000010100010101, $0000010101000000, $0000010101000001, $0000010101000100, $0000010101000101, $0000010101010000, $0000010101010001, $0000010101010100, $0000010101010101,
    $0001000000000000, $0001000000000001, $0001000000000100, $0001000000000101, $0001000000010000, $0001000000010001, $0001000000010100, $0001000000010101, $0001000001000000, $0001000001000001, $0001000001000100, $0001000001000101, $0001000001010000, $0001000001010001, $0001000001010100, $0001000001010101,
    $0001000100000000, $0001000100000001, $0001000100000100, $0001000100000101, $0001000100010000, $0001000100010001, $0001000100010100, $0001000100010101, $0001000101000000, $0001000101000001, $0001000101000100, $0001000101000101, $0001000101010000, $0001000101010001, $0001000101010100, $0001000101010101,
    $0001010000000000, $0001010000000001, $0001010000000100, $0001010000000101, $0001010000010000, $0001010000010001, $0001010000010100, $0001010000010101, $0001010001000000, $0001010001000001, $0001010001000100, $0001010001000101, $0001010001010000, $0001010001010001, $0001010001010100, $0001010001010101,
    $0001010100000000, $0001010100000001, $0001010100000100, $0001010100000101, $0001010100010000, $0001010100010001, $0001010100010100, $0001010100010101, $0001010101000000, $0001010101000001, $0001010101000100, $0001010101000101, $0001010101010000, $0001010101010001, $0001010101010100, $0001010101010101,
    $0100000000000000, $0100000000000001, $0100000000000100, $0100000000000101, $0100000000010000, $0100000000010001, $0100000000010100, $0100000000010101, $0100000001000000, $0100000001000001, $0100000001000100, $0100000001000101, $0100000001010000, $0100000001010001, $0100000001010100, $0100000001010101,
    $0100000100000000, $0100000100000001, $0100000100000100, $0100000100000101, $0100000100010000, $0100000100010001, $0100000100010100, $0100000100010101, $0100000101000000, $0100000101000001, $0100000101000100, $0100000101000101, $0100000101010000, $0100000101010001, $0100000101010100, $0100000101010101,
    $0100010000000000, $0100010000000001, $0100010000000100, $0100010000000101, $0100010000010000, $0100010000010001, $0100010000010100, $0100010000010101, $0100010001000000, $0100010001000001, $0100010001000100, $0100010001000101, $0100010001010000, $0100010001010001, $0100010001010100, $0100010001010101,
    $0100010100000000, $0100010100000001, $0100010100000100, $0100010100000101, $0100010100010000, $0100010100010001, $0100010100010100, $0100010100010101, $0100010101000000, $0100010101000001, $0100010101000100, $0100010101000101, $0100010101010000, $0100010101010001, $0100010101010100, $0100010101010101,
    $0101000000000000, $0101000000000001, $0101000000000100, $0101000000000101, $0101000000010000, $0101000000010001, $0101000000010100, $0101000000010101, $0101000001000000, $0101000001000001, $0101000001000100, $0101000001000101, $0101000001010000, $0101000001010001, $0101000001010100, $0101000001010101,
    $0101000100000000, $0101000100000001, $0101000100000100, $0101000100000101, $0101000100010000, $0101000100010001, $0101000100010100, $0101000100010101, $0101000101000000, $0101000101000001, $0101000101000100, $0101000101000101, $0101000101010000, $0101000101010001, $0101000101010100, $0101000101010101,
    $0101010000000000, $0101010000000001, $0101010000000100, $0101010000000101, $0101010000010000, $0101010000010001, $0101010000010100, $0101010000010101, $0101010001000000, $0101010001000001, $0101010001000100, $0101010001000101, $0101010001010000, $0101010001010001, $0101010001010100, $0101010001010101,
    $0101010100000000, $0101010100000001, $0101010100000100, $0101010100000101, $0101010100010000, $0101010100010001, $0101010100010100, $0101010100010101, $0101010101000000, $0101010101000001, $0101010101000100, $0101010101000101, $0101010101010000, $0101010101010001, $0101010101010100, $0101010101010101);
PUint64DecPix : pointer = @Uint64DecPix;

вы можете написать следующее:


procedure DecodePixelsPS1Pas (EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels := TDecodedPixels(Uint64DecPix[EncPixels]);
end;

procedure DecodePixelsPS1PasInline (EncPixels: Byte; var DecPixels: TDecodedPixels); inline; begin DecPixels := TDecodedPixels(Uint64DecPix[EncPixels]); end;

procedure DecodePixelsPS1Asm (EncPixels: Byte; var DecPixels: TDecodedPixels); asm lea ecx, Uint64DecPix //[<-Added in EDIT 3] //mov ecx, dword ptr PUint64DecPix - alternative to the above line (slower for me) movzx eax, al movq xmm0, [8*eax+ecx] //Using XMM rather than MMX so we don't have to issue emms at the end movq [edx], xmm0 //use MOVQ because it doesn't need mem alignment end;

Стандартные реализации PAS и ASM довольно похожи по скорости, но реализация PAS с надписью "INLINE" является самой быстрой, поскольку она избавляется от всего вызова /ret, участвующего в вызове подпрограммы.

- EDIT--: Я забыл сказать: поскольку вы неявно предполагаете что-то о макете памяти вашей структуры TDecodedPixels, было бы лучше, если вы объявите ее как


PACKED ARRAY [0..7] of byte

- EDIT2--: Вот мои результаты для сравнения:


Time1 : 2.51638266874701 ms.    <- Delphi loop.
Time2 : 2.11277620479698 ms.    <- Delphi unrolled loop.
Time3 : 2.21972066282167 ms.    <- BASM loop.
Time4a : 1.34093090043567 ms.    <- BASM unrolled loop.
Time4b : 1.52222070123437 ms.    <- BASM unrolled loop instruction switch.
Time5 : 1.17106364076999 ms.    <- Wouter van Nifterick
TimePS1 : 0.633099318488802 ms.    <- PS.Pas
TimePS2 : 0.551617593856202 ms.    <- PS.Pas Inline
TimePS3 : 0.70921094720139 ms.    <- PS.Asm (speed for version before 3rd EDIT)

Ответ 4

Составители делают очень хорошую работу по оптимизации небольших подпрограмм.

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

Изменить: Обратите внимание, что процессоры Pentium могут выполнять определенные инструкции параллельно (суперскалярная архитектура), это называется спаривание.

Ответ 5

Чистое программное решение

Используя красивую технику из этого вопроса, которая снова была вдохновлена этим вопросом, у нас будет отличное решение, подобное этому, с одной строкой кода (исключая декларации)

type TPackedDecodedPixels = record
case integer of
  0: (a: TDecodedPixels);
  1: (v: Int64);
end;

procedure DecodePixels(EncPixels: byte; var DecPixels: TDecodedPixels); inline;
const
  magic = $8040201008040201;
  mask  = $8080808080808080;
begin
  TPackedDecodedPixels(DecPixels).v := SwapEndian(((EncPixels*magic) and mask) shr 7);
end;

Конечно, вам нужно убедиться, что DecPixels правильно выровнен по 8 байтам, иначе вы можете столкнуться с некоторым замедлением (или даже с ошибками на других архитектурах). Вы также можете легко векторизовать функцию, чтобы сделать ее быстрее

Объяснение

Предположим, у нас есть следующая битовая комбинация как abcdefgh. Мы хотим, чтобы выходной массив содержал

0000000a 0000000b 0000000c 0000000d 0000000e 0000000f 0000000g 0000000h (1)

Считая 64-разрядное целое число с прямым порядком байтов, мы получим %0000000h0000000g0000000f0000000e0000000d0000000c0000000b0000000a. Мы должны найти магическое число, которое сдвигает исходные биты в позиции, в которых мы можем извлечь необходимые биты

Давайте умножим значение на магическое число

  |  b7  ||  b6  ||  b4  ||  b4  ||  b3  ||  b2  ||  b1  ||  b0  |
                                                          abcdefgh (1-byte value)
x 1000000001000000001000000001000000001000000001000000001000000001
  ────────────────────────────────────────────────────────────────
= h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh

В этот момент все биты пикселей были перемещены в старшие значащие биты соответствующих байтов. Поскольку они уже лежали в нужном месте, нам просто нужно удалить оставшиеся биты с помощью and

  |  b7  ||  b6  ||  b4  ||  b4  ||  b3  ||  b2  ||  b1  ||  b0  |
  h0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh0abcdefgh
& 1000000010000000100000001000000010000000100000001000000010000000
  ────────────────────────────────────────────────────────────────
= h0000000g0000000f0000000e0000000d0000000c0000000b0000000a0000000 (8-byte array)

Теперь биты пикселей находятся в старших значащих битах соответствующих байтов, нам нужно сделать логический сдвиг вправо на 7, чтобы переместить их в наименее значимую позицию. Поскольку OP хочет значение в обратном порядке, нам нужно SwapEndian(), чтобы преобразовать байты в старшие порядковые числа. Если вам нужен только порядок байтов, вы можете остановиться на этом шаге

Таким образом, магическое число - %1000000001000000001000000001000000001000000001000000001000000001 = $8040201008040201, а маска - %1000000010000000100000001000000010000000100000001000000010000000 = $8080808080808080. Конечно, в действительности, чтобы решить проблему и получить эти значения, мы должны сделать обратное от конечного результата → умноженный результат → магическое число


Но почему я поместил байты в порядке с прямым порядком байтов в (1), а затем должен был преобразовать обратно в старший порядок байтов? Почему бы просто не расположить байты в порядке с прямым порядком байтов и найти магическое число для этого? Если вам интересно об этом, то это потому, что так будет работать не более 7 бит за раз. Я сделал это в своем старом ответе, и мне нужно немного его разделить, а потом объединить обратно

                                                          0abcdefg
x 0000000000000010000001000000100000010000001000000100000010000001
  ────────────────────────────────────────────────────────────────
= 00000000abcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefgabcdefg
& 0000000000000001000000010000000100000001000000010000000100000001
  ────────────────────────────────────────────────────────────────    
= 000000000000000a0000000b0000000c0000000d0000000e0000000f0000000g

Аппаратная поддержка

На самом деле это особый случай битового расширения с постоянной маской. В AVX2 Intel ввела для этой цели инструкцию pdep в наборе команд BMI2, поэтому для получения результата вам нужна всего лишь одна инструкция. На других языках вы можете использовать это с встроенной функцией _pext_u64. К сожалению, AFAIK Free Pascal не поддерживает его, и вы должны использовать сборку напрямую. Однако выражение будет выглядеть так

TPackedDecodedPixels(DecPixels).v := _pext_u64(EncPixels, $0101010101010101);

Проверка правильности

Я попытался сравнить версию OP с обеими моими версиями и до сих пор не нашел никаких проблем. вывод компилятора похож на это

mov al, dil
mov rbx, rsi
movzx edi, al
movabs rax, 0x8040201008040201
imul rdi, rax
movabs rax, 0x8080808080808080
and rdi, rax
shr rdi, 0x7
call 4016a0 <SYSTEM_$$_SWAPENDIAN$INT64$$INT64>
mov QWORD PTR [rbx], rax

Выход FPC все еще в значительной степени неоптимален, поскольку компилятор не знает, как заменить вызов SwapEndian на BSWAP, и копирует данные без необходимости. Почему mov al, dil; movzx edi, al вместо просто movzx edi, dil? Как видите, выходные данные компиляторов C и C++ намного лучше

См. Как создать байт из 8 значений типа bool (и наоборот)?

Ответ 6

Я собирался дать тот же алгоритм, что и Wouter van Nifterick.

Кроме того, я бы объяснил лучшую производительность с точки зрения цепочек зависимостей. В каждой из предложенных вами версий, когда вы развернули свой основной цикл, вы сохранили зависимость между двумя последовательными итерациями: каждый из ваших сухих, $01; требует, чтобы предыдущее значение al было вычислено. Если вы организуете развернутые итерации таким образом, чтобы их можно было выполнять параллельно, они фактически будут на современном процессоре. Не обманывайте ложными зависимостями, которые могут быть подавлены переименованием регистров.

Кто-то отметил, что Pentium может выполнять сразу две инструкции. Это правда, но современные процессоры (поскольку Pentium Pro, PII,..., Core, Core 2) выполняют гораздо больше двух команд одновременно, когда у них есть шанс - то есть, когда нет зависимости между выполняемыми инструкциями. Обратите внимание, что в версии Wouter van Nifterick каждая строка может быть выполнена независимо от других.

http://www.agner.org/optimize/ содержит всю необходимую информацию, чтобы понять архитектуру современных процессоров и как их использовать.

Ответ 7

если вы поддерживаете только 80386 и выше, вы можете использовать команды BTcc и SETcc следующим образом:

BT ax,1
SETC [dx]
inc dx

BT ax,2
SETC [dx]
inc dx

и т.д.

Ответ 8

Как насчет чего-то типа:

/* input byte in eax, address to store result in edx */
and eax, 0xff    /* may not be needed */
mov ebx, eax
shl ebx, 7
or  eax, ebx
mov ebx, eax
shl ebx, 14
or  eax, ebx
mov ebx, eax
and eax, 0x01010101
mov [edx], eax
shr ebx, 4
and ebx, 0x01010101
mov [edx+4], ebx

Ответ 9

Вероятная причина, по которой 4b быстрее, чем 4a, состоит в том, что она лучше распараллеливается. Из 4a:

mov bl, al;
and bl, $01;          // data dep (bl)
mov  [edx], bl;       // data dep (bl)
shr al, $01;
mov bl, al;           // data dep (al)
and bl, $01;          // data dep (bl)
mov [edx + $01], bl;  // data dep (bl)

Инструкции с надписью "data dep" не могут начинаться до завершения предыдущей инструкции, и я написал регистры, которые вызывают эту зависимость данных. Современные процессоры могут запускать инструкцию до завершения последней, если нет зависимости. Но способ, которым вы заказали эти операции, предотвращает это.

В 4b у вас меньше зависимостей данных:

mov bl, al;
and bl, $01;          // data dep (bl)
shr al, $01;
mov [edx], bl;
mov bl, al;
and bl, $01;          // data dep (bl)
shr al, $01;
mov [edx + $01], bl;

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

Я не могу гарантировать, что это причина разницы в скорости, но это вероятный кандидат. К сожалению, трудно найти ответы как абсолютные, чем те, которые вы ищете; современные процессоры имеют отраслевые предсказатели, многоуровневые кэши, аппаратные предварительные подборщики и всевозможные другие сложности, которые могут затруднить выделение причин различий в производительности. Лучшее, что вы можете сделать, это прочитать много, выполнить эксперименты и ознакомиться с инструментами для проведения хороших измерений.

Ответ 10

Я предполагаю, что запись в память (на самом деле, кэш-память) происходит медленнее, чем работа с регистрами.

Итак,

mov [edx+...], bl
shr al, $01;
mov bl, al;

дает процессору некоторое время для записи bl в память, прежде чем снова понадобится регистр bl, а

shr al, $01;
mov [edx], bl;
mov bl, al;

требуется bl немедленно, чтобы процессор остановился и дождался завершения записи в памяти.

Это удивительно для меня. Современные процессоры Intel делают сумасшедшую конвейерную обработку и регистрируют переименование, поэтому, на мой взгляд, DecodePixels4b должен быть быстрее, так как зависимости каждой команды снова возвращаются. Вышеупомянутое объяснение я могу предложить, кроме этого:

x86 - ужасный набор инструкций, и Intel делает потрясающий и очень продвинутый фокус-фокус, чтобы сделать его эффективным. Если бы я был вами, я бы посмотрел на что-то еще. Сегодня очень мало спроса на megaMcOptimised программное обеспечение для ПК. Мое дружелюбное предложение - изучить процессоры для мобильных устройств (в основном ARM), поскольку в мобильных устройствах скорость процессора, энергопотребление и время автономной работы означают, что программное обеспечение с микрооптимизацией более важно. И ARM имеет превосходный набор инструкций для x86.

Ответ 11

SIMD

Если вы расширите алгоритм обработки массивов, то SIMD станет опцией оптимизации. Здесь версия SIMD, которая на 1/3 времени оптимизированного эквивалента C:

int main ()
{
  const int
    size = 0x100000;

  unsigned char
    *source = new unsigned char [size],
    *dest,
    *dest1 = new unsigned char [size * 32],
    *dest2 = new unsigned char [size * 32];

  for (int i = 0 ; i < size ; ++i)
  {
    source [i] = rand () & 0xff;
  }

  LARGE_INTEGER
    start,
    middle,
    end;

  QueryPerformanceCounter (&start);
  dest = dest1;
  for (int i = 0 ; i < size ; ++i)
  {
    unsigned char
      v = source [i];

    for (int b = 0 ; b < 8 ; ++b)
    {
      *(dest++) = (v >> b) & 1;
    }
  }
  unsigned char
    bits [] = {1,2,4,8,16,32,64,128,1,2,4,8,16,32,64,128},
    zero [] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0},
    ones [] = {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1};

  QueryPerformanceCounter (&middle);
  __asm
  {
    movdqu xmm1,bits
    movdqu xmm2,zero
    movdqu xmm3,ones
    mov ecx,0x100000/4
    mov esi,source
    mov edi,dest2
l1:
    lodsd
    movd xmm0,eax
    movd xmm4,eax
    punpcklbw xmm0,xmm0
    punpcklbw xmm4,xmm4
    punpcklwd xmm0,xmm0
    punpcklwd xmm4,xmm4
    punpckldq xmm0,xmm0
    punpckhdq xmm4,xmm4
    pand xmm0,xmm1
    pand xmm4,xmm1
    pcmpeqb xmm0,xmm2
    pcmpeqb xmm4,xmm2
    paddb xmm0,xmm3
    paddb xmm4,xmm3
    movdqu [edi],xmm0
    movdqu [edi+16],xmm4
    add edi,32
    dec ecx
    jnz l1
  }
  QueryPerformanceCounter (&end);

  cout << "Time taken = " << (middle.QuadPart - start.QuadPart) << endl;
  cout << "Time taken = " << (end.QuadPart - middle.QuadPart) << endl;
  cout << "memcmp = " << memcmp (dest1, dest2, size * 32) << endl;

  return 0;
}

Ответ 12

Невероятное интеллектуальное решение Chris, что бы вы сделали с обратной задачей: создать байт из массива из 8 байтов?

Не оптимизированное решение для обратной задачи:

BtBld PROC Array:DWORD, Pixels:DWORD
  mov  eax, [Array]
  add  eax, 7
  mov  edx, [Pixels]

  mov  bx, 0

  mov  ecx, 8
rpt:  or  bx, [eax]
  dec  eax
  shl  bx, 1
  loop rpt
  shr  bx, 1
  mov  [edx], bl
  ret
BtBld ENDP

Ответ 13

Как вы заметили, разница в скорости в реализациях 4a и 4b обусловлена ​​оптимизацией ЦП (путем выполнения нескольких инструкций в инструкции параллельной/конвейерной обработки). Но фактор не в операндах, а из-за природы самого оператора.

4a Instruction Sequence:
AND - MOV - SHR

4b Instruction Sequence:
AND - SHR - MOV

Оба AND и SHR используют регистр флагов, поэтому эти две команды имеют состояние ожидания в своем конвейере.

Прочитайте их следующим образом:

4a: AND (piped) MOV (piped) SHR
4b: AND (WAIT) SHR (piped) MOV

Заключение: 4b имеет еще 7 состояний ожидания в конвейере, чем 4a, поэтому он медленнее.

Джош упомянул, что существуют зависимости данных, то есть:

mov bl, al;
and bl, $01;          // data dep (bl)

но это не совсем так, так как эти две команды могут частично выполняться в паралеле на уровне CPU:

mov bl, al -> (A:) read al (B:) write bl  => (2 clocks in i386)
and bl, 01 -> (C:) read 01 (D:) write bl  => idem

Последовательно они берут 4 такта, но конвейерно они берут всего 3 "часа" (на самом деле термин "часы" не соответствует перспективам трубопровода, но я использовал его в контексте простоты)

[--A--][--B--]
 [--C--]<wait>[---D--]