连接或添加字符串而不是 string.Format 时出现内存碎片?

Memory fragmentation when concatenating or adding strings but not with string.Format?

所以一位大学教授刚刚告诉我,在 C# 中对字符串使用连接(即当您使用加号运算符时)会产生内存碎片,我应该改用 string.Format

现在,我在 stack overflow 中进行了大量搜索,发现 很多 与性能有关的线程,其中连接字符串的优势不言而喻。 (其中一些包括this, this and this

虽然我找不到谈论内存碎片的人。我使用 ILspy 打开了 .NET 的 string.Format,显然它使用 string.Concat 方法相同的 字符串生成器(如果我理解 + 符号被重载到)。事实上:它使用了 string.Concat!

中的代码

我找到了 this article from 2007,但我怀疑它在今天(或永远!)是否准确。显然编译器足够聪明,今天可以避免这种情况,因为我似乎无法重现该问题。使用 string.format 和加号添加字符串最终在内部使用相同的代码。如前所述,string.Format 使用与 string.Concat 相同的代码。

所以现在我开始怀疑他的说法了。是真的吗?

字符串连接的问题在于字符串是不可变的。 string1 + string2 不会将 string2 连接到 string1,它会创建一个全新的字符串。使用 StringBuilder(或 string.Format)没有这个问题。在内部,StringBuilder 持有一个过度分配的 char[]。将某些内容附加到 StringBuilder 不会创建任何新对象,除非它用完 char[] 中的空间(在这种情况下它会过度分配一个新对象)。

我 运行 一个快速基准。我认为这证明了这一点:)

        StringBuilder sb = new StringBuilder();
        string st;
        Stopwatch sw;

        sw = Stopwatch.StartNew();

        for (int i = 0 ; i < 100000 ; i++)
        {
            sb.Append("a");
        }

        st = sb.ToString();

        sw.Stop();
        Debug.WriteLine($"Elapsed: {sw.Elapsed}");

        st = "";

        sw = Stopwatch.StartNew();

        for (int i = 0 ; i < 100000 ; i++)
        {
            st = st + "a";
        }

        sw.Stop();
        Debug.WriteLine($"Elapsed: {sw.Elapsed}");

控制台输出:

已用:00:00:00.0011883 (StringBuilder.Append())

已用:00:00:01.7791839(+ 运算符)

So a professor in university just told me that using concatenation on strings in C# (i.e. when you use the plus sign operator) creates memory fragmentation, and that I should use string.Format instead.

不,您应该做的是 进行用户研究,设置以用户为中心的实际性能指标,并根据这些指标衡量程序的性能。什么时候,并且只有当您发现性能问题时,您才应该使用适当的分析工具来确定性能问题的原因。如果原因是 "memory fragmentation",则通过确定 "fragmentation" 的原因和 尝试实验 来确定哪些技术可以减轻影响来解决这个问题。

"tips and tricks" 无法达到 "avoid string concatenation" 的性能。性能是通过将工程学科应用于现实问题来实现的。

为了解决您更具体的问题:出于性能原因,我从未听过有利于格式化的避免串联的建议。通常给出的建议是避免 迭代连接 以支持 构建器 。迭代连接在时间上是二次方的 space 并产生收集压力。构建器分配不必要的内存,但在典型场景中是线性的。两者都不会造成托管堆的碎片;迭代连接往往会产生 连续 块垃圾。

由于托管堆的不必要碎片而导致性能问题的次数恰好是一次;在 Roslyn 的早期版本中,我们有一个模式,我们会分配一个小的长寿命对象,然后是一个小的短寿命对象,然后是一个小的长寿命对象......连续数十万次,最后得到最大碎片堆导致 用户影响 集合性能问题;我们通过 仔细测量 相关场景中的性能来确定这一点,而不是通过坐在舒适的椅子上对代码进行临时分析。

通常的建议不是避免碎片化,而是避免压力。我们在 Roslyn 的设计过程中发现,一旦解决了上述分配模式问题,压力对 GC 性能的影响远大于碎片。

我对你的建议是要么向你的教授施压,要求他做出解释,要么找一位对绩效指标有更严格方法的教授。

现在,综上所述,您应该 使用格式化而不是串联,但出于性能 的原因。相反,是为了代码可读性、本地化性和类似的风格问题。格式字符串可以做成资源,可以本地化等等。

最后,我提醒您,如果您将字符串放在一起是为了构建类似 SQL 查询或要提供给用户的 HTML 块之类的东西,那么您想要使用 none 这些技术。当您弄错这些字符串构建应用程序时,它们会产生严重的安全影响。使用专为构建这些对象而设计的库和工具,而不是自己动手制作字符串。