为什么我的更复杂的C循环更快?

时间:2014-01-12 15:52:54

标签: c performance assembly

我正在研究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

非常感谢任何解释!

3 个答案:

答案 0 :(得分:3)

我最好的猜测是它与寄存器依赖性有关 - 如果你看一下mem1中的3指令主循环,你就会对rax有一个循环依赖。天真地,这意味着每个指令都有等待最后一个指令完成 - 实际上这意味着如果指令没有足够快地退出,那么微体系结构可能会用完寄存器来重命名而只是放弃和拖了一下。

mem2中,循环中有4条指令 - 可能还有使用raxedx/dl时更多显式管道的事实 - 是可能会给无序执行硬件带来更轻松的时间,从而最终有效地管理 more

我并不声称自己是专家所以可能完全是胡说八道,但根据我所研究的Agner Fog's absolute goldmine of Intel optimisation details,这似乎并非完全不合理的假设。

编辑:出于兴趣,我在我的机器(Core 2 Duo E7500)上测试了mem1mem2,使用-O2 -falign-functions = 64编译为完全相同的汇编代码。使用给定的字符串在循环中调用1,000,000次函数并使用Linux的timemem1得到~19s,mem2得到约18.8s - 远远低于25%的差异较新的微体系结构。猜猜是时候买i5了......

答案 1 :(得分:2)

您的输入会使mem2更快。除了'\ n'之外,输入中的每个字母的值都大于'$',因此if条件从表达式的第一部分(x&lt; ='$')开始是假的,而第二部分是表达式(x =='\ n'|| x =='\ 0')永远不会执行。如果您使用“####”而不是“abcd”,我怀疑执行会变慢。

答案 2 :(得分:1)

使用缓存,mem1()的测试首当其冲地填充缓存。

首先再次运行mem1()测试,并使用第二次,因为它反映了其他测试的已启动缓存。自信它会更快,更公平的时间比较。