【问题标题】:Inline asm (32) emulation of move (copy memory) command内联 asm (32) 模拟移动(复制内存)命令
【发布时间】:2016-06-05 20:10:45
【问题描述】:

我有两个动态大小的二维数组(我猜这是正确的措辞)。我将第一个的内容复制到另一个使用:

    dest:=copy(src,0,4*x*y);
    // src,dest:array of array of longint; x,y:longint;
    // setlength(both arrays,x,y);  //x and y are max 15 bit positive!

它有效。但是我无法在 asm.xml 中重现它。我尝试了以下变体无济于事...有人可以启发我...

    MOV ESI,src; MOV EDI,dest; MOV EBX,y; MOV EAX,x; MUL EBX; 
    PUSH DS; POP ES; MOV ECX,EAX; CLD; REP MOVSD;

还尝试了 LEA(没想到会起作用,因为它应该获取指针地址而不是数组地址),没有工作,并尝试了:

    p1:=@src[0,0]; p2:=@dest[0,0]; //being no-type pointers
    MOV ESI,p1; MOV EDI,p2... (the same asm)

请提示?顺便说一句,它是 delphi 6。错误当然是访问冲突。

【问题讨论】:

  • 你为什么要这样做? asm 版本不太可能明显更快。
  • @500-InternalServerError 为什么人们需要用 Why? 来回答问题 :) 如果这是 XY 问题 可能是合理的,但我不认为是。 OP 可能只是好奇,也可能想挑战自己。
  • @MargaretBloom:这就是重点:我们不知道 OP 这样做的动机。知道这一点将使我们能够写出有针对性的答案。 - 我看到你已经回复了,这很好。
  • 查看 system.move 的 fastcode 源代码,请参阅:fastcode.sourceforge.net 和/或获取 fastmove 的源代码:fastcode.sourceforge.net/FastcodeFileDownloads/FastMove303.zip
  • @MargaretBloom “为什么”的问题是有效的。当他们的动机受到质疑时,提问者经常会感到不安。但也经常会发现,这种动机背后存在误解。也许这里不是这样,但是当我们问“为什么”问题时,通常是为了尽可能地提供帮助。

标签: delphi assembly x86 inline-assembly memcpy


【解决方案1】:

动态数组与静态数组不同,尤其是在多维方面。

请参阅this reference 了解内部格式。

关键是,尺寸为XY(按此顺序!)的Array Of Array Of LongInt 是一个指向指向Y LongInts 数组的X 指针数组的指针 .

从您的 cmets 看来,您已经为 Dest 中的所有元素分配了空间,因此我假设您想要进行深度复制。

这里是一个示例程序,为了清楚起见,组装尽可能简单。

Program Test;

Var Src, Dest: Array Of Array Of LongInt;
    X, Y, I, J: Integer;
Begin

   X := 4;
   Y := 2;

   setLength(Src, X, Y);
   setLength(Dest, X, Y);

   For I := 0 To X-1 Do
      For J := 0 To Y-1 Do
         Src[I,J] := I*Y + J;


   {$ASMMODE intel}
   Asm
       cld                         ;Be sure movsd increments the registers
       mov esi, DWORD PTR [Src]    ;Src pointer
       mov edi, DWORD PTR [Dest]   ;Dest pointer

       mov ecx, DWORD PTR [X]      ;Repeat for X times
                                   ;The number of elements in Src

      @_copy:
       push esi                    ;Save these for later
       push edi 
       push ecx 

       mov ecx, DWORD PTR [Y]      ;Repeat for Y times
                                   ;The number of element in a Src[i] array
       mov edi, DWORD PTR [edi]    ;Get the pointer to the Dest[i] array
       mov esi, DWORD PTR [esi]    ;Get the pointer to the Src[i] array
       rep movsd                   ;Copy sub array

       pop ecx                     ;Restore
       pop edi 
       pop esi 

       add esi, 04h                ;Go from Src[i] to Src[i+1]
       add edi, 04h                ;Go from Dest[i] to Dest[i+1]
      loop @_copy

   End;


   For I := 0 To X-1 Do
      Begin
         WriteLn();

         For J := 0 To Y-1 Do
            Begin
               Write(' ');
               Write(Dest[I,J]);
            End;
      End;
