在byte[]中查找byte[]和在string中查找string的速度 - 为什么后者更快?

7
我有一个任务,需要在文件中查找序列。在进行测试应用程序时,我将文件读取为字符串(File.ReadAllText),并使用string.IndexOf查找序列。当我尝试使用字节实现相同的算法(将文件作为字节数组读取,并在字节数组中查找字节数组)时,我注意到在字节数组中查找byte[]比在字符串中查找要慢大约3倍。我确保进行了彻底检查,并且完全相同的代码,一个使用byte[],另一个使用string,执行时间是3倍 - 例如,对于byte,需要16秒,而对于string,大约需要5秒。

对于查找字节数组,我使用了这里描述的方法byte[] array pattern search。对于查找字符串,我使用了string类的内置IndexOf函数。这是我尝试过的一个byte[]的IndexOf实现:

    public int IndexOf(byte[] source, byte[] pattern, int startpos = 0)
    {
        int search_limit = source.Length - pattern.Length;
        for (int i = startpos; i < search_limit; i++)
        {
            if (source[i] == pattern[0])
            {
                bool found = true;
                for (int j = 1; j < pattern.Length; j++)
                {
                    if (source[i + j] != pattern[j])
                    {
                        found = false;
                        break;
                    }
                }
                if (found)
                    return i;
            }
        }
        return -1;
    }

基本上,在字节数组中查找下一个字节序列要比在字符串中查找下一个匹配项慢三倍。我的问题是 - 为什么?是否有人知道.NET如何处理在字符串中查找字符,它进行了哪种优化,使用了哪种算法?是否有比我正在使用的更快的算法?也许有人知道我在这里做错了什么,导致它所需的时间比应该的多?我真的不明白在字符串中查找字符串如何比在字节数组中查找字节数组快三倍...更新:我已经尝试了建议的不安全算法。它如下所示:
public static unsafe long IndexOfFast(byte[] Haystack, byte[] Needle, long startpos = 0)
    {
        long i = startpos;
        fixed (byte* H = Haystack) fixed (byte* N = Needle)
        {
            for (byte* hNext = H + startpos, hEnd = H + Haystack.LongLength; hNext < hEnd; i++, hNext++)
            {

                    bool Found = true;
                    for (byte* hInc = hNext, nInc = N, nEnd = N + Needle.LongLength; Found && nInc < nEnd; Found = *nInc == *hInc, nInc++, hInc++) ;
                    if (Found) return i;

            }
            return -1;
        }
    }
}

奇怪的是,实际上它被证明比原来慢了两倍!我进行了个人调整(在尝试迭代针之前先检查第一个字母),现在它看起来像这样:

public static unsafe long IndexOfFast(byte[] Haystack, byte[] Needle, long startpos = 0)
    {
        long i = startpos;
        fixed (byte* H = Haystack) fixed (byte* N = Needle)
        {
            for (byte* hNext = H + startpos, hEnd = H + Haystack.LongLength; hNext < hEnd; i++, hNext++)
            {
                if (*hNext == *N)
                {
                    bool Found = true;
                    for (byte* hInc = hNext+1, nInc = N+1, nEnd = N + Needle.LongLength; Found && nInc < nEnd; Found = *nInc == *hInc, nInc++, hInc++) ;
                    if (Found) return i;
                }
            }
            return -1;
        }
    }

现在,执行不安全的代码和执行安全的代码所需的时间完全相同。我的问题是 - 有什么想法为什么会这样?因为它是不安全的并且使用指针,与安全代码相比,应该更快才对吗?

我会实现这个算法,用于字节数组的搜索,并进行测试。 - I4V
1
大多数字符串比较函数和 IndexOf 都归结为一个内部 CLR 调用,通常是 InternalFindNLSStringExInternalCompareString 中的一个。本地 CLR 实现可能会更快。 - vcsjones
好的,现在你有了一个指针算术解决方案,如果你想让它运行得更快,你就需要开始考虑实际上进行了哪些机器操作。例如:哪个更快:从32位字中移出一个字节四次,还是制作四个掩码,每个位置上都有一个字节,在将字节数组视为int数组时,检查每个int是否与任何一个掩码匹配,并与掩码ANDed? 后者可能会快几个周期。这就是人们在优化此算法时所做的事情。 - Eric Lippert
1
请记住,处理器喜欢执行与处理器字长相同的操作。任何您可以做的事情,以保持操作在该字长内,都有可能得到回报。 - Eric Lippert
2个回答

