我正在研究memchr
类似功能的表现,并做了一个有趣的观察。
这是check.c
,有3个实现来查找字符串中\n
个字符的偏移量:
#include <stdlib.h>
size_t mem1(const char *s)
{
const char *p = s;
while (1)
{
const char x = *p;
if (x == '\n') return (p - s);
p++;
}
}
size_t mem2(const char *s)
{
const char *p = s;
while (1)
{
const char x = *p;
if (x <= '$' && (x == '\n' || x == '\0')) return (p - s);
p++;
}
}
size_t mem3(const char *s)
{
const char *p = s;
while (1)
{
const char x = *p;
if (x == '\n' || x == '\0') return (p - s);
p++;
}
}
size_t mem4(const char *s)
{
const char *p = s;
while (1)
{
const char x = *p;
if (x <= '$' && (x == '\n')) return (p - s);
p++;
}
}
我在一个字节字符串上运行这些函数,这些字节可由Haskell表达式(concat $ replicate 10000 "abcd") ++ "\n" ++ "hello"
描述 - 即asdf
的10000倍,然后是要查找的换行符,然后是hello
。当然,所有3个实现都返回相同的偏移量:40000,如预期的那样。
有趣的是,当使用gcc -O2
进行编译时,该字符串的运行时间为:
mem1
:16 us mem2
:12 us mem3
:25 us mem4
:16 us (我正在使用criterion库以统计准确度来衡量这些时间。)
我无法向自己解释这一点。为什么mem2
比其他两个快得多?
-
由gcc -S -O2 -o check.asm check.c
生成的程序集:
mem1:
.LFB14:
cmpb $10, (%rdi)
movq %rdi, %rax
je .L9
.L6:
addq $1, %rax
cmpb $10, (%rax)
jne .L6
subq %rdi, %rax
ret
.L9:
xorl %eax, %eax
ret
mem2:
.LFB15:
movq %rdi, %rax
jmp .L13
.L19:
cmpb $10, %dl
je .L14
.L11:
addq $1, %rax
.L13:
movzbl (%rax), %edx
cmpb $36, %dl
jg .L11
testb %dl, %dl
jne .L19
.L14:
subq %rdi, %rax
ret
mem3:
.LFB16:
movzbl (%rdi), %edx
testb %dl, %dl
je .L26
cmpb $10, %dl
movq %rdi, %rax
jne .L27
jmp .L26
.L30:
cmpb $10, %dl
je .L23
.L27:
addq $1, %rax
movzbl (%rax), %edx
testb %dl, %dl
jne .L30
.L23:
subq %rdi, %rax
ret
.L26:
xorl %eax, %eax
ret
mem4:
.LFB17:
cmpb $10, (%rdi)
movq %rdi, %rax
je .L38
.L36:
addq $1, %rax
cmpb $10, (%rax)
jne .L36
subq %rdi, %rax
ret
.L38:
xorl %eax, %eax
ret
非常感谢任何解释!
答案 0 :(得分:3)
我最好的猜测是它与寄存器依赖性有关 - 如果你看一下mem1
中的3指令主循环,你就会对rax
有一个循环依赖。天真地,这意味着每个指令都有等待最后一个指令完成 - 实际上这意味着如果指令没有足够快地退出,那么微体系结构可能会用完寄存器来重命名而只是放弃和拖了一下。
在mem2
中,循环中有4条指令 - 可能还有使用rax
和edx/dl
时更多显式管道的事实 - 是可能会给无序执行硬件带来更轻松的时间,从而最终有效地管理 more 。
我并不声称自己是专家所以可能完全是胡说八道,但根据我所研究的Agner Fog's absolute goldmine of Intel optimisation details,这似乎并非完全不合理的假设。
编辑:出于兴趣,我在我的机器(Core 2 Duo E7500)上测试了mem1
和mem2
,使用-O2 -falign-functions = 64编译为完全相同的汇编代码。使用给定的字符串在循环中调用1,000,000次函数并使用Linux的time
,mem1
得到~19s,mem2
得到约18.8s - 远远低于25%的差异较新的微体系结构。猜猜是时候买i5了......
答案 1 :(得分:2)
您的输入会使mem2
更快。除了'\ n'之外,输入中的每个字母的值都大于'$',因此if
条件从表达式的第一部分(x&lt; ='$')开始是假的,而第二部分是表达式(x =='\ n'|| x =='\ 0')永远不会执行。如果您使用“####”而不是“abcd”,我怀疑执行会变慢。
答案 2 :(得分:1)
使用缓存,mem1()
的测试首当其冲地填充缓存。
首先再次运行mem1()
测试,并使用第二次,因为它反映了其他测试的已启动缓存。自信它会更快,更公平的时间比较。