String.Format có hiệu quả như StringBuilder


160

Giả sử tôi có một người xây dựng chuỗi trong C # thực hiện điều này:

StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();

điều đó sẽ hiệu quả hay hiệu quả hơn khi có:

string cat = "cat";
string s = String.Format("The {0} in the hat", cat);

Nếu vậy, tại sao?

BIÊN TẬP

Sau một số câu trả lời thú vị, tôi nhận ra có lẽ tôi nên rõ ràng hơn một chút trong những gì tôi đang hỏi. Tôi không hỏi quá nhiều về việc nối chuỗi nhanh hơn, nhưng nhanh hơn khi tiêm chuỗi này vào chuỗi khác.

Trong cả hai trường hợp trên, tôi muốn thêm một hoặc nhiều chuỗi vào giữa chuỗi mẫu được xác định trước.

Xin lỗi vì sự nhầm lẫn


Vui lòng để lại những mở để cho phép cải tiến trong tương lai.
Đánh dấu Biek

4
Trong trường hợp đặc biệt, nhanh nhất không phải là một trong hai trường hợp sau: nếu phần được thay thế có kích thước tương đương với phần mới, bạn có thể thay đổi chuỗi tại chỗ. Thật không may, điều này đòi hỏi mã phản ánh hoặc không an toàn và cố tình vi phạm tính bất biến của chuỗi. Không phải là một thực hành tốt, nhưng nếu tốc độ là một vấn đề ... :)
Abel

trong ví dụ đưa ra ở trên string s = "The "+cat+" in the hat";có thể là nhanh nhất trừ khi nó được sử dụng trong một vòng lặp, trong trường hợp nhanh nhất sẽ được StringBuilder khởi tạo bên ngoài vòng lặp.
Surya Pratap

Câu trả lời:


146

LƯU Ý: Câu trả lời này được viết khi .NET 2.0 là phiên bản hiện tại. Điều này có thể không còn áp dụng cho các phiên bản sau.

String.Formatsử dụng StringBuildernội bộ:

public static string Format(IFormatProvider provider, string format, params object[] args)
{
    if ((format == null) || (args == null))
    {
        throw new ArgumentNullException((format == null) ? "format" : "args");
    }

    StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
    builder.AppendFormat(provider, format, args);
    return builder.ToString();
}

Đoạn mã trên là một đoạn trích từ mscorlib, vì vậy câu hỏi trở thành " StringBuilder.Append()nhanh hơn StringBuilder.AppendFormat()"?

Nếu không có điểm chuẩn, tôi có thể nói rằng mẫu mã ở trên sẽ chạy nhanh hơn bằng cách sử dụng .Append(). Nhưng đó là một phỏng đoán, hãy thử điểm chuẩn và / hoặc định hình hai người để có được sự so sánh phù hợp.

Chap này, Jerry Dixon, đã làm một số điểm chuẩn:

http://jdixon.dotnetdevelopersjournal.com/opes_concatenation_opesbuilder_and_opesformat.htmlm

Cập nhật:

Đáng buồn là liên kết ở trên đã chết. Tuy nhiên, vẫn còn một bản sao trên Way Back Machine:

http://web.archive.org/web/20090417100252/http://jdixon.dotnetdevelopersjournal.com/opes_concatenation_opesbuilder_and_opesformat.htmlm

Vào cuối ngày, tùy thuộc vào việc định dạng chuỗi của bạn sẽ được gọi là lặp đi lặp lại, tức là bạn đang thực hiện một số xử lý văn bản nghiêm trọng trên 100 megabyte văn bản, hoặc liệu nó có được gọi khi người dùng nhấp vào nút một lần nữa không. Trừ khi bạn đang thực hiện một số công việc xử lý hàng loạt lớn mà tôi gắn bó với String.Format, nó hỗ trợ khả năng đọc mã. Nếu bạn nghi ngờ một nút cổ chai hoàn hảo thì hãy dán một hồ sơ vào mã của bạn và xem nó thực sự ở đâu.


