C#扩展方法比链接更慢替换,除非在紧密循环中.为什么?

cra*_*aig 5 c# optimization

我有一个扩展方法从字符串(电话号码)中删除某些字符,这些字符的执行速度比我认为应该比链接的替换调用慢得多.奇怪的是,如果循环运行大约3000次迭代,它会在循环中超过替换物,之后它会更快.低于此并且链接替换更快.这就像我的代码有一个固定的开销,而Replace没有.这可能是什么!?

快速浏览.当仅测试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];
Run Code Online (Sandbox Code Playgroud)

使用我的扩展方法:

        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");
Run Code Online (Sandbox Code Playgroud)

使用链式替换:

        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");
Run Code Online (Sandbox Code Playgroud)

有问题的扩展方法:

    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));
    }
Run Code Online (Sandbox Code Playgroud)

我尝试过的:

  • 我以另一种方式运行它们.虽然微不足道,但还不够.
  • 使它成为普通的静态方法,明显慢于扩展.由于ref参数略慢,并且参数与扩展方法大致相同.
  • 积极的内联.没有任何真正的区别.我处于发布模式,所以我怀疑编译器无论如何都要进行内联.无论哪种方式,没有太大的变化.

我也看了一下内存分配,就像我期望的那样.我的一个在托管堆上每次迭代只分配一个字符串(最后的新字符串),Replace为每个Replace分配一个新对象.所以Replace 1使用的内存要高得多.但它仍然更快!

它是否调用本机C代码并在那里做一些狡猾的事情?较高的内存使用量是否会触发GC并降低其速度(仍然不能仅在一两次迭代中解释疯狂的快速时间)

有任何想法吗?

(是的,我知道不要为了这样的事情而烦恼,这只会让我烦恼,因为我不知道为什么会这样做)

Mat*_*Mat 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 /sf/answers/152840971/
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));
}
Run Code Online (Sandbox Code Playgroud)

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

N:100
不安全删除:0,0024ms
清洁方法:0,0015ms
String.Replace:0,0021ms
True

N:100000
不安全删除:0,3889ms
清理方法:0,5308ms
String.Replace:1,3993ms
True

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

话虽这么说,由于您的问题是基于我们发现错误的假设,如果您需要更多解释为什么相反的情况是正确的(即“为什么 String.Replace() 比我的方法慢”),有很多关于字符串操作的深入基准已经这样做了。