String.Join 대 StringBuilder : 어느 것이 더 빠릅니까?


80

A의 이전 질문 서식 지정에 대한 double[][]CSV 형식으로, 그것은 제안 된 사용하는 것이 StringBuilder보다 빠른 것이다 String.Join. 이것이 사실입니까?


독자의 명확성을 위해 단일 StringBuilder 를 사용하는 것이 아니라 여러 string.Join을 사용한 다음 결합 (n + 1 조인)
Marc Gravell

2
성능의 차이는 몇 배까지 빠르게 진행됩니다. 몇 가지 조인 이상을 수행하는 경우 stringbuilder로 전환하여 많은 성능을 얻을 수 있습니다
jalf

답변:


116

짧은 대답 : 상황에 따라 다릅니다.

긴 대답 : 함께 연결할 문자열 배열 (구분 기호 사용)이 이미있는 경우 String.Join가장 빠른 방법입니다.

String.Join필요한 정확한 길이를 찾기 위해 모든 문자열을 살펴본 다음 다시 가서 모든 데이터를 복사 할 수 있습니다. 이는 추가 복사 가 필요 하지 않음을 의미 합니다. 유일한 단점은 수단이 잠재적으로 필요 이상으로 메모리 캐시에게 더 많은 시간을 불고있는 두 문자열을 통해 이동한다는 것이다.

당신이 경우 하지 않는 사전 배열로 문자열을 가지고, 그건 아마 더 빨리 사용하는 StringBuilder-하지만 그렇지 않은 경우 상황이있을 것이다. StringBuilder많은 복사 를하는 수단을 사용한다면 배열을 구축 한 다음 호출하는 String.Join것이 더 빠를 수 있습니다.

편집 : 이것은 단일 호출 String.JoinStringBuilder.Append. 원래 질문에서는 두 가지 수준의 String.Join호출이 있었으므로 각 중첩 호출은 중간 문자열을 만들었습니다. 즉, 추측하기가 훨씬 더 복잡하고 어렵습니다. 나는 일반적인 데이터로 (복잡성 측면에서) 상당히 "승리"하는 것을보고 놀랄 것입니다.

편집 : 집에있을 때 가능한 한 고통스러운 벤치 마크를 작성하겠습니다 StringBuilder. 기본적으로 각 요소의 크기가 이전 요소의 약 두 배인 배열이 있고 올바르게 가져 오면 모든 추가 (구분자가 아닌 요소의 복사본을 강제 할 수 있어야합니다. 또한 고려되어야합니다). 이 시점에서는 단순한 문자열 연결만큼 나쁘지만 String.Join문제는 없습니다.


6
사전에 문자열이 없더라도 String.Join을 사용하는 것이 더 빠른 것 같습니다. 내 대답을 확인하십시오 ...
Hosam Aly

2
배열이 어떻게 생성되는지, 크기 등에 따라 달라집니다. "In <this>의 경우 String.Join은 적어도 빠를 것입니다."라는 말을하게되어 기쁩니다. 역전.
Jon Skeet

4
(특히, StringBuilder가 String.Join을 능가하는 Marc의 대답을보십시오. 삶은 복잡합니다.)
Jon Skeet

2
@BornToCode : StringBuilder원래 문자열로를 구성한 다음 Append한 번 호출 한다는 뜻 입니까? 네, string.Join거기에서 이길 것으로 기대 합니다.
Jon Skeet 2014 년

13
[스레드 네크로]의 전류 (.NET 4.5) 실시 string.Join용도 StringBuilder.
n0rd

31

다음 int[][]은 간단하게 사용 하는 테스트 장비입니다 . 첫 번째 결과 :

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

( double결과 업데이트 :)

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

(2048 * 64 * 150으로 업데이트)

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

OptimizeForTesting이 활성화 된 경우 :

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();
        }
    }
}

감사합니다 마크. 더 큰 어레이의 경우 무엇을 얻습니까? 예를 들어 [2048] [64]를 사용하고 있습니다 (약 1MB). 또한 OptimizeForTesting()내가 사용하는 방법 을 사용하면 결과가 어떻게 다른 가요?
Hosam Aly

