Методы оптимизации сборки Intel x86 в примере задачи - PullRequest
21 голосов
/ 12 сентября 2009

Я довольно давно изучаю ассемблер и пытаюсь переписать некоторые простые процедуры \ функции, чтобы увидеть выигрыш в производительности (если есть). Мой основной инструмент разработки - 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, потому что сначала делает его копию под ecx, но не сохраняет ecx.

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

Мне очень интересны ваши комментарии.

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

Ответы [ 13 ]

16 голосов
/ 12 сентября 2009

В целом, я бы лично избегал попыток оптимизировать код с помощью трюков на уровне ассемблера, , если вам действительно нужны эти дополнительные 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
6 голосов
/ 14 сентября 2009

Ваш ассемблерный код работает медленно, так как использование конца стека записывает 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;
5 голосов
/ 12 сентября 2009

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

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

<code>
      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;

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

<code>
procedure DecodePixelsPS1Pas (EncPixels: Byte; var DecPixels: TDecodedPixels);
begin
  DecPixels := TDecodedPixels(Uint64DecPix[EncPixels]);
end;</p>

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

<p>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, вовлеченных в вызов подпрограммы.

- РЕДАКТИРОВАТЬ--: Я забыл сказать: поскольку вы неявно предполагаете что-то в отношении структуры памяти вашей структуры TDecodedPixels, было бы лучше, если бы вы объявили это как

<code>
PACKED ARRAY [0..7] of byte

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

<code>
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 голосов
/ 26 мая 2014

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

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

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)

Считая, что в little endian в виде 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 (и наоборот)?

4 голосов
/ 12 сентября 2009

Компиляторы отлично справляются с оптимизацией небольших подпрограмм.

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

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

3 голосов
/ 12 сентября 2009

Я собирался дать тот же алгоритм, что и Вутер ван Нифтерик.

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

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

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

2 голосов
/ 16 сентября 2009

Как насчет чего-то вроде:

/* 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
2 голосов
/ 12 сентября 2009

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

BT ax,1
SETC [dx]
inc dx

BT ax,2
SETC [dx]
inc dx

и т.д.

1 голос
/ 12 сентября 2009

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

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;

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

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

0 голосов
/ 01 марта 2012

Как вы заметили, разница в скорости в 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)

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

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--]
...