String.Join so với StringBuilder: cái nào nhanh hơn?


80

Trong một câu hỏi trước đây về định dạng thành định dạng double[][]CSV, người ta đã gợi ý rằng việc sử dụng StringBuildersẽ nhanh hơn String.Join. Điều này có đúng không?


Để người đọc hiểu rõ, đó là về việc sử dụng một StringBuilder duy nhất , so với nhiều chuỗi.Join, sau đó được tham gia (n + 1 lần tham gia)
Marc Gravell

2
Sự khác biệt về hiệu suất nhanh chóng lên đến một số bậc lớn. Nếu bạn làm nhiều hơn là một số ít các tham gia, bạn có thể đạt được rất nhiều hiệu suất bằng cách chuyển sang StringBuilder
jalf

Câu trả lời:


116

Câu trả lời ngắn gọn: nó phụ thuộc.

Câu trả lời dài: nếu bạn đã có một mảng các chuỗi để nối với nhau (có dấu phân cách), String.Joinlà cách nhanh nhất để thực hiện việc đó.

String.Joincó thể xem qua tất cả các chuỗi để tìm ra độ dài chính xác mà nó cần, sau đó quay lại và sao chép tất cả dữ liệu. Điều này có nghĩa là sẽ không có thêm sự sao chép liên quan. Các chỉ nhược điểm là nó phải đi qua các dây hai lần, mà phương tiện có khả năng thổi bộ nhớ cache nhiều lần hơn mức cần thiết.

Nếu bạn không có sẵn các chuỗi dưới dạng một mảng, có thể sử dụng sẽ nhanh hơn StringBuilder- nhưng sẽ có những trường hợp không như vậy. Nếu sử dụng một StringBuilderphương tiện thực hiện rất nhiều và nhiều bản sao, thì việc xây dựng một mảng và sau đó gọi String.Joincó thể nhanh hơn.

CHỈNH SỬA: Đây là điều khoản của một cuộc gọi đến String.Joinso với một loạt các cuộc gọi tới StringBuilder.Append. Trong câu hỏi ban đầu, chúng ta có hai cấp độ String.Joincuộc gọi khác nhau , vì vậy mỗi lệnh gọi lồng nhau sẽ tạo ra một chuỗi trung gian. Nói cách khác, nó thậm chí còn phức tạp hơn và khó đoán hơn. Tôi sẽ ngạc nhiên khi thấy một trong hai cách "thắng" đáng kể (về độ phức tạp) với dữ liệu điển hình.

CHỈNH SỬA: Khi tôi ở nhà, tôi sẽ viết một điểm chuẩn mà nó gây đau đớn nhất có thể StringBuilder. Về cơ bản, nếu bạn có một mảng mà mỗi phần tử có kích thước gấp đôi kích thước của phần trước đó và bạn chỉnh sửa nó vừa phải, bạn sẽ có thể buộc một bản sao cho mọi phần nối thêm (của các phần tử, không phải của dấu phân cách, mặc dù điều đó cần phải cũng được tính đến). Tại thời điểm đó, nó gần giống như việc nối chuỗi đơn giản - nhưng String.Joinsẽ không có vấn đề gì.


6
Ngay cả khi tôi không có chuỗi trước, có vẻ như sử dụng String.Join sẽ nhanh hơn. Vui lòng kiểm tra câu trả lời của tôi ...
Hosam Aly

2
Tùy thuộc vào cách mảng được tạo ra, kích thước của nó, v.v. Tôi rất vui khi đưa ra một câu khá dứt khoát "Trong trường hợp <this> String.Join sẽ nhanh nhất là" - Tôi không muốn làm đảo ngược.
Jon Skeet

4
(. Đặc biệt, nhìn vào câu trả lời của Marc, nơi StringBuilder nhịp đập ra String.Join, chỉ là về cuộc sống rất phức tạp.)
Jon Skeet

2
@BornToCode: Ý bạn là xây dựng một StringBuilderchuỗi gốc, sau đó gọi Appendmột lần? Vâng, tôi hy vọng string.Joinsẽ giành chiến thắng ở đó.
Jon Skeet

13
[Nhu cầu chủ đề]: Việc triển khai string.Joinsử dụng (.NET 4.5) hiện tại StringBuilder.
n0rd

31

Đây là giàn thử nghiệm của tôi, sử dụng int[][]cho đơn giản; kết quả đầu tiên:

