String.join vs. StringBuilder : 어느 쪽이 빠르나요?
-
06-09-2019 - |
해결책
짧은 대답 : 그것은 달라집니다.
긴 답변 : 이미 (구분자와 함께) 함께 연결하기 위해 많은 문자열이 있다면, String.Join
가장 빠른 방법입니다.
String.Join
모든 문자열을 살펴보면 필요한 정확한 길이를 해결 한 다음 다시 이동하여 모든 데이터를 복사 할 수 있습니다. 이것은있을 것임을 의미합니다 아니요 추가 복사. 그만큼 뿐 단점은 문자열을 두 번 통과해야한다는 것입니다. 즉, 메모리 캐시를 필요보다 더 많은 시간을 불어 넣을 수 있습니다.
만약 너라면 ~하지 않다 문자열을 미리 배열로 사용합니다. 아마 더 빨리 사용합니다 StringBuilder
- 그러나 그렇지 않은 상황이있을 것입니다. A를 사용하는 경우 StringBuilder
많은 사본을하고 배열을 만들고 String.Join
더 빠를 수도 있습니다.
편집 : 이것은 단일 통화 측면에서입니다. String.Join
대로 많은 전화를 걸었습니다 StringBuilder.Append
. 원래 질문에서 우리는 두 가지 수준의 String.Join
전화를 걸어 각 중첩 된 통화가 중간 문자열을 생성했을 것입니다. 다시 말해, 그것은 더 복잡하고 추측하기가 더 어렵습니다. 나는 일반적인 데이터로 "승리"를 크게 (복잡한 용어로) 보는 것에 놀랄 것입니다.
편집 : 집에있을 때는 가능한 한 고통스러운 벤치 마크를 쓸 것입니다. StringBuilder
. 기본적으로 각 요소가 이전 요소의 크기의 두 배 정도 인 배열이있는 경우, 바로 제대로 얻을 수 있다면, 모든 부록에 대한 사본을 강제 할 수 있어야합니다 (구분 기자가 아닌 요소의 요소에 대한 사본이 필요합니다. 도 고려하십시오). 그 시점에서 그것은 간단한 문자열 연결만큼 나쁘지만 - 그러나 String.Join
아무런 문제가 없을 것입니다.
다른 팁
여기 내 테스트 장비가 사용됩니다 int[][]
단순성을 위해; 첫 번째 결과 :
Join: 9420ms (chk: 210710000
OneBuilder: 9021ms (chk: 210710000
(업데이트 double
결과:)
Join: 11635ms (chk: 210710000
OneBuilder: 11385ms (chk: 210710000
(RE 2048 * 64 * 150 업데이트)
Join: 11620ms (chk: 206409600
OneBuilder: 11132ms (chk: 206409600
그리고 최적화가 활성화 된 다음 :
Join: 11180ms (chk: 206409600
OneBuilder: 10784ms (chk: 206409600
너무 빠르지 만 크게는 그렇지 않습니다. 리그 (콘솔에서 실행, 릴리스 모드 등) :
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();
}
}
}
나는 그렇게 생각하지 않습니다. 반사판을 통해, 구현 String.Join
매우 최적화 된 것 같습니다. 또한 사전에 생성 될 문자열의 총 크기를 아는 것이 추가되므로 재 할당이 필요하지 않습니다.
나는 그것들을 비교하기위한 두 가지 테스트 방법을 만들었습니다.
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();
}
각 방법을 50 번 실행하여 크기의 배열을 전달합니다. [2048][64]
. 나는 두 배열에 대해 이것을했다. 하나는 0으로 채워져 있고 다른 하나는 임의의 값으로 채워져 있습니다. 내 컴퓨터에서 다음과 같은 결과를 얻었습니다 (P4 3.0 GHz, 싱글 코어, HT 없음, 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
배열의 크기를 증가시킵니다 [2048][512]
, 반복 횟수를 10으로 줄이면 다음과 같은 결과를 얻었습니다.
// 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
결과는 반복 가능합니다 (거의; 다른 임의의 값으로 인한 작은 변동으로). 보기에 String.Join
대부분의 시간이 조금 더 빠릅니다 (매우 작은 마진으로).
이것은 내가 테스트하는 데 사용한 코드입니다.
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));
}
}
전체 프로그램이 실행되는 시간에 따라 1% 차이가 중요한 것으로 바뀌지 않으면 이는 마이크로 최적화와 같습니다. 가장 읽기 쉬운/이해하기 쉬운 코드를 작성하고 1% 성능 차이에 대해 걱정하지 않습니다.
Atwood는 약 한 달 전에 이것과 관련된 게시물을 가지고있었습니다.
네. 몇 가지 이상의 결합을하면 많이 더 빠르게.
string.join을 수행하면 런타임이 다음을 수행해야합니다.
- 결과 문자열에 메모리를 할당하십시오
- 첫 번째 문자열의 내용을 출력 문자열의 시작에 복사합니다.
- 두 번째 문자열의 내용을 출력 문자열 끝에 복사하십시오.
두 개의 결합을 수행하면 데이터를 두 번 복사해야합니다.
StringBuilder는 하나의 버퍼를 스페이스로 할당하므로 원래 문자열을 복사하지 않고도 데이터를 추가 할 수 있습니다. 버퍼에 남은 공간이 있으므로 추가 된 문자열은 버퍼에 직접 기록 될 수 있습니다. 그런 다음 끝에 전체 문자열을 한 번 복사하면됩니다.