【问题标题】:Intel x86 assembly optimization techniques for expanding 8 bits to 8 boolean bytes of 0 or 1英特尔 x86 汇编优化技术,用于将 8 位扩展为 0 或 1 的 8 个布尔字节
【发布时间】:2023-03-11 00:22:01
【问题描述】:

我学习汇编程序已经有一段时间了,我正在尝试重写一些简单的程序\函数来查看性能优势(如果有的话)。我的主要开发工具是 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.

我要求您提供有关方法 DecodePixels4aDecodePixels4b详细答案。为什么方法4b4a 慢一些?

例如,如果由于我的代码未正确对齐而导致速度较慢,那么请告诉我给定方法中的哪些指令可以更好地对齐,以及如何做到这一点以不破坏方法。

我想看看理论背后的真实例子。请记住,我正在学习汇编,我想从您的答案中获得知识,这使我将来能够编写更好的优化代码。

2.

你能写出比DecodePixels4a 更快的例程吗?如果是这样,请展示它并描述您已采取的优化步骤。 更快的例程我的意思是在此处介绍的所有例程中,在您的测试环境中运行时间最短的例程。

允许使用所有英特尔系列处理器以及与之兼容的处理器。

下面是我写的套路:

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.

这是我机器上的结果(Win32 XP 上的 Intel® Pentium® E2180):

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 &gt; Time3 &gt; Time 2 &gt; Time4b &gt; Time4a

所以我认为 Time4a 和 Time4b 之间的差异取决于方法 DecodePixels4b 中的指令切换。有时是 4%,有时是 10%,但 4b 总是比 4a 慢。

我正在考虑另一种使用 MMX 指令一次写入内存 8 个字节的方法,但我无法找到将字节解压到 64 位寄存器的快速方法。

感谢您的宝贵时间。


感谢你们的宝贵意见。虽然我可以同时回答你们所有人,但不幸的是,与现代 CPU 相比,我只有一个“管道”并且当时只能执行一条指令“回复”;-) 所以,我会尝试在这里总结一些东西,并在你的答案下写额外的 cmets。

首先,我想说的是,在发布我的问题之前,我想出了 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 看来,例程4a4b5 之间的执行时间差异是由数据依赖性引起的。但是,基于我所做的进一步测试,我不得不不同意这个观点。

我还在4a 的基础上发明了新的例程4c。这里是:

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!

您可能会看到4a4b4c5 的结果非常接近。 这是为什么?因为我从 4a、4b(4c 已经没有)中删除了两条指令:push eaxpop eax。因为我知道我不会在代码中的其他任何地方使用 eax 下的值,所以我不必保留它。 现在我的代码只有一对推送/弹出,如例程 5。 例程 5 保留 eax 的值,因为它首先在 ecx 下复制它,但它不保留 ecx。

所以我的结论是:5 和 4a 和 4b 的执行时间差异(在第三次编辑之前)不涉及数据依赖性,而是由额外的一对推送/弹出指令引起的 .

我对你的 cmets 很感兴趣。

几天后,GJ 发明了比 PhiS 更快的例程(时间 10d)。干得好 GJ!

【问题讨论】:

  • Nitpick:我认为您的意思是“实心或透明”。 “不透明”的意思是“看不透”。
  • @j_random_hacker:谢谢,会改正的。
  • 通常,Delphi“汇编程序”指令不做任何事情(只是为了与 Turbo Pascal 向后兼容),所以我有点惊讶。您使用的是哪个 Delphi 版本?您是否打开了任何编译器选项以始终生成堆栈帧或类似的东西?
  • 我刚刚检查过,用“assembler”标记 BASM 版本在 Delphi 2009 中对我没有影响。
  • @Wodzu:EBX、ESI、EDI、ESP、EBP 需要保留在 Delphi 汇编函数中。原因很简单,这是他们选择的调用约定。此外,方向标志应始终恢复,如果您使用 MMX(但不是 XMM)寄存器,则必须在例程结束时恢复到 FPU 模式(即,使用 EMMS 指令)。数据(通常)通过 EAX、EDX 和 ECX 传递给函数,然后是堆栈。如果你的函数返回一些东西,它会在 AL/AX/EAX/EDX:EAX([u]int64) 或 ST(0) (浮点值)中返回,或者在 @Result 中返回一些其他东西(作为隐藏传递给 proc参数)

标签: delphi optimization assembly x86 basm


【解决方案1】:

一般来说,我个人不会尝试通过使用汇编程序级别的技巧来优化代码,除非您确实需要额外的 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