8
Một vấn đề với các tiêu chuẩn trên trang Jerry Dixon là ông không bao giờ gọi .ToString()trên StringBuilderđối tượng. Trải qua rất nhiều lần lặp lại, thời gian đó tạo ra sự khác biệt lớn và có nghĩa là anh ta không hoàn toàn so sánh táo với táo. Đó là lý do anh ấy thể hiện hiệu suất tuyệt vời như vậy StringBuildervà có lẽ là lý do cho sự ngạc nhiên của anh ấy. Tôi chỉ lặp lại điểm chuẩn sửa lỗi đó và nhận được kết quả mong đợi: người String +vận hành nhanh nhất, theo sau StringBuilder, với String.Formatviệc đưa lên phía sau.
Ben Collins

5
6 năm sau, điều này không còn như vậy nữa. Trong Net4, string.Format () tạo và lưu trữ một cá thể StringBuilder mà nó sử dụng lại, do đó, trong một số trường hợp thử nghiệm có thể nhanh hơn StringBuilder. Tôi đã đặt một điểm chuẩn được sửa đổi trong câu trả lời bên dưới (vẫn nói rằng concat là nhanh nhất và đối với trường hợp thử nghiệm của tôi, định dạng chậm hơn 10% so với StringBuilder).
Chris F Carroll

45

Từ tài liệu MSDN :

Hiệu năng của hoạt động nối cho đối tượng String hoặc StringBuilder phụ thuộc vào tần suất phân bổ bộ nhớ xảy ra. Hoạt động nối chuỗi luôn phân bổ bộ nhớ, trong khi đó hoạt động nối chuỗi StringBuilder chỉ cấp phát bộ nhớ nếu bộ đệm đối tượng StringBuilder quá nhỏ để chứa dữ liệu mới. Do đó, lớp String thích hợp hơn cho hoạt động nối nếu một số đối tượng Chuỗi cố định được nối. Trong trường hợp đó, các hoạt động nối riêng lẻ thậm chí có thể được kết hợp thành một hoạt động duy nhất bởi trình biên dịch. Một đối tượng StringBuilder thích hợp hơn cho hoạt động nối nếu một số chuỗi tùy ý được nối; ví dụ: nếu một vòng lặp nối một số chuỗi ngẫu nhiên đầu vào của người dùng.


12

Tôi đã chạy một số điểm chuẩn hiệu suất nhanh và trong 100.000 hoạt động trung bình trên 10 lần chạy, phương thức đầu tiên (Trình tạo chuỗi) mất gần một nửa thời gian của lần thứ hai (Định dạng chuỗi).

Vì vậy, nếu điều này không thường xuyên, nó không thành vấn đề. Nhưng nếu đó là một hoạt động phổ biến, thì bạn có thể muốn sử dụng phương pháp đầu tiên.


10

Tôi hy vọng String.Format sẽ chậm hơn - nó phải phân tích chuỗi và sau đó nối nó.

Vài lưu ý:

  • Định dạng là cách dành cho các chuỗi người dùng có thể nhìn thấy trong các ứng dụng chuyên nghiệp; điều này tránh lỗi nội địa hóa
  • Nếu bạn biết trước độ dài của chuỗi kết quả, hãy sử dụng hàm tạo StringBuilder (Int32) để xác định trước dung lượng

8

Tôi nghĩ rằng trong hầu hết các trường hợp như sự rõ ràng này, và không hiệu quả, nên là mối quan tâm lớn nhất của bạn. Trừ khi bạn đang nghiền nát hàng tấn dây hoặc xây dựng một cái gì đó cho một thiết bị di động có công suất thấp hơn, điều này có thể sẽ không ảnh hưởng nhiều đến tốc độ chạy của bạn.

Tôi đã thấy rằng, trong trường hợp tôi xây dựng các chuỗi theo kiểu khá tuyến tính, thì thực hiện nối thẳng hoặc sử dụng StringBuilder là lựa chọn tốt nhất của bạn. Tôi đề nghị điều này trong trường hợp phần lớn chuỗi mà bạn đang xây dựng là động. Vì rất ít văn bản là tĩnh, nên điều quan trọng nhất là nó rõ ràng nơi mỗi đoạn văn bản động được đặt trong trường hợp nó cần được cập nhật trong tương lai.

