3

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


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

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

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

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

List 必须重新分配 string[] 的 interned 指针,而链表必须创建节点并修改指针,所以它们不是“免费”的,但如果我连接数千个 interned 字符串,那么它们看起来就像他们会更有效率一样。

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

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

  • 字符串生成器
  • 列表<string> 的实习字符串
  • LinkedList<string> 的实习字符串
  • 具有容量启发式的 StringBuilder
  • 还有什么?

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

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

4

7 回答 7

3

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

这是一个示例伪代码(语法不正确或其他任何内容):

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

然后,您将永远不需要脚本的内存表示,以及所有字符串的复制。

意见?

于 2009-05-02T20:30:37.570 回答
3

对于您的单独问题,Win32 有一个WriteFileGather函数,该函数可以有效地将(内部)字符串列表写入磁盘 - 但只有在异步调用时才会产生显着差异,因为磁盘写入会掩盖所有但非常大的串联。

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

您可以期望 StringBuilder 在每次重新分配时将分配大小加倍。这意味着将缓冲区从 256 字节增加到 1MB 只需 12 次重新分配 - 相当不错,因为您最初的估计与目标相差 3 个数量级。

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

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

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

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

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


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

尽管如此,看看这两个想法的比较以及列表何时开始变得更快会很有趣。

于 2009-05-02T23:47:14.103 回答
2

以我的经验,对于大量字符串数据,我正确分配的 StringBuilder 的性能优于大多数其他方法。浪费一些内存是值得的,甚至为了防止重新分配,将估计值超出 20% 或 30%。我目前没有使用我自己的数据来支持它的硬数字,但请查看此页面了解更多信息

然而,正如 Jeff 喜欢指出的那样,不要过早地优化!

编辑:正如@Colin Burnett 指出的那样,Jeff 进行的测试与 Brian 的测试不一致,但链接 Jeff 的帖子的重点是一般来说过早的优化。杰夫页面上的几位评论者指出了他的测试存在问题。

于 2009-05-01T18:19:55.100 回答
1

实际上StringBuilder使用String内部的一个实例。String实际上在System程序集中是可变的,这就是为什么StringBuilder可以在它之上构建。您可以StringBuilder通过在创建实例时分配合理的长度来提高效率。这样,您将消除/减少调整大小操作的数量。

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

只有当您的字符串相同时,实习才会使您受益。几乎相同的字符串不会从实习中受益,因此即使它们被实习也将是两个不同的字符串"SOMESTRINGA""SOMESTRINGB"

于 2009-05-01T18:28:29.450 回答
1

如果连接的所有(或大部分)字符串都被保留,那么您的方案可能会给您带来性能提升,因为它可能会使用更少的内存,并且可以节省一些大字符串副本。

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

唯一能真正分辨的方法是使用两种方式运行您的应用程序并测量结果。但是,除非您承受着巨大的内存压力,并且需要一种保存字节的方法,否则我不会打扰,只会使用字符串生成器。

于 2009-05-01T18:46:33.947 回答
1

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

StringBuilder为增加容量所做的重新分配意味着数据平均被复制了 1.33 次。如果您可以在创建时提供对大小的良好估计,则StringBuilder可以进一步减小该大小。

但是,为了获得一些观点,您应该查看您正在尝试优化的内容。在您的程序中花费的大部分时间是将数据实际写入磁盘,因此即使您可以将字符串处理优化为使用 a 的两倍StringBuilder(这不太可能),总体差异仍然只是百分之几。

于 2009-05-01T18:52:52.510 回答
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();

如果性能非常重要,我什至会让计算机评估使用依赖注入框架进行对象实例化的最佳路径。

于 2009-05-02T17:10:47.863 回答