2009-05-01 343 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("]"); 
    .... 
} 

然後,你永遠需要一個在腳本的內存中表示,與字符串的所有拷貝。

意見?