Mặt khác, nếu bạn đang nói về một đoạn văn bản tĩnh lớn có hai hoặc ba biến trong đó, ngay cả khi nó kém hiệu quả hơn một chút, tôi nghĩ rằng sự rõ ràng bạn có được từ chuỗi.Format làm cho nó đáng giá. Tôi đã sử dụng điều này vào đầu tuần này khi phải đặt một chút văn bản động vào giữa tài liệu 4 trang. Sẽ dễ dàng hơn để cập nhật đoạn văn bản lớn đó nếu nó nằm trong một mảnh hơn là phải cập nhật ba đoạn mà bạn ghép lại với nhau.


Đúng! Sử dụng String.Format khi có ý nghĩa để làm như vậy, tức là khi bạn định dạng chuỗi. Sử dụng nối chuỗi hoặc StringBuilder khi bạn thực hiện nối cơ học. Luôn cố gắng chọn phương thức truyền đạt ý định của bạn đến người bảo trì tiếp theo.
Cướp

8

Nếu chỉ vì chuỗi.Format không thực hiện chính xác những gì bạn nghĩ, thì đây là bản chạy lại các bài kiểm tra 6 năm sau trên Net45.

Concat vẫn nhanh nhất nhưng thực sự nó chênh lệch ít hơn 30%. StringBuilder và Format chỉ khác nhau khoảng 5-10%. Tôi đã nhận được các biến thể 20% khi chạy thử nghiệm một vài lần.

Một phần nghìn giây, một triệu lần lặp:

  • Ghép: 367
  • StringBuilder mới cho mỗi khóa: 452
  • Bộ nhớ đệm StringBuilder: 419
  • chuỗi.Format: 475

Bài học tôi rút ra là sự khác biệt về hiệu năng là không đáng kể và vì vậy nó không nên ngăn bạn viết mã dễ đọc nhất mà bạn có thể. Mà tiền của tôi thường nhưng không phải lúc nào cũng vậy a + b + c.

const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);

var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];

var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();

for(int i=0; i<iterations; i++){
    var key1= keyprefix+":" + i.ToString();
    concatkeys[i]=key1;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
    stringbuilderkeys[i]= key2;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
    cachedsbkeys[i]= key2b;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("string.Format");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
    formatkeys[i]= key3;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);

2
Bởi "string.Format không thực hiện chính xác những gì bạn nghĩ" Ý tôi là trong mã nguồn 4.5, nó cố gắng tạo và sử dụng lại một cá thể StringBuilder được lưu trữ. Vì vậy, tôi đã đưa phương pháp đó vào thử nghiệm
Chris F Carroll

6

String.Format sử dụng StringBuildernội bộ ... một cách hợp lý dẫn đến ý tưởng rằng nó sẽ kém hiệu quả hơn một chút do có nhiều chi phí hơn. Tuy nhiên, nối chuỗi đơn giản là phương pháp nhanh nhất để tiêm một chuỗi giữa hai chuỗi khác ... ở một mức độ đáng kể. Bằng chứng này đã được Rico Mariani chứng minh trong Bài kiểm tra hiệu suất đầu tiên của mình, nhiều năm trước. Thực tế đơn giản là các phép nối ... khi số lượng các phần của chuỗi được biết đến (không giới hạn..bạn có thể nối một nghìn phần ... miễn là bạn biết luôn luôn 1000 phần) ... luôn nhanh hơn StringBuilderhoặc Chuỗi. Định dạng. Chúng có thể được thực hiện với một cấp phát bộ nhớ duy nhất một loạt các bản sao bộ nhớ. Đây là bằng chứng

Và đây là mã thực tế cho một số phương thức String.Concat, cuối cùng gọi FillStringChecked sử dụng các con trỏ để sao chép bộ nhớ (được trích xuất qua Reflector):

