C#(.NET 4.0)中的快速字符串后缀检查?

时间:2011-02-08 14:55:15

标签: c# string

在C#中检查字符串后缀的最快方法是什么?

我需要检查特定术语的大型列表中的每个字符串(5000到100000个项目)。保证该术语永远不会嵌入字符串中。换句话说,如果字符串包含该术语,则它将位于字符串的末尾。该字符串也保证长于后缀。文化信息并不重要。

这些是针对100000个字符串执行的不同方法(其中一半具有后缀):

 1.  Substring Comparison - 13.60ms
 2.  String.Contains - 22.33ms
 3.  CompareInfo.IsSuffix - 24.60ms
 4.  String.EndsWith - 29.08ms
 5.  String.LastIndexOf - 30.68ms

这些是平均时间。 [编辑]忘了提到字符串也被放入单独的列表中,但这并不重要。它确实增加了运行时间。

在我的系统子字符串比较中(使用String.Substring方法提取字符串的结尾并将其与后缀术语进行比较)在针对100000字符串进行测试时始终是最快的。使用子字符串比较的问题是垃圾收集可以大大减慢它(比其他方法更多)因为String.Substring创建新的字符串。 .NET 4.0中的效果并不像3.5及更低版本那么糟糕,但它仍然很明显。在我的测试中,String.Substring在12000-13000个字符串集上执行得更慢。这在系统和实现之间显然会有所不同。

[编辑] 基准代码: http://pastebin.com/smEtYNYN

[编辑] FlyingStreudel的代码运行速度很快,但Jon Skeet建议将EndsWith与StringComparison.Ordinal结合使用似乎是最好的选择。

7 个答案:

答案 0 :(得分:18)

如果这是检查100,000个字符串所需的时间,那真的重要吗?

我个人使用string.EndsWith是因为它是最具描述性的:它确切地说明了你要测试的内容。

我有点怀疑它看起来表现最差的事实......如果你可以发布你的基准代码,那将是非常有用的。 (特别是,它实际上不应该像string.Contains那样做很多工作。)

您是否尝试过指定序数匹配?这可能会使它明显加快:

if (x.EndsWith(y, StringComparison.Ordinal))

当然,除非你想要进行序数比较,否则你不应该这样做 - 你期待文化敏感的比赛吗? (开发人员倾向于不考虑这类事情,而且非常坚定地将自己纳入该类别。)

答案 1 :(得分:14)

乔恩是绝对正确的;这可能不是一个苹果对苹果的比较,因为不同的字符串方法对于文化敏感性有不同的默认值。非常肯定你在每个人中都得到了你想要的比较语义。

除了Jon的回答,我还要补充一点,相关的问题不是“哪个最快?”而是“哪个太慢?”您对此代码的性能目标是什么?最慢的方法仍然可以在比电影放映机前进到下一帧更短的时间内找到结果,显然这是人类无法察觉的。如果你的目标是搜索对用户来说是即时的,那么你就完成了;任何这些方法都有效。如果您的目标是搜索时间不到一毫秒,那么这些方法都不起作用;它们都是数量级太慢。 预算是多少?

答案 2 :(得分:6)

我看了一下你的基准代码,坦率地说,看起来很狡猾。

你正在测量各种无关的东西以及你想要测量的东西;您正在测量foreach的成本和添加到列表中,这两个成本可能与您尝试测试的成本具有相同的数量级。

另外,你并没有抛出第一轮;请记住,JIT编译器将通过循环jit编译您在第一次时间内调用的代码,并且它会很热并准备好进入第二时间,所以你的结果会因此而扭曲;你在很多小东西上平均一个非常大的东西。在过去,当我这样做时,我发现了jit时间实际上占据了其他一切时间的情况。这是现实的吗?您是指测量jit时间,还是不应将其视为平均值的一部分?

答案 3 :(得分:4)

我不知道这有多快,但这就是我要做的事情?

static bool HasSuffix(string check, string suffix)
{
    int offset = check.Length - suffix.Length;
    for (int i = 0; i < suffix.Length; i++)
    {
        if (check[offset + i] != suffix[i])
        {
            return false;
        }
    }
    return true;
}

编辑:OOPS x2

编辑:所以我写了自己的小基准......这算了吗?它运行了25次评估100万个字符串的试验,并取得了性能差异的平均值。我运行它的几次,它始终输出CharCompare比一百万条记录快10-40毫秒。所以效率的提升非常不重要(.000000001s / call):)总而言之,我怀疑你实施哪种方法很重要。

