我有一个扩展方法从字符串(电话号码)中删除某些字符,这些字符的执行速度比我认为应该比链接的替换调用慢得多.奇怪的是,如果循环运行大约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)
我尝试过的:
我也看了一下内存分配,就像我期望的那样.我的一个在托管堆上每次迭代只分配一个字符串(最后的新字符串),Replace为每个Replace分配一个新对象.所以Replace 1使用的内存要高得多.但它仍然更快!
它是否调用本机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 /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
TrueN:100000
不安全删除:0,3889ms
清理方法:0,5308ms
String.Replace:1,3993ms
True
我高度怀疑字符串替换的优化(不与removal进行比较)String.Replace()是这里的罪魁祸首。我还从这个答案中添加了一种方法,以对删除字符进行另一种比较。该时间的行为与您的方法类似,但在更高的值(我的测试中为 80k+)上速度更快n。
话虽这么说,由于您的问题是基于我们发现错误的假设,如果您需要更多解释为什么相反的情况是正确的(即“为什么 String.Replace() 比我的方法慢”),有很多关于字符串操作的深入基准已经这样做了。