C#中字符串的拼接,编译器是如何处理的?

6
A = string.Concat("abc","def") 

B = "abc" + "def"

A vs. B

最近我很困惑为什么许多人会说肯定 A 比 B 处理速度快得多。但是,事实是他们只是这样说,因为有人这样说或者因为它就是这样。我想我可以从这里听到更好的解释。

编译器如何处理这些字符串?

谢谢!


2
对于这么大的字符串,它不会有影响。 - H H
6个回答

12

我加入C#编译器团队时,第一件事就是重写字符串连接的优化器。好时光。

正如已经提到的那样,常量字符串的字符串拼接在编译时完成。非常量字符串做了一些花式处理:

a + b --> String.Concat(a, b)
a + b + c --> String.Concat(a, b, c)
a + b + c + d --> String.Concat(a, b, c, d)
a + b + c + d + e --> String.Concat(new String[] { a, b, c, d, e })
这些优化的好处是String.Concat方法可以查看所有参数,确定它们长度的总和,然后创建一个大字符串,可以容纳所有结果。
这里有一个有趣的例子。假设你有一个返回字符串的方法M:
s = M() + "";
如果M()返回null,那么结果就是空字符串。 (null + 空字符串 = 空字符串)。如果M不返回null,则空字符串连接不会改变结果。因此,这实际上被优化为根本不需要调用String.Concat!它变成了:
s = M() ?? ""

很整洁,是吧?


5
在C#中,字符串的加法运算符只是语法糖,实际上对应的方法是String.Concat。你可以通过在反编译器中打开输出程序集来验证这一点。
另一个需要注意的事情是,如果你的代码中有字符串字面量(或常量),就像例子中一样,编译器甚至会将其更改为B = "abcdef"
但是,如果你使用String.Concat连接两个字符串字面量或常量,String.Concat仍然会被调用,跳过优化,所以+操作实际上会更快。
因此,总结一下: stringA + stringB 变成了String.Concat(stringA, stringB)
"abc" + "def" 变成了"abcdef"
String.Concat("abc", "def") 保持不变
还有一件我必须尝试的事情:
在C++/CLI中,"abc" + "def" + "ghi实际上被转换为String.Concat(String.Concat("abc", "def"), "ghi")

3
用两个字符串字面值是不行的:B 只会被直接设置为 "abcdef"。 - LukeH

5

阅读此文:微观优化剧院的悲剧(编码恐怖)

该文章与IT技术有关,旨在说明代码的微小优化对效率提升的影响非常有限,反而会导致代码可读性降低,开发周期延长等问题。作者建议开发者应该先着眼于系统架构和整体性能调优,避免过度追求微观优化。

只是一个小提示:我认为很多人过分强调StringBuilder。在.NET中还有StringWriter类,它更容易使用,因为它的公共接口与Console类中大家所熟知的非常相似。 - Al Kepp
根据MSDN的说法,StringWriterStringBuilder的包装器。因此,在优化代码时,如果已经讨论了StringBuilder,就没有必要在上下文中提到StringWriter - Brian

1
如果字符串是字面量,就像你的问题中一样,那么分配给B的字符串的连接将在编译时完成。 你的例子翻译成了:
string a = string.Concat("abc", "def");
string b = "abcdef";

如果字符串不是字面量,则编译器将把 + 运算符转换为 Concat 调用。
因此,这个……
string x = GetStringFromSomewhere();
string y = GetAnotherString();

string a = string.Concat(x, y);
string b = x + y;

...在编译时被翻译成这个:

string x = GetStringFromSomewhere();
string y = GetAnotherString();

string a = string.Concat(x, y);
string b = string.Concat(x, y);

1
在这种情况下,这两个实际上是相同的。编译器将把第二个变体(使用+运算符的那个)转换为对Concat(第一个变体)的调用。
嗯,如果这两个实际上包含了被连接的字符串变量。
这段代码:
B = "abc" + "def";

实际上完全不需要连接,就可以转换成这样:

B = "abcdef";

这是可能的,因为加法的结果可以在编译时计算,所以编译器会执行此操作。

然而,如果您使用类似于以下内容的东西:

A = String.Concat(stringVariable1, stringVariable2);
B = stringVariable1 + stringVariable2;

那么这两个将会生成相同的代码。

然而,我想确切地知道那些“许多人”说了什么,因为我认为它是不同的。

我认为他们说的是字符串拼接很糟糕,应该使用 StringBuilder 或类似的东西。

例如,如果你这样做:

String s = "test";
for (int index = 1; index <= 10000; index++)
    s = s + "test";

然后发生的是,对于每次循环迭代,您将构建一个新字符串,并使旧字符串有资格进行垃圾回收。

此外,每个这样的新字符串都将复制旧字符串的所有内容,这意味着您将移动大量内存。

而以下代码:

StringBuilder sb = new StringBuilder("test");
for (int index = 1; index <= 10000; index++)
    sb.Append("test");

相反地,我们将使用一个比实际需要更大的内部缓冲区,以防您需要将更多文本附加到其中。当该缓冲区变满时,将分配一个更大的新缓冲区,并将旧缓冲区留给垃圾回收。

因此,就内存使用和 CPU 使用而言,后一种变体要好得多。

除此之外,我会尽量避免过于关注“代码变体 X 是否比 Y 更好”,超出您已经有的经验范围。例如,现在我只是因为知道这个案例才使用 StringBuilder,但这并不意味着我编写的所有使用它的代码都实际上需要它。

在您知道自己存在瓶颈之前,请尽量避免花费时间进行微观优化代码。在那时,通常的建议是先测量,再进行裁剪。


1
实际上,B 在编译时就被解析了。你最终会得到 B ="abcdef",而对于 A,连接操作会推迟到执行时刻。

1
此外,当字符串不是面对文字时,使用 + 运算符将被转换为单个调用 string.Concat() - Joey

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