C#中的String.Join性能问题

我一直在研究一个提交给我的问题:如何编写一个函数,它接受一个字符串作为输入,并返回一个字符串之间有空格的字符串。 编写该函数是为了在每秒调用数千次时优化性能。

  1. 我知道.net有一个名为String.Join的函数,我可以将空格字符作为分隔符与原始字符串一起传递给它。

  2. 除非使用String.JoinString.Join我可以使用StringBuilder类在每个字符后追加空格。

  3. 完成此任务的另一种方法是声明一个包含2 * n-1个字符的字符数组(您必须为空格添加n-1个字符)。 可以在循环中填充字符数组,然后将其传递给String constructor

我编写了一些.net代码,每个代码使用参数"Hello, World"运行每个算法一百万次"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); } 

更新:我已将项目更改为“发布”模式,并相应地更新了问题中的已用时间。

为什么使用String.Join要比手工工作慢得多?

在这种情况下String.Join较慢的原因是您可以编写一个先前知道IEnumerable的确切性质的算法。

String.Join(string, IEnumerable) (您正在使用的重载)旨在处理任何可任意的可枚举类型,这意味着它无法预先分配到适当的大小。 在这种情况下,它具有纯粹的性能和速度的交易灵活性。

许多框架方法确实处理某些情况,通过检查条件可以加速事情,但这通常只在“特殊情况”变得普遍时才会完成。

在这种情况下,您正在有效地创建一个边缘情况,其中手写例程将更快,但它不是String.Join的常见用例。 在这种情况下,由于您事先确切地知道了所需的内容,因此您可以通过预先分配大小合适的数组并手动构建结果来避免设置灵活所需的所有开销。

您会发现,通常情况下,通常可以编写一个方法来执行某些特定输入数据的框架例程。 这很常见,因为框架例程必须与任何数据集一起使用,这意味着您无法针对特定输入方案进行优化。

您的String.Join示例适用于IEnumerable 。 使用foreach枚举IEnumerable通常比执行for循环慢(这取决于集合类型和其他情况,正如Dave Black在评论中指出的那样)。 即使Join使用StringBuilderStringBuilder的内部缓冲区也必须多次增加,因为要预先知道要追加的项目数。

由于您没有使用Release版本(默认情况下应该检查优化)和/或您正在通过visual studio进行调试,因此JITer将无法进行大量优化。 因此,你只是没有很好地了解每个操作真正需要多长时间。 添加优化后,您可以真实地了解正在发生的事情。

在Visual Studio中调试也很重要。 转到bin / release文件夹,然后双击Visual Studio以外的可执行文件。

在第一个方法中,您正在使用对Enumerable进行操作的String.Join的重载,这需要该方法使用枚举器遍历字符串的字符。 在内部,这使用StringBuilder因为确切的字符数是未知的。

您是否考虑过使用字符串(或字符串数​​组)的String.Join重载? 该实现允许使用固定长度的缓冲区(类似于您的第三种方法)以及一些内部不安全的字符串操作来提高速度。 调用将更改为 – String.Join(" ", s); 如果没有实际的测量工作量,我希望这比你的第三种方法更好或更快。

糟糕的表现不是来自String.Join ,而是来自你处理每个角色的方式。 在这种情况下,由于必须单独处理字符,因此第一种方法将创建更多的中间字符串,第二种方法会受到两个.Append方法调用每个字符的影响。 你的第三种方法不涉及大量的中间字符串或方法调用,这就是你的第三种方法最快的原因。

当你将IEnumerable传递给String.Join ,它不知道需要分配多少内存。 我分配了一块内存,如果它不足则调整它的大小并重复该过程,直到它获得足够的内存来容纳所有字符串。

arrays版本更快,因为我们知道未来分配的内存量。

另请注意,当您运行第一个版本时,GC可能已经发生。