2011-08-24 89 views
16

不要问我如何到达那里,但我正在玩一些掩蔽,循环展开等等。无论如何,出于兴趣,我正在考虑如何实现indexof方法,并且长话短说,所有这些掩盖等,这个幼稚的执行:为什么我的string.indexof(char)更快?

public static unsafe int IndexOf16(string s, int startIndex, char c) { 
      if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
      fixed (char* cs = s) { 
       for (int i = startIndex; i < s.Length; i++) { 
        if ((cs[i]) == c) return i; 
       } 
       return -1; 
      } 
     } 

比string.IndexOf(char)更快。我写了一些简单的测试,它似乎完全匹配输出。 从我的机器的一些样本输出号(它变化到一定程度,当然,但趋势是明确的):

short haystack 500k runs 
1741 ms for IndexOf16 
2737 ms for IndexOf32 
2963 ms for IndexOf64 
2337 ms for string.IndexOf <-- buildin 

longer haystack: 
2888 ms for IndexOf16 
3028 ms for IndexOf32 
2816 ms for IndexOf64 
3353 ms for string.IndexOf <-- buildin 

IndexOfChar被标记的extern,所以你不能反射器吧。但我认为这应该是(本地)执行: http://www.koders.com/cpp/fidAB4768BA4DF45482A7A2AA6F39DE9C272B25B8FE.aspx?s=IndexOfChar#L1000

他们似乎使用相同的朴素的实现。

问题来到我的脑海:

1)我失去了我在执行的东西,解释了为什么它的速度更快?我只能想到扩展字符的支持,但是他们的实现表明他们没有为此做任何特别的事情。

2)我认为大部分低级方法最终都会在手工汇编中实现,看起来并非如此。如果是这样,为什么在本地执行它,而不是像在我的示例实现中一样在C#中实现?

(这里完整的测试(我认为它太长时间贴在这里):http://paste2.org/p/1606018

(不,这不是过早的优化,它不是一个项目,我只是搞乱):-)

更新:Thnx to Oliver提示关于nullcheck和Count参数。我已经添加了这些我IndexOf16Implementation像这样:

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) { 
    if (s == null) throw new ArgumentNullException("s"); 
    if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
    if (count == -1) count = s.Length - startIndex; 
    if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count"); 

    int endIndex = startIndex + count; 
    fixed (char* cs = s) { 
     for (int i = startIndex; i < endIndex; i++) { 
      if ((cs[i]) == c) return i; 
     } 
     return -1; 
    } 
} 

的数字略有变化,但它仍然是相当快显著(32/64结果略):

short haystack 500k runs 
1908 ms for IndexOf16 
2361 ms for string.IndexOf 
longer haystack: 
3061 ms for IndexOf16 
3391 ms for string.IndexOf 

UPDATE2:此版本是更快的,但(特别是对于长草堆情况下):

public static unsafe int IndexOf16(string s, int startIndex, char c, int count = -1) { 
      if (s == null) throw new ArgumentNullException("s"); 
      if (startIndex < 0 || startIndex >= s.Length) throw new ArgumentOutOfRangeException("startIndex"); 
      if (count == -1) count = s.Length - startIndex; 
      if (count < 0 || count > s.Length - startIndex) throw new ArgumentOutOfRangeException("count"); 

      int endIndex = startIndex + count; 
      fixed (char* cs = s) { 
       char* cp = cs + startIndex; 
       for (int i = startIndex; i <= endIndex; i++, cp++) { 
        if (*cp == c) return i; 
       } 
       return -1; 
      } 
     } 

更新4: 基于与LastCoder的讨论,我认为这是依赖于架构。我的Xeon W3550似乎更喜欢这个版本,而他的i7似乎更喜欢buildin版本。我的家用机器(Athlon II)似乎介于两者之间。尽管如此,我感到惊讶。

+0

mask1应该是0xffff而不是0xff – hazzik

+0

@hazzik thnx为提示 – chrisaut

回答

4

可能性1) 这可能不成立(为真)在C#中,但是当我做了优化工作的x86-64汇编我很快就发现,而基准,从一个DLL调用代码(标记为外部)比在我的可执行文件中实现相同的确切函数要慢。最明显的原因是分页和内存,DLL(外部)方法远离其他运行代码加载到内存中,如果以前没有访问它,则需要分页。基准代码应该执行一些热身循环的功能是基准测试,以确保它们在记忆之前首先被分页。

