【问题标题】:Copying of data in a tight loop where the function "Move" can not be used在无法使用“移动”功能的紧密循环中复制数据
【发布时间】:2017-01-10 05:59:54
【问题描述】:

在我的一个软件项目中,我“需要”尽可能快的速度来实现紧密循环,该循环将索引数据从数组复制到同一数组的不同部分。

请考虑这个 freepascal 代码:

TYPE
  tIndex : ARRAY OF BYTE;
  tValues: ARRAY OF CARDINAL;

VAR
  Index : tIndex;
  Values: tValues;

CONST
  AnyCardinalSize = 65535;   

PROCEDURE InitializeArrays;
  BEGIN
    SetLength(Index, AnyCardinalSize);
    SetLength(Values,AnyCardinalSize);
  END;

PROCEDURE MoveData(CONST StartPosition,EndPosition,TargetPosition : CARDINAL);
  VAR {Note: TargetPosition is ALWAYS larger than EndPosition.}
    x, InsertOffset : CARDINAL;
  BEGIN
    InsertOffset := TargetPosition-StartPosition;
    FOR x := StartPosition TO EndPosition DO BEGIN
      Values[x+InsertOffset] := Values[Index[x]];
    END;
  END;

我的例程 MoveData 按预期工作,但我想研究用汇编代码替换它的可能性,以便在可能的情况下获得一点速度。

如果有人有如何用汇编语言完成此操作的示例,我将不胜感激。我在 Commodore 64 上做过一些 ASM,但那是很久以前的事了。

【问题讨论】:

  • 为什么你觉得手卷 asm 会比编译器做得更好?
  • 一个好的编译器完全能够生成代码来高效地读写数据。不,Stack Overflow 并不是要让其他人编写代码,因为他们缺少某些技能。它是供其他人使用的问答资源,而不仅仅是您。一个好的问题将对不是你的未来读者有价值。这不是代码编写服务。
  • 不,我完全不是这么说的。您的问题看起来好像您希望我们为您编写代码。这不是 SO 的意义所在。涵盖在help center 和元数据上。我建议你花一些时间在那里了解更多信息。我想让你解释一下为什么你觉得 asm 会带来好处。您不会这样做,而是说因为您对 asm 一无所知,所以您希望我们为您编写代码。这将如何使任何人受益?不管怎样,如果你不想听我的话,坚持认为这个问题本来就很好,很好。祝你好运。
  • 首先是通过反汇编器分析asm代码:Lazarus IDE Window: Assembler
  • 第二,阅读documentation如何在freepascal中使用程序集。

标签: assembly x86 freepascal


【解决方案1】:

大多数优化来自hoisting 表达式中代码的不变部分,然后执行强度降低以减少变体的数量。在使用汇编程序之前学习优化 Pascal,即使只是因为反汇编的 Pascal 为您提供了进一步优化汇编程序的框架,并且可以在测试汇编程序代码时作为比较。

为了更好地识别常量部分,我们将 n=0..endposition-startposition 转换为基于 0 的 for 循环,然后表达式变为:

FOR n := 0 TO EndPosition-StartPosition  DO 
  BEGIN
    Values[n+InsertOffset+StartPosition ] := Values[Index[n+StartPosition]];
  END;

然后我们从循环不变的部分中提取出常量部分,并引入指针语法。 T 是 values 数组的类型,PT 是指向它的指针。同样,TI 是索引数组的类型,PTI 是指向它的指针。

var StartValue : PT;
    StartIndex : PTI;  

StartValue:=@Values[InsertOffset+StartPosition]
StartIndex:=@Index[StartPosition];

FOR n := 0 TO EndPosition-StartPosition  DO 
    Startvalue[n]:=Values[StartIndex[n]]; // slightly depended on FPC dialect mode

现在,startvalue[n] 计算为 addressof(startvalue[0])+n*sizeof(T);但是,如果我们记住前一个循环中的地址值(在 Startvalue 中),则差异变为 PT:=PT+sizeof(T);

我们将乘法转换为加法,但更重要的是,我们从表达式中删除了一个变量(而不是 startvalue 和 x(或 n),我们现在只需要记住 startvalue)。这称为strength reduction,我们也可以将其应用于索引数组:

FOR n := 0 TO EndPosition-StartPosition  DO 
  begin
    Startvalue^:=Values[StartIndex^];
    inc(StartValue);   // inc increments with element size and we declared as PT, so this means inc(startvalue,sizeof(T));
    inc(StartIndex);
  end;

不幸的是,这并没有完全消除对 values 数组的需要,但是这个循环在循环中使用了三个值(startvalue、startindex 和 values[]),而原来的有很多(index、values、x、startposition ,插入偏移)。此外,两者都有一些东西可以确定迭代的结束。