public static string Concat(params string[] values)
{
    int totalLength = 0;

    if (values == null)
    {
        throw new ArgumentNullException("values");
    }

    string[] strArray = new string[values.Length];

    for (int i = 0; i < values.Length; i++)
    {
        string str = values[i];
        strArray[i] = (str == null) ? Empty : str;
        totalLength += strArray[i].Length;

        if (totalLength < 0)
        {
            throw new OutOfMemoryException();
        }
    }

    return ConcatArray(strArray, totalLength);
}

public static string Concat(string str0, string str1, string str2, string str3)
{
    if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
    {
        return Empty;
    }

    if (str0 == null)
    {
        str0 = Empty;
    }

    if (str1 == null)
    {
        str1 = Empty;
    }

    if (str2 == null)
    {
        str2 = Empty;
    }

    if (str3 == null)
    {
        str3 = Empty;
    }

    int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
    string dest = FastAllocateString(length);
    FillStringChecked(dest, 0, str0);
    FillStringChecked(dest, str0.Length, str1);
    FillStringChecked(dest, str0.Length + str1.Length, str2);
    FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
    return dest;
}

private static string ConcatArray(string[] values, int totalLength)
{
    string dest = FastAllocateString(totalLength);
    int destPos = 0;

    for (int i = 0; i < values.Length; i++)
    {
        FillStringChecked(dest, destPos, values[i]);
        destPos += values[i].Length;
    }

    return dest;
}

private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
    int length = src.Length;

    if (length > (dest.Length - destPos))
    {
        throw new IndexOutOfRangeException();
    }

    fixed (char* chRef = &dest.m_firstChar)
    {
        fixed (char* chRef2 = &src.m_firstChar)
        {
            wstrcpy(chRef + destPos, chRef2, length);
        }
    }
}

Vậy thì:

string what = "cat";
string inthehat = "The " + what + " in the hat!";

Thưởng thức!


trong Net4, string.Format lưu trữ và sử dụng lại một cá thể StringBuilder để trong một số cách sử dụng có thể nhanh hơn.
Chris F Carroll

3

Oh cũng vậy, nhanh nhất sẽ là:

string cat = "cat";
string s = "The " + cat + " in the hat";

không, nối chuỗi cực kỳ chậm, vì .NET tạo ra các bản sao bổ sung của các biến chuỗi của bạn giữa các hoạt động concat, trong trường hợp này: hai bản sao bổ sung cộng với bản sao cuối cùng cho bài tập. Kết quả: hiệu suất cực kỳ kém so với StringBuilderđược thực hiện để tối ưu hóa loại mã này ngay từ đầu.
Abel

Nhanh nhất để nhập có thể;)
UpTheCalet

2
@Abel: Câu trả lời có thể thiếu chi tiết, nhưng cách tiếp cận này là lựa chọn nhanh nhất, trong ví dụ cụ thể này. Trình biên dịch sẽ chuyển đổi điều này thành một lệnh gọi String.Concat (), do đó, việc thay thế bằng StringBuilder sẽ thực sự làm chậm mã.
Dan C.

1
@Vaibhav là chính xác: trong trường hợp này, nối là nhanh nhất. Tất nhiên, sự khác biệt sẽ không đáng kể trừ khi lặp đi lặp lại rất nhiều lần, hoặc có lẽ hoạt động trên một chuỗi lớn hơn nhiều.
Ben Collins

0

Nó thực sự phụ thuộc. Đối với các chuỗi nhỏ có một vài cách nối, thực sự nhanh hơn chỉ là nối các chuỗi.

String s = "String A" + "String B";

Nhưng đối với chuỗi lớn hơn (chuỗi rất rất lớn), thì sử dụng StringBuilder sẽ hiệu quả hơn.


0

Trong cả hai trường hợp trên, tôi muốn thêm một hoặc nhiều chuỗi vào giữa chuỗi mẫu được xác định trước.

Trong trường hợp đó, tôi muốn đề xuất String.Format là nhanh nhất vì nó được thiết kế cho mục đích chính xác đó.



-1

Tôi đề nghị là không, vì String.Format không được thiết kế để ghép nối, nên nó được thiết kế để định dạng đầu ra của các đầu vào khác nhau, chẳng hạn như ngày.

String s = String.Format("Today is {0:dd-MMM-yyyy}.", DateTime.Today);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.