C#中的字符串连接与实习字符串

时间:2009-05-01 18:07:41

标签: c# string performance stringbuilder

我知道这个问题has已经done,但我对此有一点不同。有几个人指出这是不成熟的优化,如果我只是为了实用性和实用性而要求,这是完全正确的。我的问题根源于实际问题,但我仍然很好奇。


我正在创建一堆SQL语句来创建脚本(因为它将保存到磁盘中)以重新创建数据库模式(很容易就有数百个表,视图等)。这意味着我的字符串连接只是附加的。根据MSDN,StringBuilder的工作原理是保留一个内部缓冲区(肯定是一个char [])和复制字符串字符,然后根据需要重新分配数组。

但是,我的代码有很多重复字符串(“CREATE TABLE [”,“GO \ n”等),这意味着我可以利用它们being interned但是如果我使用StringBuilder则不会每次都会被复制。唯一的变量本质上是表名​​,并且已经作为已经在内存中的其他对象中的字符串存在。

据我所知,在读入我的数据并创建了保存架构信息的对象之后,我的所有字符串信息都可以通过实习重用,是吗?

假设,那么字符串的List或LinkedList不会更快,因为它们保留了指向实习字符串的指针?然后,它只对String.Concat()进行一次调用,以便对整个字符串进行单个内存分配,该字符串的长度正确。

List必须重新分配interned指针的string [],链接列表必须创建节点并修改指针,所以它们不是“免费”做的,但是如果我连接成千上万的实习词然后他们看起来会更有效率。

现在我想我可以为每个SQL语句和字符计数提出一些启发式方法。计算每种类型并获得一个粗略的想法并预先设置我的StringBuilder容量以避免重新分配其char []但我必须以相当大的余量超过以减少重新分配的可能性。

因此,对于这种情况,获得单个连接字符串最快:

     
  • 的StringBuilder
  •  
  • 列表与LT;串GT;实习词串
  •  
  • 链表<串GT;实习词串
  •  
  • 具有容量启发式的StringBuilder
  •  
  • 别的什么?

作为单独的问题(我可能并不总是去磁盘)上面的内容:输出文件的单个StreamWriter会更快吗?或者,使用List或LinkedList,然后将它们写入列表中的文件,而不是先在内存中连接。

修改: 根据要求,the reference(.NET 3.5)到MSDN。它说:“如果空间可用,则将新数据附加到缓冲区的末尾;否则,分配一个新的更大的缓冲区,将原始缓冲区中的数据复制到新缓冲区,然后附加新数据到了新的缓冲区。“对我来说,这意味着一个char []被重新分配以使其更大(这需要将旧数据复制到已调整大小的数组)然后附加。

7 个答案:

答案 0 :(得分:3)

如果我实现这样的东西,我永远不会构建一个StringBuilder(或脚本的内存缓冲区中的任何其他)。 我会将其流式传输到您的文件中,并将所有字符串内联。

这是一个示例伪代码(在语法上不正确或任何东西):

FileStream f = new FileStream("yourscript.sql");
foreach (Table t in myTables)
{
    f.write("CREATE TABLE [");
    f.write(t.ToString());
    f.write("]");
    ....
}

然后,您将永远不需要脚本的内存表示,并且需要复制字符串。

评论

答案 1 :(得分:3)

对于单独的问题,Win32有一个 WriteFileGather 函数,它可以有效地将一个(实习的)字符串列表写入磁盘 - 但它会使只有在异步调用时才会出现明显的差异,因为磁盘写入会掩盖所有但非常大的连接。

对于主要问题:除非您达到兆字节的脚本或数万个脚本,否则请不要担心。

您可以期望StringBuilder在每次重新分配时将分配大小加倍。这意味着将缓冲区从256字节增加到1MB只需要12次重新分配 - 非常好,因为您的初始估计值比目标值低3个数量级。

纯粹作为练习,一些估计:构建1MB的缓冲区将扫描大约3 MB内存(1MB源,1MB目标,1MB由于 在实现期间复制)。

链表实现将扫描大约2MB,(这忽略了每个字符串引用的8字节/对象开销)。因此,与10Gbit / s和1MB L2缓存的典型内存带宽相比,您可以节省1 MB内存读/写。)

