为什么我的string.indexof(char)更快?

时间:2011-08-24 08:08:25

标签: c# string performance

不要问我是如何到达那里的,但是我正在玩一些屏蔽,循环展开等。无论如何,出于兴趣,我正在考虑如何实现indexof方法,而且长话短说,所有除了屏蔽等,这个天真的实现:

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向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)似乎介于两者之间。我对这个巨大的差异感到惊讶。

3 个答案:

答案 0 :(得分:4)

可能性1) 这可能不适用于C#但是当我对x86-64汇编程序进行优化工作时,我很快发现在调优DLL(标记为外部)时调用代码比在我的可执行文件中实现相同的精确函数要慢。最明显的原因是分页和内存,DLL(外部)方法在内存中远离其他正在运行的代码加载,如果以前没有访问它,则需要进行分页。您的基准测试代码应该做您正在进行基准测试的函数的一些预热循环,以确保它们在您计时之前首先在内存中进行分页。

可能性2) 微软往往不会最大限度地优化字符串函数,因此优化本机字符串长度,子字符串,索引等并不是闻所未闻。轶事;在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在我的Intel i7,4GB RAM,Win7 64bit上运行得更快。

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

优化有时非常符合架构。

答案 1 :(得分:2)

如果您确实进行了这样的微测量检查,那么每一位都是重要的。在MS实现中(如您提供的链接中所示),它们还检查s是否为null并抛出NullArgumentException。这也是包括count参数的实现。所以他们另外检查count是否为正确值并抛出ArgumentOutOfRangeException。

我认为,如果你在如此短的时间内经常调用它们,这些使代码更加健壮的小检查足以使它们变得有点慢。

答案 2 :(得分:1)

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

此外,“不安全的代码通过删除数组边界检查来提高性能。”这也可能是原因。

以上来自MSDN

的评论