【问题标题】:Why is my string.indexof(char) faster?为什么我的 string.indexof(char) 更快?
【发布时间】:2011-08-24 08:08:24
【问题描述】:

不要问我是如何到达那里的,但我正在玩一些掩蔽,循环展开等。无论如何,出于兴趣,我正在考虑如何实现 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 被标记为外部,所以你不能反射它。但是我认为这应该是(本机)实现: http://www.koders.com/cpp/fidAB4768BA4DF45482A7A2AA6F39DE9C272B25B8FE.aspx?s=IndexOfChar#L1000

他们似乎使用相同的幼稚实现。

我想到了一些问题:

1) 我是否在我的实现中遗漏了一些东西来解释为什么它更快?我只能想到扩展字符支持,但它们的实现表明它们也没有为此做任何特别的事情。

2) 我假设许多低级方法最终会在手工汇编器中实现,但事实似乎并非如此。如果是这样,为什么要在本机实现它,而不是像我的示例实现那样仅在 C# 中实现?

(在这里完成测试(我认为这里粘贴太长了):http://paste2.org/p/1606018

(不,这不是过早的优化,它不适合我正在搞砸的项目):-)

更新:感谢 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 似乎更喜欢内置版本。我的家用机器(Athlon II)似乎介于两者之间。不过,我对巨大的差异感到惊讶。

【问题讨论】:

  • mask1 应该是 0xffff 而不是 0xff

标签: c# string performance


【解决方案1】:

可能性 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++;
            }

edit 出于对(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  

edit 2 运行您的示例代码(使用您的最快版本更新),string.IndexOf 在我的 Intel i7、4GB RAM、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

优化有时非常依赖架构。

【讨论】:

  • 谢谢。 1) 在进行基准测试时,代码应该是完全的(jitted 并且我猜是分页的),因为它首先测试了正确性。但好点。 2)我想看看那个代码:-)
  • 3) 对内置有一点帮助,但还不够:对于 IndexOf16,短 haystack 500k 运行 1669 毫秒,对于 string.IndexOf 运行 2319 毫秒,对于 string.IndexOf 使用 2 2094 毫秒,对于 3 更长的干草堆:2289 毫秒IndexOf16 3238 ms for string.IndexOf with 2 3153 ms for string.IndexOf with 3
  • 关于你的版本,我有几乎完全相同的东西。 :-) 尽管如此,还是尝试了你的(你忘记了演员和回报-1)。它更慢:~2150/~2700 for short/long test 不过不错的建议,thnx
  • 将我头脑中的代码写到网络浏览器中是我长期以来的一个坏习惯。你有测试展开你的循环吗?可能是 2 倍(我将进行编辑演示)。
  • 嗯,我有一个 Xeon W3550(也是基于 Nehalem 的),6GB on / x64。奇怪,我会在我家的机器上试试。
【解决方案2】:

如果您真的进行了如此细微的测量,请检查每一位都很重要。在 MS 实现中(如您提供的链接中所示),他们还检查 s 是否为空并抛出 NullArgumentException。这也是包括 count 参数的实现。因此,他们还检查计数是否为正确值并抛出 ArgumentOutOfRangeException。

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

【讨论】:

  • 谢谢提示和好点。我添加了字符串空检查和计数实现。它不会显着改变数字。请参阅我更新的问题。
【解决方案3】:

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

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

以上cmets取自MSDN

【讨论】:

  • 如果有任何修复应该会使其比它们的本机实现更慢,因为它们可能有其他方式与 gc 通信。当然,由于它们是原生的,边界检查不应该发挥作用。
  • @Steven,答案可能不是“与使用动态数组分配不同,固定长度的数组将被视为结构的一部分,而不仅仅是一个引用。它还有一个额外的好处是非托管类型,因此默认情况下使用它的结构将在堆栈上分配。“因此,因为您的非托管代码在堆栈上运行,所以它比一些可能在堆上运行的托管代码更快?
  • 我不太清楚你的意思。我没有分配任何数组,是吗?你是什​​么意思“在堆栈上运行”。 AFAIK 堆栈与堆仅在内存分配中起作用。我假设这两种实现都没有进行任何堆分配。我错过了什么吗?
  • @Steven,看看这个atalasoft.com/cs/blogs/rickm/archive/2008/04/15/… 链接。这篇文章描述了堆栈和堆。 stackoverflow.com/questions/79923/…
  • 嗯,谢谢。我知道堆栈和堆。你能解释一下为什么你认为这很重要(在上下文中)?
猜你喜欢
  • 2016-02-12
  • 2011-01-02
  • 1970-01-01
  • 1970-01-01
  • 2020-03-18
  • 2014-10-24
  • 1970-01-01
  • 2021-02-20
  • 1970-01-01
相关资源
最近更新 更多