连接或添加字符串时的内存碎片,但不是string.Format?

时间:2016-05-10 18:52:15

标签: c# .net string string-concatenation memory-fragmentation

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

现在,我已经在堆栈溢出中搜索了很多,我发现很多关于性能的线程,这些串联串联赢了。 (其中一些包括thisthisthis

我找不到谈论内存碎片的人。我使用ILspy打开.NET的string.Format,显然它使用string.Concat方法相同的字符串构建器(如果我理解+符号是什么的话超载到)。实际上:它使用string.Concat中的代码!

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

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

2 个答案:

答案 0 :(得分:21)

  

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

不,您应该做的是进行用户调查,设置以用户为中心的实际绩效指标,并根据这些指标衡量计划的效果。当且仅当您找到时如果出现性能问题,您应该使用适当的分析工具来确定性能问题的原因。如果原因是“内存碎片化”,那么通过确定“碎片化”的原因和尝试实验来确定哪种技术可以减轻这种影响。

“避免字符串连接”等“提示和技巧”无法实现性能。通过将工程学科应用于现实问题来实现绩效。

解决更具体的问题:我从未听过避免连接的建议,而是出于性能原因格式化 。通常给出的建议是避免迭代连接,而选择构建器。迭代级联在时间和空间上是二次的,并产生收集压力。构建器分配不必要的内存,但在典型情况下是线性的。既不会创建托管堆的碎片;迭代连接往往会产生连续的垃圾块。

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

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

我给你的建议是要么向你的教授提出解释,要么找一位对纪律表现指标更有纪律的教授。

现在,所有这些,你应该使用格式而不是连接,但不是性能的原因。相反,代码可读性,可本地化性和类似的风格问题。格式字符串可以构成资源,可以本地化,等等。

最后,我提醒您,如果您要将字符串放在一起以构建类似SQL查询或要向用户提供的HTML块,那么您希望使用 none 这些技巧。当你弄错它们时,这些字符串构建应用会产生严重的安全影响。使用专为构造这些对象而设计的库和工具,而不是使用字符串自行编写。

答案 1 :(得分: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}");

控制台输出:

Elapsed:00:00:00.0011883(StringBuilder.Append())

经过时间:00:00:01.7791839(+运营商)