我有一个扩展方法,可以从字符串(电话号码)中删除某些字符,该方法的执行速度比我认为应该慢得多,而链式替换呼叫的执行速度要慢得多。奇怪的是,如果循环运行约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));
}
我尝试过的事情:
我也查看了内存分配,这正是我所期望的。我的代理在每次迭代中仅在托管堆上分配一个字符串(末尾为新字符串),而Replace为每个Replace分配一个新对象。因此,Replace one使用的内存要大得多。但这仍然更快!
它是在调用本地C代码并在其中做一些狡猾的事情吗?是更高的内存使用量触发了GC并使其减慢了速度(仍然仅一次或两次迭代就不会消耗如此快的时间)
有什么想法吗?
(是的,我知道不要费心优化这样的事情,这只是在烦我,因为我不知道为什么要这么做)
答案 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