C#扩展方法比链接的Replace慢,除非处于紧密循环中。为什么?

时间:2019-02-25 21:06:03

标签: c# optimization

我有一个扩展方法,可以从字符串(电话号码)中删除某些字符,该方法的执行速度比我认为应该慢得多,而链式替换呼叫的执行速度要慢得多。奇怪的是,如果循环运行约3000次迭代,则它会取代“替换”东西,然后循环更快。低于此值并链接“替换”更快。就像我的代码有固定的开销,而替换则没有。这可能是什么!?

快速浏览。仅测试10个数字时,我的测试大约需要0.3毫秒,而替换测试仅需要0.01毫秒。巨大的差异!但是当运行500万个时,我的大约需要1700毫秒,而更换大约需要2500毫秒。

电话号码只能包含0-9,+,-,(,)

以下是相关代码: 建立测试用例,我在玩testNums。

        int testNums = 5_000_000;
        Console.WriteLine("Building " + testNums + " tests");
        Random rand = new Random();
        string[] tests = new string[testNums];
        char[] letters =
        {
            '0','1','2','3','4','5','6','7','8','9',
            '+','-','(',')'
        };
        for(int t = 0; t < tests.Length; t++)
        {
            int length = rand.Next(5, 20);
            char[] word = new char[length];
            for(int c = 0; c < word.Length; c++)
            {
                word[c] = letters[rand.Next(letters.Length)];
            }
            tests[t] = new string(word);
        }

        Console.WriteLine("Tests built");
        string[] stripped = new string[tests.Length];

使用我的扩展方法:

        Stopwatch stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < stripped.Length; i++)
        {
            stripped[i] = tests[i].CleanNumberString();
        }
        stopwatch.Stop();
        Console.WriteLine("Clean: " + stopwatch.Elapsed.TotalMilliseconds + "ms");

使用链接式替换:

        stripped = new string[tests.Length];
        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < stripped.Length; i++)
        {
            stripped[i] = tests[i].Replace(" ", string.Empty)
                        .Replace("-", string.Empty)
                        .Replace("(", string.Empty)
                        .Replace(")", string.Empty)
                        .Replace("+", string.Empty);
        }
        stopwatch.Stop();
        Console.WriteLine("Replace: " + stopwatch.Elapsed.TotalMilliseconds + "ms");

有问题的扩展方法:

    public static string CleanNumberString(this string s)
    {
        Span<char> letters = stackalloc char[s.Length];
        int count = 0;
        for (int i = 0; i < s.Length; i++)
        {
            if (s[i] >= '0' && s[i] <= '9')
                letters[count++] = s[i];
        }
        return new string(letters.Slice(0, count));
    }

我尝试过的事情:

  • 我已经用另一种方式运行它们了。产生很小的变化,但还不够。
  • 将其设为普通的静态方法,该方法比扩展要慢得多。因为ref参数稍慢一些,而in参数与扩展方法大致相同。
  • 积极内联。没有任何实际的区别。我处于发布模式,因此我怀疑编译器会内联它。无论哪种方式,变化都不大。

我也查看了内存分配,这正是我所期望的。我的代理在每次迭代中仅在托管堆上分配一个字符串(末尾为新字符串),而Replace为每个Replace分配一个新对象。因此,Replace one使用的内存要大得多。但这仍然更快!

它是在调用本地C代码并在其中做一些狡猾的事情吗?是更高的内存使用量触发了GC并使其减慢了速度(仍然仅一次或两次迭代就不会消耗如此快的时间)

有什么想法吗?

(是的,我知道不要费心优化这样的事情,这只是在烦我,因为我不知道为什么要这么做)

3 个答案:

答案 0 :(得分:2)

我又使用了干净方法。有趣的是,它比替换快得多。仅第一次运行较慢。抱歉,我无法解释为什么它第一次变慢,但是我运行了更多的方法,然后才达到预期的效果。

构建100个测试 建立测试 更换:0.0528ms 干净:0.4526ms 干净:0.0413ms 清洁:0.0294ms 更换:0.0679ms 替换:0.0523ms

二手dotnet核心2.1

答案 1 :(得分:2)

做完一些基准测试后,我认为可以肯定地断定您的初始声明是错误的,确切的原因是您在已删除的答案中提到的:方法的加载时间是唯一会误导您的东西。