Join: 9420ms (chk: 210710000
OneBuilder: 9021ms (chk: 210710000

(cập nhật để biết doublekết quả :)

Join: 11635ms (chk: 210710000
OneBuilder: 11385ms (chk: 210710000

(cập nhật lại 2048 * 64 * 150)

Join: 11620ms (chk: 206409600
OneBuilder: 11132ms (chk: 206409600

và với OptimizeForTesting được bật:

Join: 11180ms (chk: 206409600
OneBuilder: 10784ms (chk: 206409600

Vì vậy, nhanh hơn, nhưng không ồ ạt như vậy; giàn (chạy ở bảng điều khiển, ở chế độ phát hành, v.v.):

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Collect()
        {
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
            GC.WaitForPendingFinalizers();
        }
        static void Main(string[] args)
        {
            const int ROWS = 500, COLS = 20, LOOPS = 2000;
            int[][] data = new int[ROWS][];
            Random rand = new Random(123456);
            for (int row = 0; row < ROWS; row++)
            {
                int[] cells = new int[COLS];
                for (int col = 0; col < COLS; col++)
                {
                    cells[col] = rand.Next();
                }
                data[row] = cells;
            }
            Collect();
            int chksum = 0;
            Stopwatch watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOPS; i++)
            {
                chksum += Join(data).Length;
            }
            watch.Stop();
            Console.WriteLine("Join: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum);

            Collect();
            chksum = 0;
            watch = Stopwatch.StartNew();
            for (int i = 0; i < LOOPS; i++)
            {
                chksum += OneBuilder(data).Length;
            }
            watch.Stop();
            Console.WriteLine("OneBuilder: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum);

            Console.WriteLine("done");
            Console.ReadLine();
        }
        public static string Join(int[][] array)
        {
            return String.Join(Environment.NewLine,
                    Array.ConvertAll(array,
                      row => String.Join(",",
                        Array.ConvertAll(row, x => x.ToString()))));
        }
        public static string OneBuilder(IEnumerable<int[]> source)
        {
            StringBuilder sb = new StringBuilder();
            bool firstRow = true;
            foreach (var row in source)
            {
                if (firstRow)
                {
                    firstRow = false;
                }
                else
                {
                    sb.AppendLine();
                }
                if (row.Length > 0)
                {
                    sb.Append(row[0]);
                    for (int i = 1; i < row.Length; i++)
                    {
                        sb.Append(',').Append(row[i]);
                    }
                }
            }
            return sb.ToString();
        }
    }
}

Cảm ơn Marc. Bạn nhận được gì cho các mảng lớn hơn? Tôi đang sử dụng [2048] [64] chẳng hạn (khoảng 1 MB). Ngoài ra, kết quả của bạn có khác nhau không nếu bạn sử dụng OptimizeForTesting()phương pháp tôi đang sử dụng?
Hosam Aly

Cảm ơn rất nhiều Marc. Nhưng tôi nhận thấy rằng đây không phải là lần đầu tiên chúng tôi nhận được các kết quả khác nhau cho các điểm chuẩn vi mô. Bạn có bất kỳ ý tưởng tại sao điều này có thể là?
Hosam Aly

2
Nghiệp? Các tia vũ trụ? Ai biết được ... nó cho thấy sự nguy hiểm của việc tối ưu hóa vi mô ;-p
Marc Gravell

Bạn có đang sử dụng bộ xử lý AMD không? ET64? Có lẽ tôi có quá ít bộ nhớ đệm (512 KB)? Hoặc có thể .NET framework trên Windows Vista được tối ưu hóa hơn so với XP SP3? Bạn nghĩ sao? Tôi thực sự quan tâm đến lý do tại sao điều này lại xảy ra ...
Hosam Aly

XP SP3, x86, Intel Core2 Duo T7250 @ 2GHz
Marc Gravell

20

Tôi không nghĩ vậy. Nhìn qua Reflector, việc triển khai các giao String.Joindiện trông rất tối ưu. Nó cũng có thêm lợi ích là biết trước tổng kích thước của chuỗi sẽ được tạo, vì vậy nó không cần bất kỳ sự phân bổ lại nào.

Tôi đã tạo hai phương pháp thử nghiệm để so sánh chúng:

public static string TestStringJoin(double[][] array)
{
    return String.Join(Environment.NewLine,
        Array.ConvertAll(array,
            row => String.Join(",",
                       Array.ConvertAll(row, x => x.ToString()))));
}

public static string TestStringBuilder(double[][] source)
{
    // based on Marc Gravell's code

    StringBuilder sb = new StringBuilder();
    foreach (var row in source)
    {
        if (row.Length > 0)
        {
            sb.Append(row[0]);
            for (int i = 1; i < row.Length; i++)
            {
                sb.Append(',').Append(row[i]);
            }
        }
    }
    return sb.ToString();
}

Tôi đã chạy mỗi phương thức 50 lần, truyền vào một mảng có kích thước [2048][64]. Tôi đã làm điều này cho hai mảng; một cái chứa đầy các số không và một cái khác chứa các giá trị ngẫu nhiên. Tôi nhận được các kết quả sau trên máy của mình (P4 3.0 GHz, lõi đơn, không có HT, đang chạy chế độ Phát hành từ CMD):

// with zeros:
TestStringJoin    took 00:00:02.2755280
TestStringBuilder took 00:00:02.3536041

// with random values:
TestStringJoin    took 00:00:05.6412147
TestStringBuilder took 00:00:05.8394650

Tăng kích thước của mảng lên [2048][512], trong khi giảm số lần lặp xuống 10, tôi nhận được kết quả sau:

// with zeros:
TestStringJoin    took 00:00:03.7146628
TestStringBuilder took 00:00:03.8886978

// with random values:
TestStringJoin    took 00:00:09.4991765
TestStringBuilder took 00:00:09.3033365

Kết quả có thể lặp lại (hầu như; với các dao động nhỏ do các giá trị ngẫu nhiên khác nhau gây ra). Rõ ràng String.Joinlà nhanh hơn một chút trong hầu hết thời gian (mặc dù biên độ rất nhỏ).

Đây là mã tôi đã sử dụng để thử nghiệm:

const int Iterations = 50;
const int Rows = 2048;
const int Cols = 64; // 512

static void Main()
{
    OptimizeForTesting(); // set process priority to RealTime

    // test 1: zeros
    double[][] array = new double[Rows][];
    for (int i = 0; i < array.Length; ++i)
        array[i] = new double[Cols];

    CompareMethods(array);

    // test 2: random values
    Random random = new Random();
    double[] template = new double[Cols];
    for (int i = 0; i < template.Length; ++i)
        template[i] = random.NextDouble();

    for (int i = 0; i < array.Length; ++i)
        array[i] = template;

    CompareMethods(array);
}

static void CompareMethods(double[][] array)
{
    Stopwatch stopwatch = Stopwatch.StartNew();
    for (int i = 0; i < Iterations; ++i)
        TestStringJoin(array);
    stopwatch.Stop();
    Console.WriteLine("TestStringJoin    took " + stopwatch.Elapsed);

    stopwatch.Reset(); stopwatch.Start();
    for (int i = 0; i < Iterations; ++i)
        TestStringBuilder(array);
    stopwatch.Stop();
    Console.WriteLine("TestStringBuilder took " + stopwatch.Elapsed);

}

static void OptimizeForTesting()
{
    Thread.CurrentThread.Priority = ThreadPriority.Highest;
    Process currentProcess = Process.GetCurrentProcess();
    currentProcess.PriorityClass = ProcessPriorityClass.RealTime;
    if (Environment.ProcessorCount > 1) {
        // use last core only
        currentProcess.ProcessorAffinity
            = new IntPtr(1 << (Environment.ProcessorCount - 1));
    }
}

13

Trừ khi sự khác biệt 1% trở thành điều gì đó đáng kể về thời gian chạy toàn bộ chương trình, điều này có vẻ giống như tối ưu hóa vi mô. Tôi sẽ viết mã dễ đọc / dễ hiểu nhất và không lo lắng về chênh lệch hiệu suất 1%.


1
Tôi tin rằng String.Join dễ hiểu hơn, nhưng bài đăng là một thử thách thú vị hơn. :) Nó cũng hữu ích (IMHO) khi biết rằng sử dụng một số phương pháp tích hợp có thể tốt hơn so với làm bằng tay, ngay cả khi trực giác có thể gợi ý khác. ...
Hosam Aly

... Thông thường, nhiều người sẽ đề xuất sử dụng StringBuilder. Ngay cả khi String.Join được chứng minh là chậm hơn 1%, nhiều người sẽ không nghĩ đến điều đó, chỉ vì họ nghĩ rằng StringBuilder nhanh hơn.
Hosam Aly

Tôi không có bất kỳ vấn đề nào với cuộc điều tra, nhưng giờ bạn đã có câu trả lời, tôi không chắc rằng hiệu suất là mối quan tâm hàng đầu. Vì tôi có thể nghĩ ra bất kỳ lý do nào để tạo một chuỗi trong CSV ngoại trừ việc viết nó ra một luồng, nên tôi có thể sẽ không xây dựng chuỗi trung gian.
tvanfosson


-3

Đúng. Nếu bạn thực hiện nhiều hơn một vài lần tham gia, nó sẽ nhanh hơn rất nhiều .

Khi bạn thực hiện một string.join, thời gian chạy phải:

  1. Phân bổ bộ nhớ cho chuỗi kết quả
  2. sao chép nội dung của chuỗi đầu tiên vào đầu chuỗi đầu ra
  3. sao chép nội dung của chuỗi thứ hai vào cuối chuỗi đầu ra.

Nếu bạn thực hiện hai phép nối, nó phải sao chép dữ liệu hai lần, v.v.

StringBuilder cấp phát một bộ đệm với không gian dự phòng, do đó, dữ liệu có thể được thêm vào mà không cần phải sao chép chuỗi gốc. Vì còn trống trong bộ đệm, chuỗi nối thêm có thể được ghi trực tiếp vào bộ đệm. Sau đó, nó chỉ phải sao chép toàn bộ chuỗi một lần vào cuối.


1
Nhưng String.Join biết trước số tiền cần phân bổ, trong khi StringBuilder thì không. Hãy xem câu trả lời của tôi để rõ hơn.
Hosam Aly

@erikkallen: Bạn có thể xem mã cho String.Join trong Reflector. red-gate.com/products/reflector/index.htm
Hosam Aly
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.