【问题标题】:Delphi Optimize IndexOf FunctionDelphi 优化 IndexOf 函数
【发布时间】:2017-09-29 08:35:02
【问题描述】:

有人可以帮我加速我的 Delphi 函数吗 在不使用二分查找的情况下在字节数组中查找值。

这个函数我调用了几千次,能不能用汇编优化一下?

非常感谢。

function IndexOf(const List: TArray< Byte >; const Value: byte): integer;
var
  I: integer;
begin
  for I := Low( List ) to High( List ) do begin
   if List[ I ] = Value then
    Exit ( I );
  end;
  Result := -1;
end;

数组的长度约为 15 项。

【问题讨论】:

  • 非常感谢。我纠正了。你知道有什么方法可以改进吗?
  • 为什么你的汇编器比编译器更好?如果您关心性能,请开始关心算法。考虑一个二分搜索。虽然可能列表很小,所以线性搜索可能会更快。
  • 在这篇文章的 cmets 中,有人描述了他们为在 Delphi 中更快地进行循环搜索所做的工作。 compaspascal.blogspot.com/2009/06/delphi-is-fast-very-fast.html 还有其他方法,例如记忆化,但是要知道什么样的事情会起作用,需要更多地了解该函数运行的上下文。我同意@DavidHeffernan 的观点,即汇编程序不是我要找的地方。
  • 您的列表中有多少个元素?如果很小,那么二进制搜索会更慢。但我已经说过了。如果您经常搜索同一个列表,请使用查找表。
  • 再来一次 - 你的名单通常有多长?

标签: arrays function delphi for-loop inline-assembly


【解决方案1】:

好吧,让我们想想。首先,请编辑这一行:

For I := Low( List ) to High( List ) do

(你最后忘记了“做”)。当我们在没有优化的情况下编译它时,这里是这个循环的汇编代码:

Unit1.pas.29: If List [I] = Value then
005C5E7A 8B45FC           mov eax,[ebp-$04]
005C5E7D 8B55F0           mov edx,[ebp-$10]
005C5E80 8A0410           mov al,[eax+edx]
005C5E83 3A45FB           cmp al,[ebp-$05]
005C5E86 7508             jnz $005c5e90
Unit1.pas.30: Exit (I);
005C5E88 8B45F0           mov eax,[ebp-$10]
005C5E8B 8945F4           mov [ebp-$0c],eax
005C5E8E EB0F             jmp $005c5e9f
005C5E90 FF45F0           inc dword ptr [ebp-$10]
Unit1.pas.28: For I := Low (List) to High (List) do
005C5E93 FF4DEC           dec dword ptr [ebp-$14]
005C5E96 75E2             jnz $005c5e7a

这段代码远非最优:局部变量i是真正的局部变量,即:它存储在RAM中,在堆栈中(通过[ebp-$10]地址可以看到,ebp是堆栈指针) .

所以在每次新的迭代中,我们都会看到如何将数组的地址加载到 eax 寄存器中 (mov eax, [ebp-$04]), 然后我们将 i 从堆栈加载到 edx 寄存器 (mov edx, [ebp-$10]), 然后我们至少将 List[i] 加载到 al 寄存器中,它是 eax 的低字节 (mov al, [eax+edx]) 之后将其与再次从内存而不是寄存器中获取的参数“值”进行比较!

这个实现非常慢。

但让我们最后打开优化!它在项目选项 -> 编译 -> 代码生成中完成。让我们看一下新代码:

Unit1.pas.29: If List [I] = Value then
005C5E5A 3A1408           cmp dl,[eax+ecx]
005C5E5D 7504             jnz $005c5e63
Unit1.pas.30: Exit (I);
005C5E5F 8BC1             mov eax,ecx
005C5E61 5E               pop esi
005C5E62 C3               ret 
005C5E63 41               inc ecx
Unit1.pas.28: For I := Low (List) to High (List) do
005C5E64 4E               dec esi
005C5E65 75F3             jnz $005c5e5a

现在只有 4 行代码不断重复。

值存储在 dl 寄存器中(edx 寄存器的低字节), 数组的第 0 个元素的地址存储在 eax 寄存器中, i 存储在 ecx 寄存器中。

因此,'if List[i] = Value' 行仅转换为 1 条装配线:

005C5E5A 3A1408           cmp dl,[eax+ecx]

下一行是条件跳转,之后的3行只执行一次或从不执行(如果条件为真),最后增加i, 循环变量的递减(将其与零进行比较,然后与其他任何东西进行比较更容易)

所以,我们能做的只有带优化器的 Delphi 编译器没有的事情!

如果你的程序允许,你可以尝试反向搜索,从最后一个元素到第一个元素:

For I := High( List ) downto Low( List ) do

这样编译器将很乐意将 i 与零进行比较,以表明我们检查了所有内容(此操作是免费的:当我们减少 i 并得到零时,CPU 零标志打开!)

但在这样的实现中,行为可能会有所不同:如果您有多个条目 = 值,那么您将得到的不是第一个,而是最后一个!

另一个非常简单的事情是将这个 IndexOf 函数声明为内联函数:这样您可能不会在这里调用函数:这段代码将插入到您调用它的每个位置。函数调用是相当慢的事情。

