在这个热点代码路径中不要使用StringBuilder或foreach。

32

我正在浏览开源项目SignalR的源代码,发现这个差异代码标题为"不要在此热点代码路径中使用StringBuilder或foreach"

-           public static string MakeCursor(IEnumerable<Cursor> cursors)
+           public static string MakeCursor(IList<Cursor> cursors)
            { 
-               var sb = new StringBuilder();
-               bool first = true;
-               foreach (var c in cursors)
+               var result = "";
+               for (int i = 0; i < cursors.Count; i++)
                {
-                   if (!first)
+                   if (i > 0)
                    {
-                       sb.Append('|');
+                       result += '|';
                    }
-                   sb.Append(Escape(c.Key));
-                   sb.Append(',');
-                   sb.Append(c.Id);
-                   first = false;
+                   result += Escape(cursors[i].Key);
+                   result += ',';
+                   result += cursors[i].Id;
                }
-               return sb.ToString();
+               return result;
            }

我知道为什么 foreach 有时会不太高效,而且它被 for 取代了。

然而,我学习并体验到 StringBuilder 是连接字符串的最有效方式。所以我想知道作者为什么决定用标准的连接方法来替换它。

使用 StringBuilder 有什么问题或普遍的缺点吗?


8
你怎么认为foreachfor效率低?这取决于实现方式。此外,在这种情况下,重复字符串连接看起来像是个糟糕的想法,尤其是如果有大量光标。 - Jon Skeet
1
非常同意Jon的观点。如果StringBuilder版本的性能比连接版本差,我会感到惊讶。根据循环迭代次数的不同,如果StringBuilder使用更大的初始容量进行初始化,则可能有助于防止一些“隐藏”的隐式对象初始化。您已经测量/分析了这两个版本以进行比较吗? - Jens H
4
你应该向@dfowler提问 :) - Paolo Moretti
3
我已经修改了代码,效果非常显著,避免了大量无用的枚举器分配(GetEnumerator()),假设这段代码每秒会执行数百万次,那么优化后的性能提升非常可观。请查看最新版本:https://github.com/SignalR/SignalR/blob/master/SignalR/MessageBus/MessageBus.cs#L546 - davidfowl
2
请跟进https://gist.github.com/3703926。 - davidfowl
显示剩余2条评论
5个回答

30

大卫,不使用foreach而仍然使用StringBuilder怎么样? - Anthony Mills
非常感谢您抽出时间建立一个原型来展示这些更改的优点。如果我理解正确,StringBuilder更适合处理长字符串,而不是在长循环中一个接一个地处理短字符串。 - Larry
你不应该链接到主分支,而是链接到项目历史记录中的特定提交 -_- " - Cetin Sert
1
如果您关注性能,我认为正确的方法是只读一次.Count,如果它等于1,则返回cursors[0].Key + ',' + cursors[0].id; 如果等于2,则返回cursors[0].Key + ',' + cursors[0].id + '|' + cursors[1].Key + ',' + cursors[1].id(注意:没有循环,因此只有一个字符串分配)。如果大于两个,我可能会使用StringBuilder,在每个项目后面添加|并在最后减少长度,然后进行最终的ToString();最佳性能可能是分配大小为Count*4的数组,然后... - supercat
使用循环填充数组,包括第一个项目的“|”,将第一个项目替换为空字符串,然后进行单个连接,以“”,cursors [0] .Key,“,”,cursors [0] .id,“|”,cursors [1] .key,“,”,cursors [1] .id等方式填充。 数组分配一次,临时字符串为零。 - supercat
听起来像是一个可供衡量的替代实现。在gist中添加评论并将其加入个人资料! - davidfowl

3

希望更改这个代码的人实际上有测量差异的能力。

  • 每次实例化新的字符串构建器都会产生开销,这也会给内存/垃圾回收带来压力。
  • 编译器可以为简单的连接生成类似于字符串构建器的代码。
  • FOR循环实际上可能更慢,因为它可能需要边界检查,而foreach循环不需要进行边界检查,因为编译器“知道”它们在范围内。

2
通过与 David Fowler 在 JabbR 上了解项目并跟踪其进展,我几乎可以确定这正是促成了这个变化的原因。他花了很多时间使用 CLRProfiler 等工具进行测量,并处理所有正在创建的垃圾。希望他能在主题讨论中提供具体细节。 - Drew Marsh

2
这取决于提供给函数的 Cursor 的数量。
大多数比较两种方法的结果似乎更支持使用 StringBuilder 而不是字符串连接,尤其是在连接4-10个字符串时。如果我没有明确的理由不这样做(例如对于我的问题/应用程序,两种方法的性能比较),我很可能会更喜欢使用 StringBuilder。我会考虑在 StringBuilder 中预先分配一个缓冲区,以避免(许多)重新分配。
有关此主题的一些讨论,请参见String concatenation vs String Builder. PerformanceConcatenating with StringBuilders vs. Strings

1
感谢提供的信息。这个相关的SO问题也非常有趣:https://dev59.com/hnRB5IYBdhLWcg3wr48V - Larry

1

我会把我的钱投资在

           StringBuilder sb = new StringBuilder();
           bool first = true;
           foreach (Cursor c in cursors)
           {
                if (first)
                {
                   first = false;  // only assign it once
                }
                else
                {
                    sb.Append('|');
                }
                sb.Append(Escape(c.Key) + ',' + c.Id);
            }
            return sb.ToString();

但我会把我的钱和dfowler的更新放在一起。请查看他回答中的链接。


1
你要进行多少次字符串拼接?如果很多次,使用 StringBuilder。如果只是几次,那么创建 StringBuilder 的开销将超过任何优势。

请注意,在这种情况下,我们只需要到达两个光标就可以得到7个字符串连接,这相当恶劣。 - Jon Skeet
不一定 - 对于未知大小的数据,你可能需要多次重新分配内部缓冲区。这 可能 会很昂贵。 - Oded
那么正确的方法不是应该看游标的数量,对于很少的游标使用一条路径(String),对于很多的游标使用另一条路径(StringBuilder)吗? - Matt Kerr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接