在我们的例子中,可能是当 startvalue 到达数组的末尾时,但为此我们必须使用一段时间:

 var StartValue,EndValue : PT;
     StartIndex : PTI;  

 StartValue:=@Values[InsertOffset+StartPosition]
 EndValue:=@Values[InsertOffset+EndPosition]
 StartIndex:=@Index[StartPosition];

 while (StartValue<=Endvalue) do
    begin
      Startvalue^:=Values[StartIndex^];
      inc(StartValue);   // inc increments with element size and we declared as PT, so this means inc(pbyte(startvalue),sizeof(T));
      inc(StartIndex);
    end;

理论上编译器可以进行这些优化。 C 编译器通常会在正确指示时执行此操作,但大多数 Pascal 编译器还没有达到那个水平。然而,他们没有理由不能这样做。

启动汇编程序的最佳方法是首先对 Pascal 进行大量优化并尝试理解生成的代码。

我多年来一直在 Delphi 中进行图像分析,而使用汇编程序只是为了手动纠正编译器没有得到它(例如,不提升或选择次优不变量),或者当寄存器分配出错时。

唯一的另一种情况是 SSE 中的整个图像操作,但这并不适用于此。

【讨论】:

  • 这个答案非常有用。它很容易掌握,而且我认为我的 sn-p 代码不能被简化,这里的概念实际上通过使用更多代码来简化数据的移动,从而加快代码速度。极好的!它仍然没有回答我最初的问题,但结合其他人对 Agner Fog 的 asmlib 的链接和如何开始使用 Inline ASM 和 FPC 的链接,我想在我的项目中进行。
  • 您可以使用内联汇编器,但这只会让编译器感到沮丧,因为编译器无法将其数据保存在寄存器中,像 gcc 的寄存器无关汇编器宏这样的概念在FPC 自动取款机请注意,FPC 的动作已经基于像 Agner Fog 的动作这样的概念。但正如 PhiS 所说,第一项工作是重新排列你的数据结构,看看你是否真的可以一次移动多个。例如。如果索引中的大多数条目映射到值中的顺序条目,也许您可​​以创建一些索引来跟踪记录范围。
  • 并开始使用 -al 检查不同优化级别的代码,并查看 FPC RTL(i386/ 和 x86_64/ 目录)中的汇编程序
  • 我将致力于重新排列数据结构,特别是尝试将索引用作字节数组,而不是将索引用作整数数组。至少在移动或移动字节索引的一半时,这会将所需的字节减少 3/4。索引值中的顺序条目是可能的,绝对值得研究!但也有相当多的工作要实施。每次添加都需要几条额外的指令,并且不确定这是否可以证明仅对某些数据进行可能的边际改进。我相信它会给予整体惩罚。
【解决方案2】:

首先,在继续之前,我认为您必须确定以这种方式复制数据确实是瓶颈,并且没有替代策略可以完全消除复制的需要。

如果您确定需要更快的复制例程,值得注意的是,这些优化可能非常棘手,并且取决于硬件以及内存布局/对齐/数据组织和大小。如果没有大量经验或时间,您极不可能提出值得付出努力的卓越解决方案。

但是,可以使用优化的内存复制/移动例程。特别是,您可能想查看 Agner Fog 的 asmlib,其中包含可以从编译代码中调用的优化内存复制/移动例程。

【讨论】:

  • 数据的复制是瓶颈,是我的代码-sn-p 中对 CPU 要求最高的部分。不过,我确实有两个选择。 1. 按索引顺序复制索引数据,或者 2. 使用 Move 按顺序复制索引数据,但是我还必须对每个索引值执行计算,以便将索引与数据重新对齐。如果按索引顺序复制索引数据,那么所需的新索引将只是:0,1,2,3,4,5,6,7,8 等,我可以将其设置在单独的数组中作为常量并简单地使用 Move on :)
  • Agner Fog 在这里并不重要,因为复制的最大块是一个整数。 (由于 a[b[x]] 双重间接表达式。此外,FPC afaik 已经有一个相当不错的移动例程。
  • @MarcovandeVoort - 也许你是对的(我同意你的回答),但我不知道 OP 是否可以采取不同的逻辑,在这种情况下,更少、更大的动作可能会诡计。如果是这样,AF 的图书馆是一个很好的了解资源。
  • 我不知道哪个移动功能最快,但我很欣赏 AF 库的链接。我的主要目标是能够在 FPC(Lazarus) 中内联简单的 ASM,但 AF 的库包含很多我可以用作汇编代码示例的内容,以便执行我想做的事情。我只需要多学习一点:) 在工作和家庭生活之间:)
  • PhiS:我对 AF 的唯一期望是 move() 和 FPC 已经是类似的东西(来自英特尔手册),但他不想使用它 :-) 所以首先在去 AF 之前对 FPC 的 mov 进行基准测试。
猜你喜欢
  • 1970-01-01
  • 2013-12-27
  • 1970-01-01
  • 1970-01-01
  • 2015-12-05
  • 2016-10-09
  • 2011-01-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多