是的,列表实现可能更快,如果您的缓冲区大一个数量级,则差异很重要。

对于更常见的小字符串情况,算法增益可以忽略不计,并且很容易被其他因素抵消:StringBuilder代码可能已经在代码缓存中,并且是微优化的可行目标。此外,如果最终字符串适合初始缓冲区,则在内部使用字符串意味着根本没有复制。

使用链接列表还会将重新分配问题从O(字符数)降低到O(段数) - 您的字符串引用列表面临与字符串相同的问题!

<小时/> 因此,IMO StringBuilder的实现是正确的选择,针对常见情况进行了优化,并且主要针对意外大型目标缓冲区降级。我希望列表实现首先会针对很多小段降级,这实际上是StringBuilder尝试优化的极端情况。

然而,看到这两个想法的比较,以及列表开始变得更快时会很有趣。

答案 2 :(得分:2)

根据我的经验,我正确分配的StringBuilder在大量字符串数据方面优于大多数其他内容。即使通过将估计值超过20%或30%来防止重新分配,也值得浪费一些记忆。我目前没有硬数据来支持使用我自己的数据,但请查看this page for more

However, as Jeff is fond of pointing out, don't prematurely optimize!

编辑:正如@Colin Burnett指出的那样,Jeff所进行的测试与Brian的测试并不一致,但将Jeff的帖子联系在一起的意思是关于一般的过早优化。 Jeff的页面上的几位评论者指出了他的测试问题。

答案 3 :(得分:1)

实际上StringBuilder在内部使用String的实例。事实上StringSystem汇编中是可变的,这就是StringBuilder可以在其上构建的原因。通过在创建实例时指定合理的长度,可以使StringBuilder更有效。这样你就可以消除/减少调整大小操作的次数。

字符串实习适用于可在编译时识别的字符串。因此,如果你在执行过程中生成了很多字符串,除非你自己通过在字符串上调用interning方法这样做,否则它们不会被实现。

如果您的字符串相同,实习只会让您受益。几乎相同的字符串不会从实习中受益,因此"SOMESTRINGA""SOMESTRINGB"将是两个不同的字符串,即使它们是实习的。

答案 4 :(得分:1)

如果连接的所有(或大多数)字符串都被实现,那么你的方案可能会提升性能,因为它可以有效地使用更少的内存,并且可以节省一些大的字符串副本。

但是,它是否真正改善了性能取决于您正在处理的数据量,因为改进是恒定因素,而不是算法的数量级。

真正告诉的唯一方法是使用两种方式运行您的应用并测量结果。但是,除非你受到很大的内存压力,并且需要一种节省字节的方法,否则我不会打扰并只使用字符串构建器。

答案 5 :(得分:1)

StringBuilder不使用char[]来存储数据,它使用内部可变字符串。这意味着在连接字符串列表时没有额外的步骤来创建最终字符串,StringBuilder只是将内部字符串缓冲区作为常规字符串返回。

StringBuilder为增加容量所做的重新分配意味着数据平均被复制了1.33倍。如果您在创建StringBuilder时可以对大小做出很好的估计,那么可以减少甚至更进一步。

但是,为了获得一些观点,您应该看看您尝试优化的是什么。在程序中大部分时间都需要实际将数据写入磁盘,因此即使您可以优化字符串处理速度是使用StringBuilder的两倍(这是非常不可能的),整体差异仍然只有百分之几。

答案 6 :(得分:0)

您是否考虑过C ++?是否有一个已经构建T / SQL表达式的库类,最好用C ++编写。

关于字符串最慢的事情是malloc。在32位平台上每串需要4KB。考虑优化创建的字符串对象的数量。

如果你必须使用C#,我会推荐这样的东西:

string varString1 = tableName;
string varString2 = tableName;

StringBuilder sb1 = new StringBuilder("const expression");
sb1.Append(varString1);

StringBuilder sb2 = new StringBuilder("const expression");
sb2.Append(varString2);

string resultingString = sb1.ToString() + sb2.ToString();

如果perf非常重要,我甚至可以让计算机使用依赖注入框架评估对象实例化的最佳路径。