3

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

现在,我在堆栈溢出中进行了很多搜索,并且发现了很多关于性能的线程,其中连接字符串胜出。(其中一些包括这个这个这个

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

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

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

4

2 回答 2

22

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

不,您应该做的是进行用户研究,设置以用户为中心的实际性能指标,并根据这些指标衡量程序的性能。 当且仅当您发现性能问题时,您应该使用适当的分析工具来确定性能问题的原因。如果原因是“内存碎片”,那么通过识别“碎片”的原因并尝试实验来确定哪些技术可以减轻这种影响来解决这个问题。

性能不是通过“避免字符串连接”之类的“技巧和窍门”来实现的。性能是通过将工程学科应用于现实问题来实现的。

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

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

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

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

现在,说了这么多,您应该使用格式而不是连接,但不是出于性能原因。相反,为了代码的可读性、可本地化和类似的风格问题。可以将格式字符串制成资源,可以将其本地化,等等。

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

于 2016-05-10T19:43:00.880 回答
0

字符串连接的问题在于字符串是不可变的。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(+ 运算符)

于 2016-05-10T19:15:09.357 回答