End.

注意 1 此源代码旨在使用 freepascal 进行编译。
欢迎捐赠 Spare Time(TM) 用于下载和安装 Delphi

注2此源代码仅用于说明目的,很明显,上面已经说明了,但不知何故不是每个人都明白。
如果 OP 想要一种快速复制数组的方法,他们应该这么说。

注意3我不保存被破坏的寄存器,这是不好的做法,我的坏;我忘了,因为没有子程序,没有优化,编译器也没有理由在两个 fors 之间的寄存器中传递数据。
这留给读者作为练习。

【讨论】:

  • rep 指令对于短数组的性能很差,最好使用特制的循环。 (并且无需推送/弹出 ecx),永远永远不要使用 loop
  • "指向一个指向 Y LongInts 数组的 X 指针数组的指针" 这解决了我的问题,我想,我稍后会检查你的代码,但我相信这个信息应该足够了。当然,我可能会切换到一维数组并计算索引。我非常同意为什么评论,当我访问这个网站时,我经常看到。当然我不需要这个来复制一个块,而是执行更复杂的操作并想学习如何访问数据。
  • 您没有保存寄存器,这将导致 AV,不需要 cld。在德尔福CLD 中总是如此。 {$asmmode} 是一个 freepascal 指令,而不是 Delphi。
  • @miodrag:您应该始终解释您真正想知道的内容,至少作为您实际提出问题的背景。它对潜在的回答者有很大帮助。可能有不同的方法在相反的情况下表现良好,答案可能会花更多时间在适合您情况的方法上。或者回答者可能考虑的任何其他权衡,但您不知道,否则您不会问。当然,“我该怎么做”会让人们想花时间告诉你为什么这是一个糟糕的计划。
  • @Johan,这是一个示例程序。它旨在微不足道且接近 OP 尝试过的内容,以便他们可以检查它并轻松找出数据的布局。感谢您指出 {$asmmode} 指令,我会添加注释。
【解决方案2】:

这真的是一个二重三重问题。

  1. 动态数组的结构是什么。

  2. asm 中的哪些指令会复制数组。

  3. 我正在向 CPU 扔随机汇编程序,为什么它不起作用?

动态数组的结构
见:http://docwiki.embarcadero.com/RADStudio/Seattle/en/Internal_Data_Formats 引用:

动态数组类型

在 32 位平台上,动态数组变量占用 4 个字节的内存(在 64 位上为 8 个字节),其中包含指向动态分配数组的指针。当变量为空(未初始化)或持有零长度数组时,指针为 nil,并且没有动态内存与变量关联。对于非空数组,该变量指向动态分配的内存块,其中包含数组以及 32 位(Win64 上为 64 位)长度指示符和 32 位引用计数。下表显示了动态数组内存块的布局。

动态数组内存布局(32 位和 64 位)

Offset 32-bit    -8       -4       0 
Offset 64-bit   -12       -8       0
contents      refcount  count    start of data   

所以动态数组变量是指向上述结构中间的指针。

如何在 asm 中访问它

假设数组包含 TMyRec 类型的记录

您需要为外部数组中的每个内部数组运行此代码以执行深层复制。我把这个作为练习留给读者。 (你可以用帕斯卡做另一部分)。

type
  TDynArr: array of TMyRec;