这是该问题的简化版本的完整基准:

static void Main(string[] args)
{
    // Build string of n consecutive "ab"
    int n = 1000;
    Console.WriteLine("N: " + n);
    char[] c = new char[n];

    for (int i = 0; i < n; i+=2)
        c[i] = 'a';
    for (int i = 1; i < n; i += 2)
        c[i] = 'b';

    string s = new string(c);

    Stopwatch stopwatch;

    // Make sure everything is loaded
    s.CleanNumberString();
    s.Replace("a", "");
    s.UnsafeRemove();

    // Tests to remove all 'a' from the string

    // Unsafe remove
    stopwatch = Stopwatch.StartNew();

    string a1 = s.UnsafeRemove();

    stopwatch.Stop();
    Console.WriteLine("Unsafe remove:\t" + stopwatch.Elapsed.TotalMilliseconds + "ms");

    // Extension method
    stopwatch = Stopwatch.StartNew();

    string a2 = s.CleanNumberString();

    stopwatch.Stop();
    Console.WriteLine("Clean method:\t" + stopwatch.Elapsed.TotalMilliseconds + "ms");

    // String replace
    stopwatch = Stopwatch.StartNew();

    string a3 = s.Replace("a", "");

    stopwatch.Stop();
    Console.WriteLine("String.Replace:\t" + stopwatch.Elapsed.TotalMilliseconds + "ms");

    // Make sure the returned strings are identical
    Console.WriteLine(a1.Equals(a2) && a2.Equals(a3));

    Console.ReadKey();

}

public static string CleanNumberString(this string s)
{
    char[] letters = new char[s.Length];
    int count = 0;
    for (int i = 0; i < s.Length; i++)
        if (s[i] == 'b')
            letters[count++] = 'b';
    return new string(letters.SubArray(0, count));
}

public static T[] SubArray<T>(this T[] data, int index, int length)
{
    T[] result = new T[length];
    Array.Copy(data, index, result, 0, length);
    return result;
}

// Taken from https://stackoverflow.com/a/2183442/6923568
public static unsafe string UnsafeRemove(this string s)
{
    int len = s.Length;
    char* newChars = stackalloc char[len];
    char* currentChar = newChars;

    for (int i = 0; i < len; ++i)
    {
        char c = s[i];
        switch (c)
        {
            case 'a':
                continue;
            default:
                *currentChar++ = c;
                break;
        }
    }
    return new string(newChars, 0, (int)(currentChar - newChars));
}

以不同的n值运行时,很明显,扩展方法(或者至少是我的等效方法)具有使其比String.Replace()更快的逻辑。实际上,无论大小字符串,它的性能都更高:

  

N:100
  不安全删除:0.0024ms
  清洁方法:0,0015ms
  字符串替换:0,0021ms
  是

     

N:100000
  不安全删除:0,3889ms
  清洁方法:0,5308ms
  字符串替换:1,3993ms
  正确

我高度怀疑String.Replace()中字符串的替换(不与去除进行比较)的优化可能是这里的罪魁祸首。我还从this answer中添加了一种方法,可以对字符删除进行另一个比较。那个时候的行为与您的方法类似,但是在n的更高值(在我的测试中为80k +)上会更快。

话虽如此,由于您的问题是基于我们发现错误的假设,因此,如果您需要更多解释相反的原因,那么(例如,“为什么String.Replace()比我的方法要慢”) ,许多关于字符串操作的深入基准测试已经做到了。

答案 2 :(得分:1)

因此,在下面的daehee Kim和Mat的帮助下,我发现这只是第一次迭代,但它适用于整个第一个循环。之后的每个循环都可以。

我使用以下行来强制JIT执行其操作并初始化此方法:     RuntimeHelpers.PrepareMethod(typeof(CleanExtension).GetMethod(“ CleanNumberString”,BindingFlags.Public | BindingFlags.Static).MethodHandle);

我发现JIT通常在这里花费大约2-3ms的时间来完成它的工作(包括大约0.1ms的反射时间)。请注意,您可能不应该这样做,因为您现在也已经获得了Reflection成本,并且无论如何都会在此之后立即调用JIT,但这可能是比较基准的一个好主意。

您知道的越多!

我对5000次迭代,使用随机字符串重复5000次并取平均值的基准是:

  

清洁:0.41078ms

     

替换:1.4974ms