【问题标题】:Assembler function on 64-bit platform on DelphiDelphi 64位平台上的汇编函数
【发布时间】:2014-07-23 20:05:32
【问题描述】:

我有以下功能,需要使其兼容64位平台:

procedure ExecuteAsm(Tab, Buf: Pointer; Len: DWORD);
asm
     mov   ebx, Tab
     mov   ecx, Len
     mov   edx, Buf
@1:  mov   al,  [edx]
     xlat
     mov   [edx], al
     inc   edx
     dec   ecx
     jnz @1
end;

Delphi XE5 在带有TabLen 参数的行上引发错误[dcc64 Error] E2107 Operand size mismatch

不幸的是,我对汇编程序的了解不足以自己解决问题。我应该改变什么才能成功编译函数?

【问题讨论】:

  • 你为什么使用汇编程序?而且您没有保存/恢复 ebx(rbx) 注册!
  • Ofc 它将在 64 位上不匹配。而且这种优化可能比从纯 Pascal 源生成的代码还要慢。

标签: delphi assembly 64-bit inline-assembly


【解决方案1】:

为什么要使用汇编程序?

没有充分的理由!

这是将您的 asm 代码直接翻译成 Delphi pascal:

procedure ExecuteAsm(Tab, Buf: PByte; Len: DWORD);
 repeat
   Buf^ := Tab[Buf^];
   inc(Buf);
   dec(Len);
 until Len = 0;
end;

但是正如您现在所看到的,如果值 Len 为 0,那么程序应该会损坏程序内存。

...

这段代码看起来更好,因为while循环测试0值并且从不执​​行循环。

procedure ExecuteAsm(Tab, Buf: PByte; Len: cardinal);
begin
  while Len > 0 do
  begin
    Buf^ := Tab[Buf^];
    inc(Buf);
    dec(Len);
  end;
end;

但是,如果您仍然喜欢汇编程序,则必须保留 ebx/rbx 寄存器之类的...

procedure ExecuteAsm(Tab, Buf: Pointer; Len: DWORD);
asm
    push    ebx   //rbx

//... your code

    pop     ebx   //rbx
end;

编辑:添加 32 位和 64 位测试

因为 HeartWare 没有做 David Heffernan 的作业,所以我做了。 最初的测试做了大卫赫弗南,看看 HeartWares cmets。我只做了一点改动,又添加了两个测试用例。 这个指令很重要:{$O+} //打开编译器优化... :)

{$APPTYPE CONSOLE}

uses
  Diagnostics;

 {$O+} //Turn on compiler optimisation... :)

procedure _asm_GJ(Tab, Buf : PByte; Len : Cardinal);
//    32-bit   eax edx           ecx
//    64-bit   rcx rdx           r8
asm
{$IFDEF CPUX64 }
        test    Len, Len
        jz      @exit
@loop:
        movzx   rax, [Buf]
        mov     al, byte ptr[Tab + rax]
        mov     [Buf],al
        inc     Buf
        dec     Len
        jnz     @loop
{$ELSE }
        test    Len, Len
        jz      @exit
        push    ebx
@loop:
        movzx   ebx, [Buf]
        mov     bl,byte ptr[Tab + ebx]
        mov     [Buf], bl
        inc     Buf
        dec     Len
        jnz     @loop
        pop     ebx
{$ENDIF }
@exit:
end;

procedure _asm_HeartWare(Tab, Buf : PByte; Len : Cardinal);
//  32-bit     EAX EDX           ECX
//  64-bit     RCX RDX           R8
asm
    {$IFDEF CPUX64 }
        XCHG    R8,RCX
        JECXZ   @OUT
        XOR     RAX,RAX
    @LOOP:
        MOV     AL,[RDX]
        MOV     AL,[R8+RAX]
        MOV     [RDX],AL
        INC     RDX
        DEC     ECX
        JNZ     @LOOP
        // LOOP @LOOP
    {$ELSE }
        JECXZ   @OUT
        PUSH    EBX
        XCHG    EAX,EBX
        XOR     EAX,EAX
    @LOOP:
        MOV     AL,[EDX+ECX-1]
        MOV     AL,[EBX+EAX]
        MOV     [EDX+ECX-1],AL
        DEC     ECX
        JNZ     @LOOP
        // LOOP @LOOP
        POP     EBX
    {$ENDIF }
    @OUT:
end;

procedure _pas_normal(Tab, Buf: PByte; Len: Cardinal);
begin
  while Len > 0 do begin
    Buf^ := Tab[Buf^];
    inc(Buf);
    dec(Len);
  end;
end;

procedure _pas_inline(Tab, Buf: PByte; Len: Cardinal); inline;
begin
  while Len > 0 do begin
    Buf^ := Tab[Buf^];
    inc(Buf);
    dec(Len);
  end;
end;