procedure SlowButBasicMove(const Source: TDynArr; var dest);
asm
  //insert register pushes, see below.
  mov esi,Source          //esi = pointer to source data
  mov edi,Dest            //edi = pointer to dest
  sub esi,8               
  mov ebx,[esi]           //ebx = refcount (just in case)
  mov ecx,[esi+4]         //ecx = element count
  mov edx,SizeOf(TMyRec)  //anywhere from 1 to zillions
  mul ecx,edx             //==ecx=number of bytes in array.
  //// now we can start moving 
  xor ebx,ebx             //ebx =0
  add eax,8               //eax = @data
  @loop:
  mov eax,[esi+ebx]       //Get data from source
  mov [edi+ebx],esi       //copy it to dest
  add ebx,4               //4 bytes at a time
  cmp ebx,ecx             //is ebx> number of bytes?
  jle loop 
  //Done copying.
  //insert register pops, see below
end;

复制就完成了,但是为了不让系统崩溃,您需要保存和恢复非易失性寄存器(除 EAX、ECX、EDX 之外的所有寄存器),请参阅:http://docwiki.embarcadero.com/RADStudio/Seattle/en/Program_Control

push ebx
push esi
push edi
--- insert code shown above
//restore non-volatile registers
pop edi
pop esi
pop ebx  //note the restoring must happen in the reverse order of the push.

如果您完全不熟悉 asm,请参阅 Jeff Dunteman 的书 assembly step by step

如果出现以下情况,您将违反访问权限:

  • 您尝试从错误的地址读取。
  • 您尝试写错地址。
  • 您读到了数组的末尾。
  • 您在使用 GetMem 或其他任何方式之前写入尚未声明的内存。
  • 如果您写入超出缓冲区的末尾。
  • 如果您不恢复所有非易失性寄存器

请记住,您直接与 CPU 打交道。 Delphi 不会以任何方式帮助您。

真正快速的代码将使用某种形式的 SSE 在展开的循环中为每条指令移动 16 个字节,有关优化汇编器的示例,请参见上述快速代码。

随机汇编器
在汇编程序中,您需要确切地知道您要做什么、CPU 是如何以及做什么的。
设置断点并运行您的代码。按 ctrl + alt + C 并查看 CPU 调试窗口。
这将允许您查看 Delphi 生成的代码。
您可以单步执行代码以查看 CPU 的功能。
见:http://www.plantation-productions.com/Webster/index.html
更多阅读。

【讨论】:

  • 在英特尔 IvB 及更高版本上,rep movsb 实际上对于对齐的输入非常快,并且可以与 128B 或更大的块 IIRC 的 SSE 循环很好地竞争。在早期的 CPU 上,rep movsq 然后清理会更好。 (这就是 Linux 的 memcpy 所做的,在 CPU 上 rep movs 完全值得使用(我认为不是 AMD)。)@mio:还可以查看 agner.org/optimize 和 x86 标记 wiki 以获取其他链接。
  • @PeterCordes,rep mov 从一个处理器到另一个处理器的性能变化很大,这在我的书中是不可行的。我不喜欢仅针对最近的英特尔进行优化。手工制作的循环(这里需要一些额外的优化,当然)总是表现良好。
  • 是的,这是一个好方法。除了非常大的 memcpy(英特尔上的 rep movs 会在存储未在缓存中丢失时避免 RFO)之外,并没有失去太多。只有在使用运行时 CPU 分派时,才能接受一个 CPU 上好但另一个 CPU 上不好的东西。对于到处使用的 memcpy 之类的东西,您需要避免在每个调用站点进行比较和分支或间接调用的东西(例如动态链接器技巧,或 JIT 样式覆盖 memcpy 符号指向的字节)。这通常是不值得的。
  • 我感谢您的详细回答,尽管一个足够的答案是:“您忽略了数组数组与二维数组不同的事实”。关于随机汇编器,这是一个非常复杂的问题。大多数人一生都没有意识到几乎任何事情,所以一个小的随机汇编器不会像你说的那么糟糕。
  • mul ecx,edx 看起来像是 imul ecx,edx 的拼写错误(除非 Delphi 的汇编程序实际上有别名/伪指令)。
猜你喜欢
  • 2021-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-04
  • 1970-01-01
  • 2022-09-27
  • 1970-01-01
相关资源
最近更新 更多