class Program
{
    volatile static List<string> strings;
    static double[] results = new double[25];
    static void Main(string[] args)
    {
        strings = new List<string>();
        Random r = new Random();
        for (int rep = 0; rep < 25; rep++)
        {
            Console.WriteLine("Run " + rep);
            strings.Clear();
            for (int i = 0; i < 1000000; i++)
            {
                string temp = "";
                for (int j = 0; j < r.Next(3, 101); j++)
                {
                    temp += Convert.ToChar(
                        Convert.ToInt32(
                        Math.Floor(26 * r.NextDouble() + 65)));
                }
                if (i % 4 == 0)
                {
                    temp += "abc";
                }
                strings.Add(temp);
            }
            OrdinalWorker ow = new OrdinalWorker(strings);
            CharWorker cw = new CharWorker(strings);
            if (rep % 2 == 0)
            {
                cw.Run();
                ow.Run();
            }
            else
            {
                ow.Run();
                cw.Run();                    
            }
            Thread.Sleep(1000);
            results[rep] = ow.finish.Subtract(cw.finish).Milliseconds;
        }
        double tDiff = 0;
        for (int i = 0; i < 25; i++)
        {
            tDiff += results[i];
        }
        double average = tDiff / 25;
        if (average < 0)
        {
            average = average * -1;
            Console.WriteLine("Char compare faster by {0}ms average", 
                average.ToString().Substring(0, 4));
        }
        else
        {
            Console.WriteLine("EndsWith faster by {0}ms average", 
                average.ToString().Substring(0, 4));
        }

    }
}   



class OrdinalWorker
{
    List<string> list;
    int count;
    public Thread t;
    public DateTime finish;
    public OrdinalWorker(List<string> l)
    {
        list = l;
    }

    public void Run()
    {
        t = new Thread(() => {
            string suffix = "abc";
            for (int i = 0; i < list.Count; i++)
            {
                count = (list[i].EndsWith(suffix, StringComparison.Ordinal)) ? 
                    count + 1 : count;
            }
            finish = DateTime.Now;
        });
        t.Start();
    }
}

class CharWorker 
{
    List<string> list;
    int count;
    public Thread t;
    public DateTime finish;
    public CharWorker(List<string> l)
    {
        list = l;
    }

    public void Run()
    {
        t = new Thread(() =>
        {
            string suffix = "abc";
            for (int i = 0; i < list.Count; i++)
            {
                count = (HasSuffix(list[i], suffix)) ? count + 1 : count;
            }
            finish = DateTime.Now;
        });
        t.Start();
    }

    static bool HasSuffix(string check, string suffix)
    {
        int offset = check.Length - suffix.Length;
        for (int i = 0; i < suffix.Length; i++)
        {
            if (check[offset + i] != suffix[i])
            {
                return false;
            }
        }
        return true;
    }
}

答案 4 :(得分:0)

您是否尝试过直接访问? 我的意思是,你可以制作一个循环来观察类似的字符串,它可能比制作一个子字符串并具有相同的行为更快。

int i,j;
foreach(String testing in lists){
   i=0;
   j=0;
   int ok=1;
   while(ok){
      i = testing.lenght - PATTERN.lenght;
      if(i>0 && i<testing.lenght && testing[i] != PATTERN[j])
        ok = 0;
      i++;
      j++;
   }
   if(ok) return testing;
}

此外,如果它是大字符串,你可以尝试使用哈希值。

答案 5 :(得分:0)

我并不自称是这方面的专家,但是我觉得必须至少在某种程度上描述这一点(完全清楚我的虚拟场景与你自己的情况有很大不同),这就是我的想法用:

看来,至少在我看来,EndsWith带头LastIndexOf一直排在第二位,有些时间是:

SubString:    00:00:00.0191877
Contains:     00:00:00.0201980
CompareInfo:  00:00:00.0255181
EndsWith:     00:00:00.0120296
LastIndexOf:  00:00:00.0133181

这些是从处理100,000个字符串中收集到的,其中所需的后缀出现在所有字符串中,所以对我来说简单地回应了Jon的答案(其中的好处是速度和描述性)。以前用于获得这些结果的代码:

class Program
{
    class Profiler
    {
        private Stopwatch Stopwatch = new Stopwatch();

        public TimeSpan Elapsed { get { return Stopwatch.Elapsed; } }

        public void Start()
        {
            Reset();
            Stopwatch.Start();
        }

        public void Stop()
        {            
            Stopwatch.Stop();
        }

        public void Reset()
        {
            Stopwatch.Reset();
        }
    }

    static string suffix = "_sfx";
    static Profiler profiler = new Profiler();
    static List<string> input = new List<string>();
    static List<string> output = new List<string>();

    static void Main(string[] args)
    {
        GenerateSuffixedStrings();

        FindStringsWithSuffix_UsingSubString(input, suffix);
        Console.WriteLine("SubString:    {0}", profiler.Elapsed);

        FindStringsWithSuffix_UsingContains(input, suffix);
        Console.WriteLine("Contains:     {0}", profiler.Elapsed);

        FindStringsWithSuffix_UsingCompareInfo(input, suffix);
        Console.WriteLine("CompareInfo:  {0}", profiler.Elapsed);

        FindStringsWithSuffix_UsingEndsWith(input, suffix);
        Console.WriteLine("EndsWith:     {0}", profiler.Elapsed);

        FindStringsWithSuffix_UsingLastIndexOf(input, suffix);
        Console.WriteLine("LastIndexOf:  {0}", profiler.Elapsed);

        Console.WriteLine();
        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }

    static void GenerateSuffixedStrings()
    {
        for (var i = 0; i < 100000; i++)
        {
            input.Add(Guid.NewGuid().ToString() + suffix);
        }
    }

    static void FindStringsWithSuffix_UsingSubString(IEnumerable<string> strings, string suffix)
    {
        output.Clear();
        profiler.Start();
        foreach (var s in strings)
        {
            if(s.Substring(s.Length - 4) == suffix)
                output.Add(s);
        }
        profiler.Stop();
    }

    static void FindStringsWithSuffix_UsingContains(IEnumerable<string> strings, string suffix)
    {
        output.Clear();
        profiler.Start();
        foreach (var s in strings)
        {
            if (s.Contains(suffix))
                output.Add(s);
        }
        profiler.Stop();
    }

    static void FindStringsWithSuffix_UsingCompareInfo(IEnumerable<string> strings, string suffix)
    {
        var ci = CompareInfo.GetCompareInfo("en-GB");
        output.Clear();
        profiler.Start();
        foreach (var s in strings)
        {
            if (ci.IsSuffix(s, suffix))
                output.Add(s);
        }
        profiler.Stop();
    }

    static void FindStringsWithSuffix_UsingEndsWith(IEnumerable<string> strings, string suffix)
    {
        output.Clear();
        profiler.Start();
        foreach (var s in strings)
        {
            if (s.EndsWith(suffix))
                output.Add(s);
        }
        profiler.Stop();
    }

    static void FindStringsWithSuffix_UsingLastIndexOf(IEnumerable<string> strings, string suffix)
    {
        output.Clear();
        profiler.Start();
        foreach (var s in strings)
        {
            if (s.LastIndexOf(suffix) == s.Length - 4)
                output.Add(s);
        }
        profiler.Stop();
    }
}

修改

如评论所述,我再次尝试了这一点,只有一些字符串应用了后缀,结果如下:

SubString:    00:00:00.0079731
Contains:     00:00:00.0243696
CompareInfo:  00:00:00.0334056
EndsWith:     00:00:00.0196668
LastIndexOf:  00:00:00.0229599

字符串生成器方法更新如下,以生成字符串:

    static void GenerateSuffixedStrings()
    {
        var nxt = false;
        var rnd = new Random();            
        for (var i = 0; i < 100000; i++)
        {
            input.Add(Guid.NewGuid().ToString() + 
                (rnd.Next(0, 2) == 0 ? suffix : string.Empty));
        }
    }

此外,如果字符串的 none 具有后缀:

,则此趋势会继续
SubString:    00:00:00.0055584
Contains:     00:00:00.0187089
CompareInfo:  00:00:00.0228983
EndsWith:     00:00:00.0114227
LastIndexOf:  00:00:00.0199328

然而,当将四分之一的输入分配为后缀(第一季度,然后排序到randomise覆盖范围)时,这种差距会再次缩短:

SubString:    00:00:00.0302997
Contains:     00:00:00.0305685
CompareInfo:  00:00:00.0306335
EndsWith:     00:00:00.0351229
LastIndexOf:  00:00:00.0322899

结论? IMO,并同意Jon,EndsWith似乎还有很长的路要走(基于这种有限的测试,无论如何)。

进一步修改:

为了治愈Jon的好奇心,我在EndsWith上进行了一些测试,有和没有Ordinal字符串比较......

在100,000个字符串上,后缀为四分之一:

EndsWith:            00:00:00.0795617
OrdinalEndsWith:     00:00:00.0240631

在1,000,000个字符串上,其中四分之一为后缀:

EndsWith:            00:00:00.5460591
OrdinalEndsWith:     00:00:00.2807860

10,000,000个字符串,其中四分之一为后缀:

EndsWith:            00:00:07.5889581
OrdinalEndsWith:     00:00:03.3248628

请注意,我只运行了最后一次测试,因为生成字符串证明这台笔记本电脑需要更换

答案 6 :(得分:-1)

这里有很多好消息。我想要注意的是,如果你的后缀很短,那么单独查看最后几个字符可能会更快。我在此处修改了基准代码版本:http://pastebin.com/6nNdbEvW。它给出了这些结果:

  1. 最后一个字符相等:1.52毫秒(50000)
  2. 最后2个字符相等:1.56毫秒(50000)
  3. EndsWith使用StringComparison.Ordinal:3.75 ms(50000)
  4. 包含:11.10毫秒(50000)
  5. LastIndexOf:14.85毫秒(50000)
  6. IsSuffix:11.30 ms(50000)
  7. 子串比较:17.69 ms(50000)