Knuth 中还描述了一些疯狂的方法,如何尽可能快地在简单数组中搜索,他引入了数组的“虚拟”最后一个元素,它等于你的“值”,这样你就不必检查边界(它总是会在超出范围之前找到一些东西),因此循环内只有 1 个条件而不是 2 个。另一种方法是循环的“展开”:您在循环内写下 2 或 3 次或更多次迭代,因此更少每次检查都会跳转,但这有更多的缺点:它仅对相当大的数组有益,而对于具有 1 或 2 个元素的数组可能会更慢。

正如其他人所说:最大的改进是了解您存储的数据类型:它是否经常更改或长时间保持不变,您是否寻找随机元素或有一些“领导者”获得最多注意力。这些元素必须与您放置它们的顺序相同,还是可以根据需要重新排列它们?然后您可以相应地选择数据结构。如果您一直在寻找 1 或 2 个相同的条目并且可以重新排列它们,那么一个简单的“移动到前面”方法会很棒:您不仅返回索引,而且首先将元素移动到第一位,所以它下次会很快找到的。

【讨论】:

  • 非优化代码运行5775个周期*,优化运行4540个周期*,几乎没有惊天动地。 _(* 请参阅我的答案中的时间安排),并且您的答案没有解决使用更好设置(即启用二进制搜索)的大象。
  • @Johan 我对如此小的差异感到惊讶,可能 CPU 本身至少通过将局部变量放入缓存中进行了某种“优化”,因此瓶颈在其他地方。我想知道:CPU 是否预测向下跳 4 条指令,还是希望每次不需要跳转?
  • CPU 是超标量的,因此每个周期可以处理多条指令。这和依赖链决定了吞吐量。
【解决方案2】:

如果您的数组,您可以使用 x86 内置的字符串扫描REP SCAS
它采用微码编码,启动时间适中,但 在 CPU 中进行了高度优化,并且在数据结构足够长(>= 100 字节)的情况下运行速度很快。
事实上,在现代 CPU 上,它经常胜过非常聪明的 RISC 代码。

如果您的数组很短,那么对该例程进行任何优化都无济于事,因为您的问题出在问题中未显示的代码中,因此我无法给您答案。

见:http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Internal_Data_Formats_(Delphi)

function IndexOf({$ifndef RunInSeperateThread} const {$endif} List: TArray<byte>; const Value: byte): integer;
//Lock the array if you run this in a separate thread.
{$ifdef CPUX64}
asm
  //RCX = List
  //DL = byte.
  mov r8,[rcx-8]        //3 - get the length ASAP.
  push rdi              //0 - hidden in mov r,m
  mov eax,edx           //0 - rename
  mov rdi,rcx           //0 - rename
  mov rcx,r8            //0 - rename
  mov rdx,r8            //0 - remember the length
  //8 cycles setup
  repne scasb           //2n - repeat until byte found.
  pop rdi               //1 
  neg rcx               //0
  lea rax,[rdx+rcx]     //1 result = length - bytes left.
end;
{$ENDIF}
{$ifdef CPUX86}
asm
  //EAX = List
  //DL = byte.
  push edi
  mov edi,eax
  mov ecx,[eax-4]        //get the length
  mov eax,edx
  mov edx,ecx            //remember the length
  repne scasb            //repeat until byte found.
  pop edi
  neg ecx
  lea eax,[edx+ecx]      //result = length - bytes left.
end;     

计时
在我的笔记本电脑上使用 1KB 数组,目标字节位于末尾,这给出了以下计时(使用 100.0000 次运行的最低时间)

Code                           | CPU cycles
                               | Len=1024 | Len=16      
-------------------------------+----------+---------
Your code optimizations off    | 5775     | 146
Your code optimizations on     | 4540     |  93
X86 my code                    | 2726     |  60
X64 my code                    | 2733     |  69

加速是好的(ish),但不值得付出努力。

如果您的数组很短,那么此代码将无法帮助您,您将不得不求助于更好的其他选项来优化您的代码。

使用二分搜索可以加快速度
二分搜索是一个 O(log n) 操作,而 O(n) 是简单搜索。
使用相同的数组,这将在 log2(1024) * CPU 周期/搜索 = 10 * 20 +/- 200 个周期中找到您的数据。比我优化的代码快 10 倍以上。

【讨论】:

  • 非常感谢。不幸的是,您的 x86 函数没有返回正确的索引,并且在循环中使用时会在数组长度中产生访问冲突。我的数组很短,包含 15 个项目,我没有隐藏任何功能,即使这是我在屏幕上发布的功能也很简单。我的处理器是英特尔 i3。祝你有美好的一天。
  • @WesleyBobato,已修复
  • 非常感谢您的指正。不幸的是,您的代码比我的代码慢 10 倍,我不明白为什么。如果您想查看我的团队查看器,请拨打 202 092 900,谢谢。
  • @WesleyBobato,我想你误解了 SO 的概念,它不是个人调试服务。上面给出的代码不能比你的代码慢 10 倍,你在别处有错误。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-04-01
  • 2016-03-06
  • 1970-01-01
相关资源
最近更新 更多