감사합니다. Marc. 그러나 마이크로 벤치 마크에 대해 다른 결과를 얻은 것은 이번이 처음이 아닙니다. 왜 그런지 아십니까?
Hosam Aly

2
업? 우주 광선? 누가는, 마이크로 최적화의 위험을 보여줍니다 ... 알고 ;-p하지만
마크 Gravell

예를 들어 AMD 프로세서를 사용하고 있습니까? ET64? 캐시 메모리 (512KB)가 너무 적습니까? 아니면 Windows Vista의 .NET 프레임 워크가 XP SP3보다 최적화되어 있습니까? 어떻게 생각해? 왜 이런 일이 일어나고 있는지 정말 관심이 있습니다 ...
Hosam Aly

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

20

나는 그렇게 생각하지 않는다. Reflector를 살펴보면 구현이 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 번 실행하여 size 배열을 전달했습니다 [2048][64]. 두 개의 배열에 대해이 작업을 수행했습니다. 하나는 0으로 채워지고 다른 하나는 임의의 값으로 채워집니다. 내 컴퓨터에서 다음과 같은 결과를 얻었습니다 (P4 3.0GHz, 단일 코어, 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));
    }
}

13

1 % 차이가 전체 프로그램을 실행하는 데 걸리는 시간 측면에서 중요한 것으로 바뀌지 않는 한 이는 마이크로 최적화처럼 보입니다. 가장 읽기 쉽고 이해하기 쉬운 코드를 작성하고 1 % 성능 차이에 대해 걱정하지 않습니다.


1
나는 String.Join이 더 이해하기 쉽다고 생각하지만 게시물은 더 재미있는 도전이었습니다. :) 또한 몇 가지 기본 제공 방법을 사용하는 것이 직관이 달리 제안 할 수있는 경우에도 손으로하는 것보다 낫다는 것을 배우는 것도 유용합니다 (IMHO). ...
Hosam Aly

... 일반적으로 많은 사람들이 StringBuilder 사용을 제안했을 것입니다. String.Join이 1 % 더 느린 것으로 판명 되었더라도 많은 사람들은 StringBuilder가 더 빠르다고 생각 하기 때문에 그것에 대해 생각하지 않았을 것입니다.
Hosam Aly

나는 조사에 아무런 문제가 없지만 이제 답을 얻었으므로 성능이 최우선 관심사인지 확실하지 않습니다. 스트림에 쓰는 것을 제외하고 CSV로 문자열을 구성하는 이유를 생각할 수 있기 때문에 중간 문자열을 전혀 구성하지 않을 것입니다.
tvanfosson


-3

예. 조인을 몇 번 이상 수행 하면 훨씬 빠릅니다.

string.join을 수행 할 때 런타임은 다음을 수행해야합니다.

  1. 결과 문자열에 메모리 할당
  2. 첫 번째 문자열의 내용을 출력 문자열의 시작 부분에 복사
  3. 두 번째 문자열의 내용을 출력 문자열의 끝에 복사합니다.

두 번의 조인을 수행하면 데이터를 두 번 복사해야하는 식입니다.

StringBuilder는 여유 공간이있는 하나의 버퍼를 할당하므로 원본 문자열을 복사하지 않고도 데이터를 추가 할 수 있습니다. 버퍼에 남은 공간이 있으므로 추가 된 문자열을 버퍼에 직접 쓸 수 있습니다. 그런 다음 마지막에 전체 문자열을 한 번만 복사하면됩니다.


1
그러나 String.Join은 할당 할 양을 미리 알고 있지만 StringBuilder는 그렇지 않습니다. 자세한 내용은 내 대답을 참조하십시오.
Hosam Aly

@erikkallen : Reflector에서 String.Join에 대한 코드를 볼 수 있습니다. red-gate.com/products/reflector/index.htm
Hosam Aly
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.