var
  Stopwatch: TStopwatch;
  i: Integer;
  x, y: array [0 .. 1023] of Byte;

procedure refresh;
begin
  for i := low(x) to high(x) do
  begin
    x[i] := i mod 256;
    y[i] := (i + 20) mod 256;
  end;
end;

begin
{$IFDEF CPUX64 }
  Writeln('64 bit mode');
{$ELSE }
  Writeln('32 bit mode');
{$ENDIF }
  refresh;
  Stopwatch := TStopwatch.StartNew;
  for i := 1 to 1000000 do
  begin
    _asm_HeartWare(PByte(@x), PByte(@y), SizeOf(x));
  end;
  Writeln('asm HeartWare : ', Stopwatch.ElapsedMilliseconds, 'ms');

  refresh;
  Stopwatch := TStopwatch.StartNew;
  for i := 1 to 1000000 do
  begin
    _asm_GJ(PByte(@x), PByte(@y), SizeOf(x));
  end;
  Writeln('asm GJ        : ', Stopwatch.ElapsedMilliseconds, 'ms');

  refresh;
  Stopwatch := TStopwatch.StartNew;
  for i := 1 to 1000000 do
  begin
    _pas_normal(PByte(@x), PByte(@y), SizeOf(x));
  end;
  Writeln('pas normal    : ', Stopwatch.ElapsedMilliseconds, 'ms');

  refresh;
  Stopwatch := TStopwatch.StartNew;
  for i := 1 to 1000000 do
  begin
    _pas_inline(PByte(@x), PByte(@y), SizeOf(x));
  end;
  Writeln('pas inline    : ', Stopwatch.ElapsedMilliseconds, 'ms');

  Readln;
end.

结果...

包含...

几乎无话可说!数字说话...

Delphi编译器不错,嗯很好!

我已经测试了另一个 asm 优化程序,因为 HeartWare asm 优化不是真正的优化。

【讨论】:

  • 你的for循环并不比原始代码更安全,因为当Len为0时循环体仍然会执行。而cardinal是无符号的,不能小于0,所以循环将永远运行,因为循环计数器将包装到 4294967295 并继续运行。在这种情况下,while 循环更合适:while Len > 0 do begin ... Dec(Len); end;
  • @Remy Lebeau:在 XE3 下,for 循环应该只执行一次。我同意你的看法,while 循环更好,更正,thanx!
  • 如果性能至关重要,内联 purepascal ExecuteAsm 可能会使例程更快一点。
【解决方案2】:

该汇编代码本质上只是在执行以下操作,这将在 32 位和 64 位中均有效:

procedure ExecuteAsm(Tab, Buf: Pointer; Len: DWORD);
var
  pBuf: PByte;
begin
  pBuf := PByte(Buf);
  repeat
    pBuf^ := PByte(Tab)[pBuf^];
    Inc(pBuf);
    Dec(Len);
  until Len = 0;
end;

那么为什么不直接使用 Delphi 代码,让编译器处理程序集呢?

【讨论】:

  • 或:pBuf^ := PByte(Tab)[pBuf^]; 等 AFAIK,所有具有 64 位编译器的版本也允许 PByte 上的指针数学运算。
【解决方案3】:

注意:请阅读 GJ 接受的答案,因为它包含一个 Pascal 实现,它击败了我的版本中的废话(我似乎通过使用 ABSOLUTE 来克服 GJ 的实现所具有的签名问题,这是原因之一为什么我没有将它用作 Pascal 版本,但即使重新编码以匹配签名并在例程中使用显式类型转换,它仍然比我的 Pascal 版本快得多,并且与优化的汇编器版本相当,因此在我自己的回复和所有其他人中声明,尽可能使用 Pascal 实现,除非它是一个称为 gazillion times 的时间关键型例程,并且实际的基准测试表明 ASM 版本明显更快 -这(在我的辩护中)我的基准确实显示了。

{$IFDEF MSWINDOWS }
PROCEDURE ExecuteAsm(Tab,Buf : POINTER ; Len : DWORD); ASSEMBLER; Register;
  //      32-bit     EAX EDX             ECX
  //      64-bit     RCX RDX             R8
  ASM
    {$IFDEF CPUX64 }
        XCHG    R8,RCX
        JECXZ   @OUT
        XOR     RAX,RAX
    @LOOP:
        MOV     AL,[RDX]
        MOV     AL,[R8+RAX]
        MOV     [RDX],AL
        INC     RDX
        DEC     ECX
        JNZ     @LOOP
        // LOOP @LOOP
    {$ELSE }
        JECXZ   @OUT
        PUSH    EBX
        XCHG    EAX,EBX
        XOR     EAX,EAX
    @LOOP:
        MOV     AL,[EDX+ECX-1]
        MOV     AL,[EBX+EAX]
        MOV     [EDX+ECX-1],AL
        DEC     ECX
        JNZ     @LOOP
        // LOOP @LOOP
        POP     EBX
    {$ENDIF }
    @OUT:
  END;
{$ELSE }
PROCEDURE ExecuteAsm(Tab,Buf : POINTER ; Len : DWORD);
  VAR
    TabP    : PByte ABSOLUTE Tab;
    BufP    : PByte ABSOLUTE Buf;
    I       : Cardinal;

  BEGIN
    FOR I:=1 TO Len DO BEGIN
      BufP^:=TabP[BufP^];
      INC(BufP)
    END
  END;
{$ENDIF }

这应该是所有当前支持的编译器和平台的有效替代。虽然我同意使用纯 Pascal 版本可能会更好,但它确实会导致一些可怕的汇编代码以及大量不必要的寄存器重新加载(至少在 32 位中),因此纯汇编版本肯定更快。

但是,除非您将它运行无数次,否则您可能不会在实际使用中注意到它,而纯 Pascal 例程很可能会充分发挥作用。但是,只有您可以确定是否需要提高速度。

无论如何,这是在 256 字节数组上执行 100.000 次 PROCEDURE 的时间(使用 XE5):

32-bit ASM: 47 ms
64-bit ASM: 47 ms
32-bit PAS: 63 ms
64-bit PAS: 78 ms

以及在 RELEASE 配置中运行 10.000.000 次的时间:

32-bit ASM: 5281 ms
64-bit ASM: 5281 ms
32-bit PAS: 7765 ms
64-bit PAS: 10031 ms

不过,ASM 版本在所有情况下都胜过 Pascal 版本...

手动优化的汇编版本表现更好:

32-bit ASM: 1906 ms
64-bit ASM: 1859 ms
32-bit PAS: 7781 ms
64-bit PAS: 10015 ms

改为 10.000 乘以 25.600 字节:

32-bit ASM: 218 ms
64-bit ASM: 172 ms
32-bit PAS: 734 ms
64-bit PAS: 937 ms

在所有情况下,我的 ASM 例程都能胜过编译器。我根本无法重现您的计时...您使用了什么代码和编译器?

计算时间的实际代码如下(10.000乘以25.600字节):

T:=GetTickCount;
FOR I:=1 TO 10000 DO ExecuteAsm(TAB,BUF,25600);
T:=GetTickCount-T;

【讨论】:

  • 您没有发布基准。您刚刚发布了正在测试的代码。您确实需要发布生成这些时间的代码。因为我无法重现您声称的结果。
  • 好的,我现在明白了,你的Pascal函数不行。您对absolute 的使用似乎是问题所在。这当然是值得学习的有趣消息。尝试使用 GJ 答案中的代码,比如说。
  • GJ 的代码无效 - 它有一个修改过的参数列表。虽然这可能在特定情况下是可以接受的,但它不是用于时序比较的有效例程。无论如何-即使我更改了他的代码以匹配正确的签名,Pascal 版本的性能与 ASM 版本相当(但无法以 60% 的速度重现您的代码,至少与我优化的汇编程序版本相比)。
  • 您应该考虑弃用 absolute 关键字。现在大多数时候,不用它就可以写得更清楚。
  • 如果一定要用原Pointer参数列表就好了。只需在函数内部进行转换。编译器发出的代码是相同的。您的 asm 代码确实比 Pascal 代码慢得多。我不明白你为什么坚持认为答案中的 asm 代码可以跟上编译器从 Pascal 源生成的代码。它不能。
【解决方案4】:

绝对不确定它会正常工作,但它编译成功:

procedure ExecuteAsm(Tab, Buf: Pointer; Len: DWORD);
asm
     mov   rbx, Tab
     mov   ecx, Len
     mov   rdx, Buf
@1:  mov   al,  [rdx]
     xlat
     mov   [rdx], al
     inc   rdx
     dec   ecx
     jnz @1
end;

这是正确的答案吗?

【讨论】:

  • 请注意,如果这确实解决了您的问题,您可能需要添加条件来封装 32 位和 64 位代码
  • FWIW, mov rdx,Buf 是不必要的,因为 RDX 已经包含 Buf。我通常避免使用参数名称并执行:MOV RBX,RCX; MOV ECX,R8D; 等。这表明我不会覆盖包含参数的寄存器,并且它会在不需要指令时告诉我。
  • 事实上,正如其中一个答案所说,RBX 必须通过在开始时将其推入堆栈并在例程结束时弹出它来保留。
  • @PhiS:当然,但是 XLAT 依赖 RBX 作为表库。所以 xlat 必须编码为XOR RAX,RAX; ... MOV AL,BYTE PTR [RDX]; MOV AL,[R9+RAX]; 等。
  • 问“这是正确的答案吗?”意味着您发布的内容不是答案,而是一个问题。请不要在标题为“您的答案”的区域中发布任何不打算成为问题的实际答案的内容。如果你不确定,那就不是。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多