可能性2) 微软倾向于不优化字符串函数,以最充分,所以出优化本地字符串长度,子串,的indexOf等是不是真的前所未闻的。轶事;在x86-64汇编程序中,我能够创建WinXP64的RtlInitUnicodeString函数,几乎在所有实际的用例中运行速度提高了2倍。

可能性3)您的基准测试代码显示您为IndexOf使用2参数重载,此函数可能会调用3参数重载IndexOf(Char,Int32,Int32),这会为每次迭代增加额外的开销。


这可能会更快,因为您可以删除每次迭代的i变量增量。

  char* cp = cs + startIndex; 
      char* cpEnd = cp + endIndex; 
      while (cp <= cpEnd) { 
       if (*cp == c) return cp - cs; 
       cp++; 
      } 

编辑在回答关于(2)你的好奇心,编码早在2005年,并用来修补我的WinXP64机ntdll.dll的。 http://board.flatassembler.net/topic.php?t=4467

RtlInitUnicodeString_Opt: ;;rcx=buff rdx=ucharstr 77bytes 
      xor r9d,r9d 
      test rdx,rdx 
      mov dword[rcx],r9d 
      mov [rcx+8],rdx 
      jz  .end 
      mov r8,rdx 
    .scan: 
      mov eax,dword[rdx] 

      test ax,ax 
      jz  .one 
      add rdx,4 
      shr eax,16 
      test ax,ax 
      jz  .two 
      jmp .scan 
    .two: 
      add rdx,2 
    .one: 
      mov eax,0fffch 
      sub rdx,r8 
      cmp rdx,0fffeh 
      cmovnb rdx,rax 
      mov [ecx],dx 
      add dx,2 
      mov [ecx+2],dx 
      ret 
    .end: 
      retn 

编辑2运行您的示例代码(您最快的版本更新)的string.IndexOf运行速度更快的我的英特尔i7处理器,4GB内存,Win7的64位。

short haystack 500k runs 
2590 ms for IndexOf16 
2287 ms for string.IndexOf 
longer haystack: 
3549 ms for IndexOf16 
2757 ms for string.IndexOf 

优化有时非常依赖架构。

+0

Thnx。 1)代码在基准测试时应该完全(jitted,我猜是分页),因为它首先测试了正确性。但好点。 2)我想看到的代码:-) – chrisaut

+0

3)有助于inbuild一个略有下降,但还不够: 短草垛500K运行 1669毫秒IndexOf16 2319毫秒string.IndexOf 2 2094毫秒string.IndexOf with 3 更长的干草堆: 2289 ms for IndexOf16 3238 ms for string.IndexOf with 2 3153 ms for string.IndexOf with 3 – chrisaut

+0

关于您的版本,我几乎完全一样。 :-)尽管如此,仍然尝试过你(你忘了一个演员,并返回-1)。它比较慢:〜2150 /〜2700短/长测试 不错的建议虽然,thnx – chrisaut

2

如果你真的做了这样的微观测量检查每一个位数。在MS实现中(如您在提供的链接中看到的),他们还检查s是否为null并抛出NullArgumentException。这也是包含count参数的实现。所以他们另外检查是否计数为正确的值并抛出ArgumentOutOfRangeException。

我认为这些小小的检查让代码更健壮,足以让它们在如此短的时间内如此频繁地调用它们的速度变慢一点点。

+0

Thnx为提示和好点。我添加了字符串空检查和计数实现。它不会显着改变数字。看到我更新的问题。 – chrisaut

1

这可能与“固定”语句有关,因为“它将src和dst对象的位置固定在内存中,以便它们不会被垃圾回收移动。”也许加快了方法?

此外,“不安全的代码通过消除数组边界检查来提高性能”。这也可能是为什么。从MSDN采取

上述评论

+0

如果有任何问题需要解决,那么与其本地实施相比,它们可能会变得更慢,因为它们可能有其他方式与gc通信。当然,因为它们是本地的,所以边界检查不应该起作用。 – chrisaut

+0

@Steven,难道答案不是“与使用动态数组分配不同,固定长度的数组将被视为结构的一部分而不仅仅是引用,它还具有作为非托管类型的额外好处,因此一个结构使用它将在默认情况下分配堆栈“。因此,因为非托管代码在堆栈上运行速度更快,那么可能会在堆上运行一些托管代码? – Jethro

+0

我不太清楚你的意思。我没有分配任何数组,我吗?你是什​​么意思“在堆栈上运行”。 AFAIK堆栈vs堆只在mem分配中起作用。我假设这两个实现都没有做任何堆分配。我错过了什么吗? – chrisaut

相关问题