10
基本上,在字节数组中查找下一个匹配的字节序列比在字符串中查找下一个匹配的字符序列要慢三倍。我的问题是 - 为什么?
因为字符串搜索算法已经被大量优化;它是用紧凑的非托管代码编写的,不需要检查数组边界。如果您使用相同的方式优化字节搜索算法,它也将同样快速;字符串搜索算法使用了您正在使用的相同的朴素算法。
您的算法很好-这是标准的“朴素”搜索,尽管Kevin声称朴素算法在实践中几乎总是在典型数据上运行最快。通过数组寻找字节非常快,对于现代硬件来说非常快速。这取决于您问题的大小;如果您正在人类基因组的中间寻找长DNA字符串,则Boyer-Moore算法完全值得预处理的开销。如果您正在20KB文件中寻找0xDEADBEEF,那么如果朴素算法被有效实现,您将无法击败它。
为了达到最大速度,您应该在此处关闭安全系统,并使用不安全的指针算术来编写代码。

我觉得你会对我添加的计时数据感兴趣。这肯定验证了朴素搜索对于“典型数据”,即在少量数据中搜索,要好得多。 - Kevin

4
你的字节搜索算法非常低效!
所有其他字符串搜索算法都以基本算法 Boyer-Moore 为比较基准。我打赌.NET字符串搜索使用它或其变体。还有其他算法,但是将Boyer-Moore算法实现到字节上会给您带来更好的性能。 编辑:灵敏的SO!这里是一个简单的C# Boyer-Moore字节数组实现 使用时间数字进行编辑: Eric的评论让我非常感兴趣,因为我一直听说.Net字符串搜索使用Boyer-Moore。我真的很感激有人告诉我不同的情况。经过思考后,这完全是有道理的。我决定对Boyer-Moore和Naive字节搜索进行时序测试,发现对于小针头和小干草堆,Naive搜索比Boyer-Moore快13倍以上。但是,令我惊讶的是,“平衡点”比我预期的要低得多。Boyer-Moore随着模式大小(或我的计时器中的针尺寸)而显著改善,因此您要查找的模式越大,它搜索得越快。对于8个字节的针尖,Naive搜索与Boyer-Moore在500-600个字节的干草堆中搜索时处于并列状态。对于更大的干草堆,特别是具有更大的针头,Boyer-Moore变得更好。对于一个20KB的干草堆和64字节的针头,Boyer-Moore快10倍。

以下是所有测试的完整时间数字,供有兴趣的人参考。

所有测试都是使用上述简单Boyer-Moore和Op的Naive字节搜索算法进行的,执行1M次搜索迭代。

1000000 iterations, haystack size = 20 bytes, needle size = 8 bytes
20ms total : Naive Search
268ms total : Boyer-Moore Search

1000000 iterations, haystack size = 600 bytes, needle size = 8 bytes
608ms total : Naive Search
582ms total : Boyer-Moore Search

1000000 iterations, haystack size = 2000 bytes, needle size = 8 bytes
2011ms total : Naive Search
1339ms total : Boyer-Moore Search

1000000 iterations, haystack size = 2000 bytes, needle size = 32 bytes
1865ms total : Naive Search
563ms total : Boyer-Moore Search

1000000 iterations, haystack size = 2000 bytes, needle size = 64 bytes
1883ms total : Naive Search
466ms total : Boyer-Moore Search

1000000 iterations, haystack size = 20000 bytes, needle size = 8 bytes
18899ms total : Naive Search
10753ms total : Boyer-Moore Search

1000000 iterations, haystack size = 20000 bytes, needle size = 32 bytes
18639ms total : Naive Search
3114ms total : Boyer-Moore Search

1000000 iterations, haystack size = 20000 bytes, needle size = 64 bytes
18866ms total : Naive Search
1807ms total : Boyer-Moore Search

在我提供的链接中,它是最快的之一。您能详细说明我算法哪里出了问题吗? - Istrebitel
你不需要检查每个字节来寻找模式的起始位置。通过对模式字节进行一些预处理,你可以在搜索数组中跳过一些内容。Boyer-Moore算法的效率随着模式长度的增加而提高,因为你可以跳过更多的搜索数组内容。 - Kevin
嗯,我现在明白了。Boyer-Moore是一个古老的算法,难道没有一个针对byte[]的C#的著名实现吗?我的意思是,我可以尝试自己写下来,但不想重复造轮子。 - Istrebitel
3
实际上,这个答案完全错误。我在Microsoft实习时,曾向著名的Tim Paterson问过一个问题:为什么标准库不使用Boyer-Moore或其他非naive算法。Tim明智地回答说,在执行真正的业务线代码中的绝大多数字符串搜索问题中,朴素算法找到答案所需的时间比预处理字符串在Boyer-Moore中花费的时间要少得多。大多数LOB程序员正在地址中查找“伦敦”,而不是解决人类基因组DNA搜索问题。 - Eric Lippert
1
IndexOf 进行一种朴素的逐字符搜索;它只是在紧密优化的 C 代码中执行此操作,而不对每个数组访问进行边界检查。 - Eric Lippert
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接