2009-05-01 279 views
3

我知道这个问题has已被done但我有一个稍微不同的扭曲。有几位已经指出,这是过早的优化,如果我只是为了实用性和实用性的缘故而这是完全正确的。我的问题植根于实际问题,但我仍然很好奇。在C#中使用字符串连接的字符串连接


我创建了一堆SQL语句来创建一个脚本(如它将被保存到磁盘)来重新创建一个数据库模式(容易很多很多的数百个表,视图等)。这意味着我的字符串连接是仅附加的。根据MSDN,StringBuilder通过保留内部缓冲区(当然是char [])和将字符串复制到它中,根据需要重新分配数组来工作。但是,我的代码有很多重复字符串(“CREATE TABLE [”,“GO \ n”等),这意味着我可以利用它们的优势being interned,但如果我使用StringBuilder,因为它们会被复制,所以不能使用它们每一次。唯一的变量基本上是表名,并且这些变量已经作为已存在于内存中的其他对象中的字符串存在。

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

假设,那么不会更快的List或LinkedList的字符串,因为它们保留指向interned字符串的指针?那么对于整个字符串的单个内存分配而言,只有一次调用String.Concat(),该分配恰好是正确的长度。

一个列表将不得不重新分配的String []实习指针和链表必须创建节点和修改指针,所以它们不是“免费”的事,但如果我串联成千上万的实习字符串,那么他们会觉得他们会更有效率。

现在,我想我能想出的字符计数启发式每个SQL语句&统计每个类型,并得到一个粗略的想法,并预先设定的我的StringBuilder的能力,以避免重新分配它的char [],但我不得不冲以公平的幅度减少重新分配的可能性。

因此,对于这种情况下,这将是最快的得到一个连接字符串:

  • 的StringBuilder
  • 列表<串实习串
  • 的LinkedList <串>实习串>
  • 具有容量启发式的StringBuilder
  • 还有其他的东西吗?

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

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

+0

这听起来像是不成熟的优化。是否需要比字符串生成器的性能更好? – kevindaub 2009-05-02 21:08:30

+0

如果你正在编写一个程序来复制数据库模式,并且你正在研究字符串连接的性能,那么你应该重新考虑你的优先级。 – 2009-05-02 22:50:31

+0

是啊,我不是新来的游戏,我明白过早的优化和优先事项(至少还有其他一些事情)。尽管如此,我并不是要求提供建议。 :)虽然这个问题根植于一个实际问题,但我并没有严格要求实用性。这就是说:你能回答这个问题吗? – 2009-05-02 23:21:48

回答

3

为了您单独的问题,Win32的有WriteFileGather功能,可以有效地写入的(实习)字符串到磁盘列表 - 但它将使只有一个显着的区别被异步调用的时候,因为磁盘的写入将会掩盖所有但非常大的连接。

对于你的主要问题:除非你达到兆字节的脚本,或成千上万的脚本,不用担心。

您可以期望StringBuilder在每次重新分配时加倍分配大小。这意味着将缓冲区从256字节增加到1MB只需要12次重新分配 - 相当不错,因为您的初始估计值远低于目标值的3个数量级。一些估计:构建一个1MB的缓冲区将扫描大约3 MB内存(1MB源,1MB目标,由于在实时操作期间复制 而产生的1MB)。

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

是的,列表实现可能更快,而且如果您的缓冲区是数量级更大。

对于小字符串更常见的情况,算法增益可以忽略不计,并且可以被其他因素轻易抵消:StringBuilder代码可能已经存在于代码缓存中,并且是用于微优化的可行目标。另外,如果最后一个字符串适合初始缓冲区,那么在内部使用字符串意味着完全不会复制。

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


所以,IMO StringBuilder的实现是正确的选择,对于常见情况进行了优化,并且主要降级为意外大的目标缓冲区。我期望一个列表实现首先会降低很多小部分,这实际上是StringBuilder试图优化的极端情况。

不过,看到两个想法的比较,以及何时该列表开始变得更快,将会很有趣。

2

根据我的经验,我正确地分配了StringBuilder比其他大部分字符串数据的表现都要优秀。值得浪费一些记忆,甚至是为了防止重新分配,超出你的估计20%或30%。我目前没有硬数据来支持使用我自己的数据,但看看this page for more

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

编辑:由于@Colin伯内特指出,杰夫进行不Brian的测试同意的测试,但连接杰夫的文章的观点是对一般过早的优化。杰夫的网页上有几位评论者指出他的测试存在问题。

+0

这两个链接只比较String和StringBuilder。我不确定我能推断其他4种解决方案的表现。 我发现Brian(第一个链接)和Jeff(第二个链接)在100,000次迭代中获得了截然不同的结果。布赖恩的189分和0.3分,杰夫分别是0.606和0.588。他们甚至没有可比性。 – 2009-05-01 18:33:05

+0

如果你阅读了Jeff的一些评论,他们会让他不去测试正确的东西。 “史蒂夫H”显示测试符合布赖恩的结果。但是,杰夫的观点仍然存在。除非*有*,否则不要担心。 – 2009-05-01 18:39:32

1

实际上StringBuilder在内部使用String的一个实例。 StringSystem组件中实际上是可变的,这就是为什么StringBuilder可以在其上构建的原因。通过在创建实例时分配合理的长度,可以使StringBuilder更有效一些。这样你将消除/减少调整大小操作的次数。

字符串实习可以在编译时识别字符串。因此,如果您在执行过程中生成大量字符串,它们将不会被禁用,除非您自己通过调用字符串的interning方法来执行此操作。

如果你的字符串是相同的,实习只会使你受益。几乎相同的字符串不会受益于实习,因此即使它们被实施,"SOMESTRINGA""SOMESTRINGB"也将是两个不同的字符串。

1

如果所有(或大部分)字符串被连接在一起,那么您的方案MIGHT可以提高性能,因为它可以有效地使用更少的内存,并且可以节省大量的字符串副本。

但是,它是否实际上提高了perf性能取决于您正在处理的数据量,因为改进是以恒定因子,而不是算法的数量级。

要真正说出的唯一方法是使用两种方式运行您的应用程序并测量结果。然而,除非你有明显的内存压力,并且需要一种保存字节的方法,否则我不会打扰,只会使用字符串生成器。

1

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

StringBuilder增加容量的重新分配意味着数据平均被复制了额外的1.33倍。如果您可以在创建StringBuilder时对尺寸做出较好的估计,那么您可以减少甚至更多。

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

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是这很重要。

3

如果我正在实现这样的事情,我永远不会构建一个StringBuilder(或任何其他脚本的内存缓冲区)。 我只是将它流式传输到您的文件,而不是内联。

下面是一个例子伪代码(不是语法正确或东西):

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

然后,你永远需要一个在脚本的内存中表示,与字符串的所有拷贝。

意见?