【问题标题】:Most effective code to search character in string在字符串中搜索字符的最有效代码
【发布时间】:2018-08-20 07:42:03
【问题描述】:

假设我们有一个给定的字符串

DataString DB 'AGIJKSZ', 0FFH ; 

在其中找到J 最省时的程序是什么? 时效性是指最少的时钟滴答声。

这是一个带有这些指令集的 x86 处理器:

MMX, SSE, SSE2, SSE3, SSSE3, SSE4.1, SSE4.2, EM64T, VT-x, AES, AVX, AVX2, FMA3, TSX

假设字符串和搜索的字符都可以更改,但只能通过编辑代码来更改,并且我们总是在寻找单个字符。字符串是 ASCII。字符串结束标记为FF

答案应该只是将EAX 设置为找到1/未找到0

这是我能想到的

FindChar_1 PROC
 MOV ESI, OFFSET DataString ; 
SI
 MOV AH, 'J' ; 
Check_End:
 CMP BYTE PTR [ESI], 0FFH ; 
 JE Not_Find ; 
 CMP AH, [ESI] ; 
'DataString'
 JE Got_Equal ;
 ADD ESI, 1 ; 
 JMP Check_End ;
Got_Equal:
 MOV DL, [ESI] ; 
 JMP Done
Not_Find:
 MOV EAX,0 ; 
 RET ;
Done:
 MOV EAX,1 ; 
 RET ; 
FindChar_1 ENDP

编辑:

现在我意识到我应该提到其他一些事情。我使用的是 masm32,所以我可以使用的指令仅限于非常基本的指令。

【问题讨论】:

  • 在什么 CPU 上? x86 与 SSE2? AVX2?什么微架构?搜索模式是固定常数吗?还是字符串是固定常量(就像您在此处显示的那样)而要查找的字符是可变的?或者都是运行时变量?字符串长度是可变的吗?如果它很长,它是否在缓存中很热? 搜索模式是否总是一个字节? 优化strchrstrstr 的功能非常不同。字符串是 ASCII,还是像 UTF-8 这样的可变宽度编码?
  • tl;dr 这 太宽泛了,甚至不清楚你在问什么问题。我的意思是在这种情况下最有效的是Jpos equ 4,如果模式和字符串都是常量,那么结果就是常量。 (这是速度不依赖于 CPU 的一种情况:P)
  • 好的,所以是带有 AVX2 的 CPU。 Ryzen、挖掘机、Haswell、Broadwell、Skylake 还是 Knight's Landing?不同的微架构会为不同的操作占用不同数量的时钟周期,并且具有不同的能力来乱序执行周围代码的搜索。 (agner.org/optimizestackoverflow.com/tags/x86/info)。周围的代码是否对字符串搜索的延迟或总 uop 吞吐量有瓶颈?即您是否针对延迟或吞吐量进行了优化?总时钟周期不是代码每个部分的时钟总和。
  • 字符串的长度是如何确定的?它是 NULL 终止的吗?它有固定的长度吗?我们是否可以读取字符串的末尾?是否提前知道角色会出现?
  • 您说字符串和搜索模式都可以是构建时常量,但这使问题变得愚蠢:将答案硬编码为JPos equ 4。那么我们可以做出哪些假设呢?字符串对齐到 16 字节还是 32 字节?或者至少,我们可以假设加载向量不会出现段错误吗?你需要什么形式的结果?整数寄存器中的指针?矢量中的位图?一个真/假条件而不需要知道它在哪里?按条件分支?

标签: search assembly optimization string-search


【解决方案1】:

当您需要快速编写代码时,请避免内存访问、跳转和复杂指令。我会扫描字符串两次:一次找到结束标记,然后找到搜索的字符:

FindChar_2 PROC
MOV ESI, OFFSET DataString
XOR EAX,EAX
XOR ECX,ECX
XOR EDX,EDX
NOT EAX ; Let AL=EndOfString marker.
NOT ECX ; Let ECX=Max.integer.
MOV EDI,ESI
CLD
REPNE SCASB ; ECX -= String size (-9 in this example).
NOT ECX ; ECX= String size (8).
MOV AL,'J' ; The searched needle.
MOV EDI,ESI ; Restore the haystack pointer.
REPNE SCASB ; The actual search. 
  ; It returns CF if not found, because the needle is below 0FFH.
CMC ; Invert the logic.
MOV EAX,EDX ; Return false.
ADC EAX,EDX ; Return true if found.
RET
FindChar_2 ENDP

【讨论】:

  • 如果你想让它更快,你应该使用mov eax,-1而不是xor-zero/not
  • 这在任何绝对意义上都远非好,尤其是在具有 MMX 的 CPU 上(更不用说 SSE2 或 AVX2)了。但即使没有,缓慢地扫描字符串两次也是相当糟糕的。 repne scasb 在当前 CPU 上一次只检查一个字节;只有rep movsrep stos 有“快速字符串”优化。根据agner.org/optimizerep scas for n 计数需要 >= 2n 周期,因此对于长度为 n 的字符串的代码将采用 4n 周期的顺序,用于中长字符串。 OP 的代码有很多内存引用,但在 Haswell 上可能每字节运行约 1.5 个周期。
  • 对于短字符串,问题是分支错误预测的成本是否高于任何设置/启动开销rep scas。如果它像rep movs (stackoverflow.com/q/33902068) 之类的,那比分支错过更糟糕。在循环内两次引用相同的内存操作数是愚蠢的,但是您的代码会在字符串上单独循环,即使目标字符早,也会一直循环到结尾!这些内存引用对可以从 L1d 缓存每个时钟执行 2 次加载的 CPU 并没有太大影响。 (即使对于大字符串,两者都太慢而无法成为内存瓶颈。)
  • 无论如何,OP 的代码都是废话,但我认为这更糟。 OP 的代码在 Sandybridge 上可能更像每 3 个周期运行一个,在分支吞吐量上遇到瓶颈。 Haswell 为未采用的分支添加了一个额外的执行端口,因此它可以在每次迭代 1.5 个周期内咀嚼这 5 个融合域微指令。如果像do{}while() 那样编写,每次迭代可以轻松运行 1 个周期,但除了从头重写之外,这个答案几乎没有改进空间。
  • 我的解决方案的总时钟周期会比这个少吗?
猜你喜欢
  • 1970-01-01
  • 2012-12-18
  • 2016-05-06
  • 2012-03-23
  • 2012-01-29
  • 1970-01-01
  • 2016-12-18
  • 2020-12-21
  • 2016-11-30
相关资源
最近更新 更多