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 StringBuilder
sẽ nhanh hơn String.Join
. Điều này có đúng không?
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 StringBuilder
sẽ nhanh hơn String.Join
. Điều này có đúng không?
Câu trả lời:
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.Join
là cách nhanh nhất để thực hiện việc đó.
String.Join
có 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 StringBuilder
phươ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.Join
có thể nhanh hơn.
CHỈNH SỬA: Đây là điều khoản của một cuộc gọi đến String.Join
so 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.Join
cuộ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.Join
sẽ không có vấn đề gì.
StringBuilder
chuỗi gốc, sau đó gọi Append
một lần? Vâng, tôi hy vọng string.Join
sẽ giành chiến thắng ở đó.
string.Join
sử dụng (.NET 4.5) hiện tại StringBuilder
.
Đâ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 double
kế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();
}
}
}
OptimizeForTesting()
phương pháp tôi đang sử dụng?
Tôi không nghĩ vậy. Nhìn qua Reflector, việc triển khai các giao String.Join
diệ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.Join
là 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));
}
}
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%.
Atwood đã có một bài đăng liên quan đến điều này khoảng một tháng trước:
Đú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:
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.