【讨论】:

  • 提高速度的另一个可能原因:您现在有 8 个独立执行流,它们可以(部分)在现代超标量处理器(尤其是 P4 和向上)。以前,每个位的计算必须在前一个位的计算完成后才能开始。
  • 感谢 Wouter 的回复。正如我在我编辑的问题中所说的 - 我在提出问题之前采用了相同的方法,在我的机器上,结果比我在问题中提供的方法 1 和 2 测量的时间更差。我也不太明白:“它更快,因为操作可以只用寄存器完成,而不需要存储和获取内存。”我认为这不是正确的解释,因为我的方法 4a 和 4b 除了将解压缩的位写入内存之外,也不存储和获取内存。我的组装方法仅在 CPU 寄存器上进行。
  • 原始程序集不使用内存负载。您的版本使用完全相同数量的内存存储。我唯一能想到的是,您在避免管道停顿方面效率更高。
【解决方案2】:

您的 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;

【讨论】:

  • 感谢 GJ 的关注。不幸的是,您的例程是我测试中每个例程中最慢的。在我的答案中查看更新的结果。再次感谢,稍后会分析您的日常工作。
  • 耶......我没有测试它......我忘记了指令“rcl ecx,8”很慢。所以新版本快了3倍左右。
  • 你是如何测量它快 3 倍的?根据我的测试,它快了大约 40%。 +1 对于新方法。
  • 这取决于 CPU,单核 CPU 非常快,但在我的 4 核 CPU 上只有 40% 左右!检查版本 3...
  • xor ecx,ecxmov ecx,ecx快。至少从 P6 的早期版本(大约 1995 年)开始,处理器优化了错误读取依赖性。两者的执行时间相同,但由于异或版本需要的代码缓存空间较小,因此是首选。
【解决方案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 实现在速度方面非常相似,但标有“INLINE”的 PAS 实现是最快的,因为它消除了调用例程所涉及的所有 call/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)

【讨论】:

  • 请注意,我的 Asm 实现对可用指令集 (SSE2) 进行了假设。
  • 感谢 PhiS 为我的问题的第二部分提供的解决方案。还有一个“汇编程序”指令,我已将其添加到您的汇编方法中以查看它是否有帮助。
  • @Wodzu:“assembler”指令在现代 Delphi 版本中没有任何作用。它只是为了向后兼容 Turbo Pascal 代码,因此您需要标记仅汇编程序/函数。
  • 在汇编版本中将 "mov ecx, dword ptr PUint64DecPix" 更改为 "lea ecx, Uint64DecPix" 对我来说仍然更快。
  • 而不是lea,只需使用LUT地址作为加载中的位移:movq xmm0, [8*eax+ Uint64DecPix]。顺便说一句,LEA 比从存储在内存中的指针引导地址更快也就不足为奇了。但是(在 32 位模式下)直接使用地址或mov ecx, OFFSET Uint64DecPix 对您没有任何好处。在 64 位模式下,您可能需要一个相对于 RIP 的 LEA...
【解决方案4】:

编译器在优化小程序方面做得很好。

我会使用查找表来优化您的代码。
由于您解码单个字节 - 256 种不同的状态 - 您可以使用解压缩的值预先计算 256 个数组。

编辑:请注意,奔腾处理器可以并行执行特定指令(Superscalar architecture),这称为配对。

【讨论】:

  • 谢谢尼克。我在download.intel.com/ids/mmx/MMX_Manual_Tech_Developers_Guide.pdf 下的文档中阅读了有关配对的内容,并且方法 4b 的发明受到了该文档的启发;)
  • U/V 管道的配对规则仅适用于实际的 P5 / PMMX CPU,不适用于使用乱序执行的 Pentium II 或更高版本。见agner.org/optimize。针对现代 CPU 进行优化不同于针对 P5 进行优化。 (但不要投反对票,因为 LUT 是个好主意。)
【解决方案5】:

纯软件解决方案

使用来自this question 的优美技术,它再次受到this question 的启发,我们将有一个像这样的出色解决方案,只需一行 代码(不包括声明)

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 位。我是这样做的 in my old answer 并且必须分开一点,然后再组合回来

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

硬件支持

这实际上是bit expand 的一个特例,带有一个常量掩码。在 AVX2 中,英特尔为此目的在 BMI2 指令集中引入了pdep instruction,因此您只需要一条指令即可获得结果。在其他语言中,您可以将其与内部函数 _pext_u64 一起使用。不幸的是,AFAIK Free Pascal 不支持它,您必须直接使用汇编。但是表达式看起来像这样

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

正确性检查

我试过comparing the OP's version with both my versions,直到现在都没有发现任何问题。 compiler output是这样的

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 输出仍然非常不理想,因为编译器不知道用BSWAP 替换对SwapEndian 的调用,并且它不必要地复制数据。为什么mov al, dil; movzx edi, al 而不仅仅是movzx edi, dil?如您所见,C 和 C++ 编译器的输出为 a lot better

