为什么StringBuilder比字符串操作更快,但List <t>比LinkedList <t>更快?</t> </t>

时间:2014-01-29 17:39:05

标签: c# arrays string list linked-list

因此我们被告知,当您对字符串执行多个操作时,应该使用StringBuilder(我听说只有三个)。因此我们应该替换它:

string s = "";
foreach (var item in items) // where items is IEnumerable<string>
    s += item;

有了这个:

string s = new StringBuilder(items).ToString();

我假设内部StringBuilder保存对每个Appended字符串的引用,然后根据请求进行组合。让我们将它与HybridDictionary进行比较,HybridDictionary对前10个元素使用LinkedList,然后当列表增长超过10时交换到HashTable。我们可以看到这里有相同类型的模式,少量引用= linkedList,否则不断增加数组块。

让我们看一下List的工作原理。从列表大小开始(内部默认值为4)。将元素添加到内部数组,如果数组已满,则创建一个当前数组大小的两倍的新数组,复制当前数组的元素,然后添加新元素并使新数组成为当前数组。

你能否看到我对性能优势的困惑?对于除字符串之外的所有元素,我们创建新数组,复制旧值并添加新值。但对于那些不好的字符串呢?因为我们知道“a”+“b”从两个旧的引用“a”和“b”中创建一个新的字符串引用。

希望我的问题不会太混乱。为什么字符串连接和数组连接之间似乎存在双重标准(我知道字符串是字符数组)?

字符串:制作新的参考资料很糟糕!

T:其中T!= String :制作新的参考资料很好!

编辑:也许我在这里真正要求的是,何时制作新的,更大的数组并复制旧的值,开始比在整个堆中随机放置对象的引用更快?

双重编辑:更快,我的意思是阅读,编写和查找变量,而不是插入或删除(例如,LinkedList会在插入时插入,但我不关心这一点。)

最终编辑:我不关心StringBuilder,我对将数据从堆的一部分复制到另一部分以进行缓存对齐所花费的时间感兴趣,而不仅仅是采取缓存从teh cpu中丢失,并在整个堆中都有引用。什么时候比另一个快?*

3 个答案:

答案 0 :(得分:4)

  

因此我们应该替换它:

不,你不应该。第一种情况是您显示字符串连接,它可以在编译时发生并用字符串连接替换它,该字符串连接发生在运行时。前者很多更令人满意,并且执行速度比后者快。

在编译时不知道正在连接的字符串数时,使用字符串生成器很重要。通常(但不总是)这意味着在循环中连接字符串。

早期版本的String Builder(在4.0之前,如果内存服务),内部看起来或多或少像List<char>,并且正确4.0后它看起来更像LinkedList<char[]>。但是,使用StringBuilder和在循环中使用常规字符串连接之间的关键区别不在于链接列表样式与对象包含对“链”中下一个对象的引用和基于数组的链接列表样式之间的区别内部缓冲区分配空间并根据需要偶尔重新分配的样式,而不是可变对象和不可变对象之间的差异。传统字符串连接的问题在于,由于字符串是不可变的,因此每个连接必须将两个字符串中的所有内存复制到新字符串中。使用StringBuilder时,只需将新字符串复制到某种类型的数据结构的末尾,保留所有现有内存。什么类型的数据结构在这里不是非常重要;我们可以依靠Microsoft来使用已被证明在最常见的情况下具有最佳性能特征的结构/算法。

答案 1 :(得分:1)

在我看来,您正在将列表的大小调整与字符串表达式的评估混为一谈,并假设两者的行为方式相同。

考虑您的示例:string s = "a" + "b" + "c" + "d"

假设没有对常量表达式进行优化(编译器会自动处理),那么它将依次评估每个操作:

string s = (("a" + "b") + "c") + "d"

这导致字符串"ab""abc"被创建为该单个表达式的一部分。这必须发生,因为.NET中的strings不可变的,这意味着它们的值一旦创建就无法更改。这是因为,如果字符串是可变的,你可以使用这样的代码:

string a = "hello";
string b = a;       // would assign b the same reference as a
string b += "world"; // would update the string it references
// now a == "helloworld"

如果这是List,代码会更有意义,甚至不需要解释:

var a = new List<int> { 1, 2, 3 };
var b = a;
b.Add(4);
// now a == { 1, 2, 3, 4 }

因此,非字符串“list”类型提前分配额外内存的原因是出于提高效率的原因,以及在扩展列表时减少分配。 string 的原因是因为string的值在创建后永远不会更新。

关于StringBuilder操作的假设是无关紧要的,但StringBuilder的目的主要是创建一个非不可变对象,以减少多个string操作的开销。

答案 2 :(得分:0)

StringBuilder支持商店char[],可根据需要调整大小。在您调用StringBuilder.ToString()之前,没有任何东西变成字符串。

List<T>的后备商店是T[],可根据需要调整大小。

这样的问题
string s = a + b + c + d ;

是编译器将其解析为

  +
 / \
a   +
   / \
  b   +
     / \
    c   d

并且除非能看到优化的机会,否则请执行

之类的操作
string t1 = c +  d ;
string t2 = b + t1 ;
string s  = a + t2 ;

因此创建了两个临时和最终的字符串。但是,使用StringBuilder,它将构建所需的字符数组,最后创建一个字符串。

这是一个胜利,因为字符串一旦创建,就是不可变(无法更改),并且通常在字符串池中 interned (意味着只有字符串的一个实例......无论你创建字符串"abc"多少次,每个实例都将始终是对字符串池中同一个对象的引用。

这也增加了字符串创建的成本:确定候选字符串后,运行时必须检查字符串池以查看它是否已存在。如果是,则使用该引用;如果没有将候选字符串添加到字符串池中。

你的例子,但是:

string s = "a" + "b" + "c" + "d" ;

是非sequitur:编译看到常量表达式并执行一个名为常量折叠的优化,因此它变为(即使在调试模式下):

string s = "abcd" ;

算术表达式会发生类似的优化:

int x = 12 / 3 ;

将被优化为

int x = 4 ;