gpt4 book ai didi

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

转载 作者:太空狗 更新时间:2023-10-29 23:12:12 26 4
gpt4 key购买 nike

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

快看。当只测试10个数字时,我的大约需要0.3ms,而Replace只需要0.01ms。一个巨大的差异!但是当运行 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 参数与扩展方法大致相同。
  • 积极的内联。没有任何真正的区别。我处于 Release模式,所以我怀疑编译器无论如何都会内联它。无论哪种方式,变化不大。

我还查看了内存分配,这是我所期望的。我的每次迭代只在托管堆上分配一个字符串(末尾的新字符串),其中 Replace 为每个 Replace 分配一个新对象。所以替换一个使用的内存要高得多。但它仍然更快!

它是否调用 native C 代码并在那里做一些狡猾的事情?更高的内存使用率是否触发了 GC 并减慢了它的速度(仍然不能解释只有一两次迭代的异常快的时间)

有什么想法吗?

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

最佳答案

在做了一些基准测试之后,我认为可以安全地断言您的初始陈述是错误的,原因与您在删除的答案中提到的确切原因相同:方法的加载时间是唯一误导您的因素。

这是问题简化版的完整基准测试:

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
Unsafe remove: 0,0024ms
Clean method: 0,0015ms
String.Replace: 0,0021ms
True

N: 100000
Unsafe remove: 0,3889ms
Clean method: 0,5308ms
String.Replace: 1,3993ms
True

我高度怀疑 String.Replace() 中字符串的替换优化(不要与删除相比)罪魁祸首在这里。我还添加了一个来自 this answer 的方法对删除字符进行另一个比较。该时间的行为与您的方法类似,但在 n 的较高值(在我的测试中为 80k+)时变得更快。

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

关于C# 扩展方法比链式替换慢,除非在紧密循环中。为什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54874650/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com