我知道这个问题 到过 完毕 但我对此有稍微不同的看法。一些人指出这是不成熟的优化,如果我只是为了实用而要求实用性,那么这是完全正确的。我的问题源于一个实际问题,但我仍然很好奇。


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

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

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

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

列表必须重新分配内部指针的 string[] ,而链表必须创建节点并修改指针,因此它们不是“自由”的,但如果我 连接数千个内部字符串 那么他们看起来就会更有效率。

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

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

  • 字符串生成器
  • 内部字符串列表<string>
  • 内部字符串的 LinkedList<string>
  • 具有容量启发式的 StringBuilder
  • 还有别的事吗?

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

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

有帮助吗?

解决方案

为您 单独的问题, Win32 有一个 写文件收集 函数,它可以有效地将(驻留的)字符串列表写入磁盘 - 但只有在异步调用时才会产生显着差异,因为磁盘写入将掩盖除极大级连接之外的所有连接。

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

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

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

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

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

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

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


因此,在我看来,StringBuilder 的实现是正确的选择,针对常见情况进行了优化,并且对于意外大的目标缓冲区来说,性能大多会下降。我预计列表实现首先会针对很多小段而降级,这实际上是 StringBuilder 试图优化的极端场景。

尽管如此,对这两种想法进行比较,以及列表何时开始变得更快,还是很有趣的。

其他提示

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

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

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

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

意见?

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

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

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

实际上 StringBuilder 使用一个实例 String 内部。 String 实际上是可变的 System 装配,这就是为什么 StringBuilder 可以建立在它之上。你(们)能做到 StringBuilder 在创建实例时分配合理的长度会更有效。这样您将消除/减少调整大小操作的数量。

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

只有当你的琴弦相同时,实习才会对你有利。几乎相同的字符串不会从实习中受益,所以 "SOMESTRINGA""SOMESTRINGB" 即使它们被保留,也将是两个不同的字符串。

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

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

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

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

重新分配 StringBuilder 增加容量意味着数据平均多复制 1.33 次。如果您可以在创建时提供对尺寸的良好估计 StringBuilder 您还可以进一步减少。

然而,为了获得一些视角,您应该看看您正在尝试优化的内容。程序中花费的大部分时间是将数据实际写入磁盘,因此即使您可以优化字符串处理,使其速度比使用 StringBuilder (这是非常不可能的),总体差异仍然只有几个百分点。

你考虑过用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();

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top