How to create a byte out of 8 bool values (and vice versa)?

【讨论】:

  • 非常感谢,这是一个非常有趣的想法。我想知道与其他人相比它将如何表现。当我有时间再次运行我的测试时,我会更新我的结果。
  • @Wodzu 我已经修复了代码。此方法使用 64 位算法,因此在 x86_64 上会快得多。此外,如果您经常使用大量像素执行此操作,请考虑使用 SIMD。明年当 AVX-512 出来时,你可以一次解压 64 像素或至少 8 个 64 位字
  • @Wodzu,当使用常量 (155) 调用不同的 DecodePixels 例程时,编译器可以很聪明。如果它可以预编译结果,它将这样做并用仅分配结果来替换调用。为避免在您的测试比较程序中出现这种情况,请改为使用 155 传递变量。
  • 相关:How to efficiently convert an 8-bit bitmap to array of 0/1 integers with x86 SIMD 有一个没有 BMI2 的答案,它也适用于 16 位 -> 16 字节。但是_mm_set1_epi8() 在没有 AVX2 的情况下花费了几条指令,所以你对乘法技巧更好的评论可能是准确的。 How to perform the inverse of _mm256_movemask_epi8 (VPMOVMSKB)? 有一个 AVX2 答案。
【解决方案6】:

我正要给出与 Wouter van Nifterick 相同的算法。

此外,我将解释依赖链方面的更好性能。 在您提出的每个版本中,当您展开基本循环时,您在两个连续迭代之间保持依赖关系:您的每个 shr al, $01; 都需要计算之前的 al 值。 如果您组织展开的迭代以便它们可以并行执行,它们实际上将在现代处理器上。不要被可以通过寄存器重命名抑制的虚假依赖所迷惑。

有人指出奔腾可以同时执行两条指令。确实如此,但是现代处理器(从 Pentium Pro、PII、...、Core、Core 2 开始)在有机会的情况下(即没有依赖关系)同时执行两条以上的指令在正在执行的指令之间。请注意,在 Wouter van Nifterick 的版本中,每一行都可以独立于其他行执行。

http://www.agner.org/optimize/ 拥有了解现代处理器架构以及如何利用它们所需的所有信息。

【讨论】:

  • 很好的解释和链接! +1。
  • 感谢帕斯卡的回答。但是我认为您的回答仅指我的 Delphi 版本的例程。我提供的汇编例程与 Wouter van Nifterick 例程生成的汇编代码的工作方式非常相似。
  • 不!您的汇编例程 4b 与 5 一点也不相似。4b 对 al 的最终值有很长的依赖链。在执行 4b 期间,无序处理器将大部分时间等待计算 al 的先前值,以便它可以计算 al 的新值。相比之下,在为版本 5 生成的程序集中,没有这么长的依赖链(如果您了解寄存器重命名。为此,请阅读 agner.org/optimize 的材料)。指令可以一次执行多个。
  • j_random_hacker 在他对 Wouter van Nifterick 的回答的评论中说了同样的话,如果你更喜欢他的说法的话。
  • @Wodzu:Pascal 是对的,您的 3、4a 和 4b 版本与 WvN 之间存在很大差异。这对现代 CPU 产生了重大影响。
【解决方案7】:

如果你只支持 80386 及以上,你可以通过这种方式使用 BTcc 和 SETcc 指令集:

BT ax,1
SETC [dx]
inc dx

BT ax,2
SETC [dx]
inc dx

【讨论】:

  • 你也可以只扫描那些被设置的位,使用 BSF 或 BSR。
  • @PhiS:请注意,英特尔自己的优化手册建议避免使用 BSF 和 BSR(以及其他),因为它们是微编码的——本质上,是从 ROM 中的一个微小“程序”在 CPU 上解释的。所以它们有利于 size 优化,但不利于速度。 (当然,唯一真正知道的方法是测试它!)
  • 谢谢 Dmitry 我不知道那些说明。
  • @j_random_hacker: bsf / bsr 在 Intel P6 及更高版本上速度很快;具有 3 个周期延迟的单 uop。 (agner.org/optimize) 您是否正在查看有关针对 P5 Pentium 进行优化的一些古老版本的手册? bsf/bsr 在 AMD 上稍慢(只有 tzcnt/lzcnt 快),所以如果您编写代码以正常工作于 tzcnt 或 bsf,请使用rep bsf,以便支持它的 CPU 将其解码为tzcnt
  • @PeterCordes:在我调查 CPU 指令延迟的时候,最新的芯片——我认为是奔腾 4——有 0.5 周期延迟的“简单”算术指令(ADD、SUB 等) . - 但不是 ADC 或 SBB,我记得我懊恼地发现了),即使是“简单”的转换和旋转,延迟也更高。刚刚检查,BSF 在 P4 上有 4 个周期的延迟。与其他 ALU 指令典型的单周期延迟相比,后续 CPU 上的 3 个周期仍然非常慢。
