我一直在研究一个呈现给我的问题:如何编写一个以字符串作为输入并返回字符之间带空格的字符串的函数。编写该函数是为了在每秒调用数千次时优化性能。
我知道.net有一个名为String.Join
的函数,我可以将空格字符作为分隔符和原始字符串一起传递给它。
除非使用String.Join
,否则我可以使用StringBuilder
类在每个字符后附加空格。
完成此任务的另一种方法是声明一个包含2 * n-1个字符的字符数组(您必须为这些空格添加n-1个字符)。字符数组可以在循环中填充,然后传递给字符串constructor
。
我编写了一些.net代码,使用参数"Hello, World"
运行每个算法一百万次,并测量执行所需的时间。方法(3)比(1)或(2)快得多。
我知道(3)应该非常快,因为它避免了创建任何额外的字符串引用以进行垃圾收集,但在我看来,内置的.net函数(如String.Join
)应该会产生良好的性能。为什么使用String.Join
比手工工作要慢得多?
public static class TestClass
{
// 491 milliseconds for 1 million iterations
public static string Space1(string s)
{
return string.Join(" ", s.AsEnumerable());
}
//190 milliseconds for 1 million iterations
public static string Space2(string s)
{
if (s.Length < 2)
return s;
StringBuilder sb = new StringBuilder();
sb.Append(s[0]);
for (int i = 1; i < s.Length; i++)
{
sb.Append(' ');
sb.Append(s[i]);
}
return sb.ToString();
}
// 50 milliseconds for 1 million iterations
public static string Space3(string s)
{
if (s.Length < 2)
return s;
char[] array = new char[s.Length * 2 - 1];
array[0] = s[0];
for (int i = 1; i < s.Length; i++)
{
array[2*i-1] = ' ';
array[2*i] = s[i];
}
return new string(array);
}
更新:我已将项目更改为“发布”模式并相应更新了问题中的已用时间。
答案 0 :(得分:7)
为什么使用String.Join要比手工工作慢得多?
String.Join
在这种情况下的速度较慢的原因是您可以编写一个先前了解IEnumerable<T>
的确切性质的算法。
String.Join<T>(string, IEnumerable<T>)
(你正在使用的重载)旨在处理任何可任意的可枚举类型,这意味着它无法预先分配到适当的大小。在这种情况下,它具有纯粹的性能和速度的交易灵活性。
许多框架方法确实处理某些情况,通过检查条件可以加速事情,但这通常只在“特殊情况”变得普遍时才会完成。
在这种情况下,您可以有效地创建一个边缘情况,其中手写例程会更快,但它不是String.Join
的常见用例。在这种情况下,由于您事先确切地知道所需的内容,因此您可以通过预先分配大小合适的数组并手动构建结果来避免设置灵活所需的所有开销。
您会发现,通常情况下,通常可能编写一个方法,该方法将执行某些特定输入数据的框架例程。这很常见,因为框架例程必须与任何数据集一起使用,这意味着您无法针对特定输入方案进行优化。
答案 1 :(得分:4)
您的String.Join
示例适用于IEnumerable<char>
。使用IEnumerable<T>
枚举foreach
通常比执行for
循环慢(这取决于集合类型和其他情况,正如Dave Black在评论中指出的那样)。即使Join
使用StringBuilder
,StringBuilder
的内部缓冲区也必须多次增加,因为要追加的项目数量不会提前知道。
答案 2 :(得分:3)
由于您没有使用Release版本(默认情况下应该检查优化)和/或您正在通过visual studio进行调试,因此JITer将无法进行大量优化。因此,你只是没有很好地了解每个操作真正需要多长时间。添加优化后,您可以真实了解正在发生的事情。
在Visual Studio中进行调试也很重要。转到bin / release文件夹,然后双击visual studio以外的可执行文件。
答案 3 :(得分:2)
在第一种方法中,您正在使用在Enumerable上运行的String.Join
的重载,这需要该方法使用枚举器遍历字符串的字符。在内部,它使用StringBuilder
,因为确切的字符数是未知的。
您是否考虑过使用带有字符串(或字符串数组)的String.Join
重载?该实现允许使用固定长度的缓冲区(类似于您的第三种方法)以及一些内部不安全的字符串操作来提高速度。调用将变为 - String.Join(" ", s);
如果没有实际进行测量的工作量,我希望这与第三种方法相同或更快。
答案 4 :(得分:1)
糟糕的表现不是来自String.Join
,而是来自你处理每个角色的方式。在这种情况下,由于必须单独处理字符,因此第一种方法将创建更多的中间字符串,第二种方法会因每个字符的两个.Append
方法调用而受到影响。你的第三种方法不涉及大量的中间字符串或方法调用,这就是你的第三种方法最快的原因。
答案 5 :(得分:0)
当您将IEnumerable
传递给String.Join
时,它不知道需要分配多少内存。我分配了一块内存,如果它不足则调整它的大小并重复该过程,直到它获得足够的内存来容纳所有字符串。
阵列版本更快,因为我们知道未来分配的内存量。
另外请注意,当您运行第一个版本时,GC可能已经发生。