【解决方案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

【讨论】:

  • 谢谢 Chris,但它会产生不好的结果。
  • 我的错,它产生了很好的效果。感谢您的回答:)我会将其添加到基准测试中。
【解决方案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”的指令在前一条指令完成之前无法开始执行,并且我已经编写了导致这种数据依赖的寄存器。如果没有依赖关系,现代 CPU 能够在最后一条指令完成之前启动一条指令。但是您订购这些操作的方式可以防止这种情况发生。

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

通过这种指令顺序,更少的指令依赖于前一条指令,因此有更多的并行机会。

我不能保证这就是速度差异的原因,但它是一个可能的候选者。不幸的是,很难找到与您正在寻找的答案一样绝对的答案。现代处理器具有分支预测器、多级缓存、硬件预取器以及各种其他复杂性,这些复杂性可能难以隔离性能差异的原因。您能做的最好的事情就是大量阅读、进行实验并熟悉进行良好测量的工具。

【讨论】:

  • 对我来说,这听起来像是一个很好的(并且是适当的试探性:))解释。还可以解释 Wouter van Nifterick 代码的惊人速度。
  • 如果不是一件事,那将是一个很好的答案 - 4b 比 4a 慢。出于与您指出 Josh 相同的原因,我创建了例程 4b。看到基准测试结果我很困惑。
【解决方案10】:

是写入内存(实际上是缓存内存)比使用寄存器慢。

所以,

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

在再次需要bl 寄存器之前,给处理器一些时间将bl 写入内存,同时

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

立即需要bl,因此处理器必须停止并等待内存写入完成。

这让我很惊讶。现代英特尔处理器进行疯狂的流水线和寄存器重命名,所以在我看来,如果有的话,DecodePixels4b 应该更快,因为每条指令的依赖关系更靠后。以上是我能提供的所有解释,除此之外:

x86 是一个糟糕的指令集,英特尔做了惊人的非常先进的 hocus-pocus 来提高它的效率。如果我是你,我会研究别的东西。如今,用于 PC 的 megaMcOptimized 软件的需求非常少。我的友好建议是研究用于移动设备(主要是 ARM)的处理器,因为在移动设备中,处理器速度、功耗和电池寿命问题意味着微优化软件更为重要。并且 ARM 具有针对 x86 的高级指令集。

【讨论】:

  • 我怀疑是不是这个原因;寄存器重命名 (en.wikipedia.org/wiki/Register_renaming) 应该可以防止由于等待寄存器可用而导致的停顿。
  • 谢谢阿特柳斯。我也这么认为,这就是为什么我用 mov 切换了 shr。似乎肯定有其他因素导致 4b 比 4a 慢。
【解决方案11】:

SIMD

如果您将算法扩展到处理数组,那么 SIMD 将成为一种优化选项。这是一个 SIMD 版本,它的时间是优化的 C 等效项的 1/3:

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

【讨论】:

  • 使用pxor xmm2,xmm2 将 xmm reg 归零。 lodsd / movd xmm0, eax 是写movd xmm0, [esi] / add esi, 4 的不好方式。此外,使用movdqa 复制xmm 寄存器,而不是使用movd 两次。实际上,您在前两个步骤中对 eax 的 2 个副本执行相同的随机播放。太疯狂了,复制punpcklwd 结果。或者更好的是,使用pshufd 复制+随机播放。
  • 但是除了糟糕的加载和解包策略之外,这是实现位图 -> 向量的好方法(即pmovmskb的倒数:另见stackoverflow.com/questions/21622212/…)。
【解决方案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

【讨论】:

  • movq xmm0, [Array] / pslld xmm0, 7 / pmovmskb eax, xmm0 为您提供Array 的每个字节的低位。 (移动它们,然后用pmovmskb 提取每个字节的高位)。您也可以pcmpeqb 反对零而不是移位,对零/非零进行打包比较。
【解决方案13】:

如您所见,4a 和 4b 实现的速度差异是由于 CPU 优化(通过并行执行多条指令/流水线指令)。但这个因素并不在操作数上,而是因为操作符本身的性质。

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 在其管道中的等待状态比 4a 多 7 个,因此速度较慢。

Josh 提到存在数据依赖关系,即:

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

【讨论】:

  • immediate-count shr 对任何现代 x86 微架构上的标志都没有输入依赖性。寄存器重命名避免了写后写的风险。请参阅agner.org/optimizethis Q&A,了解有关班次标志处理的更多详细信息。
猜你喜欢
  • 2011-02-14
  • 1970-01-01
  • 2011-04-29
  • 1970-01-01
  • 2015-09-29
  • 2014-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多