String.Join बनाम StringBuilder: जो तेज है?


80

एक में पिछले प्रश्न एक प्रारूपित करने के बारे double[][]सीएसवी प्रारूप करने के लिए, यह सुझाव दिया गया था कि का उपयोग करते हुए StringBuilderतेजी से होगा String.Join। क्या ये सच है?


पाठकों स्पष्टता के लिए, यह एक का उपयोग कर के बारे में था एक StringBuilder, बनाम कई string.Join है, जो तब जुड़े हुए थे (n + 1 मिलती है)
मार्क Gravell

2
प्रदर्शन में अंतर बहुत तेज़ी से परिमाण के कई आदेशों तक चलता है। यदि आप कुछ मुट्ठी भर से अधिक काम करते हैं, तो आप स्ट्रिंगर के पास जाकर बहुत अच्छा प्रदर्शन हासिल कर सकते हैं
jalf

जवाबों:


116

संक्षिप्त उत्तर: यह निर्भर करता है।

लंबा उत्तर: यदि आपके पास पहले से ही एक साथ (एक सीमांकक के साथ) समतल करने की एक सरणी है, तो String.Joinयह करने का सबसे तेज़ तरीका है।

String.Joinसभी स्ट्रिंग्स के माध्यम से देख सकते हैं कि इसकी सही लंबाई की आवश्यकता है, फिर से जाएं और सभी डेटा को कॉपी करें। इसका मतलब है कि इसमें कोई अतिरिक्त नकल नहीं होगी । केवल नकारात्मक पक्ष यह है कि यह तार के माध्यम से दो बार जाने के लिए है, जो साधन संभावित मेमोरी कैश आवश्यकता से अधिक बार बह गई है।

आप तो नहीं है पहले से एक सरणी के रूप में तार है, यह शायद तेजी से उपयोग करने के लिए StringBuilder- लेकिन स्थितियों होगा जहां यह नहीं है। यदि StringBuilderबहुत सारी और बहुत सारी प्रतियों का उपयोग करने वाले साधनों का उपयोग किया जाता है, तो एक सरणी का निर्माण करना और फिर कॉल String.Joinकरना अच्छी तरह से तेज हो सकता है।

EDIT: यह कॉल करने के लिए कॉल का String.Joinएक गुच्छा बनाम एकल कॉल के संदर्भ में है StringBuilder.Append। मूल प्रश्न में, हमारे पास String.Joinकॉल के दो अलग-अलग स्तर थे , इसलिए प्रत्येक नेस्टेड कॉल ने एक मध्यवर्ती स्ट्रिंग बनाई होगी। दूसरे शब्दों में, इसके बारे में अनुमान लगाना और भी जटिल और कठिन है। मैं विशिष्ट डेटा के साथ किसी भी तरह से "जीत" काफी (जटिलता शब्दों में) देखकर आश्चर्यचकित रहूंगा।

संपादित करें: जब मैं घर पर होता हूं, तो मैं एक बेंचमार्क लिखूंगा जो संभवतः के लिए उतना ही दर्दनाक है StringBuilder। मूल रूप से यदि आपके पास एक ऐसा सरणी है जहां प्रत्येक तत्व पिछले एक के आकार से लगभग दोगुना है, और आपको यह सही लगता है, तो आपको प्रत्येक परिशिष्ट (तत्वों के नहीं, परिसीमाक की नहीं) के लिए एक प्रतिलिपि तैयार करने में सक्षम होना चाहिए, हालांकि इसकी आवश्यकता है इस पर भी ध्यान दिया जाए)। उस बिंदु पर यह लगभग उतना ही बुरा है जितना कि साधारण स्ट्रिंग का संघनन - लेकिन String.Joinइसमें कोई समस्या नहीं होगी।


6
यहां तक ​​कि जब मेरे पास पहले से तार नहीं है, तो यह स्ट्रिंग का उपयोग करने के लिए तेज लगता है। कृपया मेरे उत्तर की जांच करें ...
होसाम आली

2
पर निर्भर करेगा कि कैसे सरणी का उत्पादन किया जाता है, इसका आकार आदि मैं काफी निश्चित रूप से "मैं इस मामले में> स्ट्रिंग में खुश हूं। जो कम से कम तेजी से हो रहा है" - मैं ऐसा नहीं करना चाहूंगा उलटना।
जॉन स्कीट

4
(विशेष रूप से, मार्क के उत्तर को देखें, जहाँ स्ट्रिंगबर्ल स्ट्रींग को बाहर निकालता है। बस, सिर्फ जीवन ही जटिल है।)
जॉन स्कीट

2
@BornToCode: क्या आपका मतलब है StringBuilderएक मूल स्ट्रिंग के साथ निर्माण करना , फिर Appendएक बार कॉल करना ? हां, मैं string.Joinवहां जीतने की उम्मीद करूंगा ।
जॉन स्कीट

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

(अपडेट २०४ 20 * ६४ * १५०)

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] का उपयोग कर रहा हूं (लगभग 1 एमबी)। यदि आप मेरे द्वारा उपयोग की जाने वाली OptimizeForTesting()विधि का उपयोग करते हैं, तो भी आपके परिणाम किसी भी तरह से भिन्न होंगे ?
होसम ऐली

बहुत बहुत धन्यवाद मार्क। लेकिन मैंने देखा कि यह पहली बार नहीं है जब हमें माइक्रो-बेंचमार्क के लिए अलग-अलग परिणाम मिले हैं। क्या आपके पास कोई विचार है कि ऐसा क्यों हो सकता है?
होसाम ऐली

2
कर्म? ब्रह्मांडीय किरणों? कौन जानता है ... यह, सूक्ष्म के अनुकूलन के खतरों से पता चलता है, हालांकि ;-p
मार्क Gravell

आप उदाहरण के लिए एक AMD प्रोसेसर का उपयोग कर रहे हैं? ET64? शायद मेरे पास बहुत कम कैश मेमोरी (512 KB) है? या शायद Windows Vista पर .NET फ्रेमवर्क XP SP3 की तुलना में अधिक अनुकूलित है? तुम क्या सोचते हो? मुझे वाकई दिलचस्पी है कि ऐसा क्यों हो रहा है ...
होसम ऐली

XP SP3, 86, इंटेल Core2 जोड़ी T7250 @ 2GHz
मार्क Gravell

20

मुझे ऐसा नहीं लगता। परावर्तक के माध्यम से देखते हुए, 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]। मैंने दो सरणियों के लिए ऐसा किया; एक शून्य से भरा है और दूसरा यादृच्छिक मूल्यों से भरा है। मुझे अपनी मशीन (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));
    }
}

13

जब तक कि पूरे कार्यक्रम को चलाने में लगने वाले समय के संदर्भ में 1% अंतर कुछ महत्वपूर्ण हो जाता है, यह सूक्ष्म अनुकूलन जैसा दिखता है। मैं वह कोड लिखूंगा जो सबसे पठनीय / समझने योग्य है और 1% प्रदर्शन अंतर के बारे में चिंता नहीं करता है।


1
मेरा मानना ​​है कि स्ट्रिंग। जॉइन अधिक समझ में आता है, लेकिन पोस्ट एक मजेदार चुनौती थी। :) यह सीखने के लिए भी उपयोगी है (IMHO) कि कुछ अंतर्निहित विधियों का उपयोग करना हाथ से करने से बेहतर हो सकता है, तब भी जब अंतर्ज्ञान अन्यथा सुझाव दे सकता है। ...
होसम ऐली

... आम तौर पर, कई लोगों ने स्ट्रिंगब्यूलर का उपयोग करने का सुझाव दिया होगा। यहां तक ​​कि अगर String.Join 1% धीमा साबित हुआ, तो कई लोगों ने इसके बारे में नहीं सोचा होगा, सिर्फ इसलिए कि उन्हें लगता है कि StringBuilder तेज है।
होसम ऐली

मुझे जांच से कोई समस्या नहीं है, लेकिन अब जब आपके पास एक उत्तर है तो मुझे यकीन नहीं है कि प्रदर्शन चिंताजनक है। चूंकि मैं CSV में एक स्ट्रिंग बनाने के लिए किसी भी कारण के बारे में सोच सकता हूं, इसलिए इसे एक धारा के बाहर लिखने के अलावा, मैं संभवतः मध्यवर्ती स्ट्रिंग का निर्माण नहीं कर सकता।
tvanfosson


-3

हाँ। यदि आप कुछ जोड़े से अधिक करते हैं, तो यह बहुत तेज़ होगा।

जब आप एक string.join करते हैं, तो रनटाइम को निम्न करना होता है:

  1. परिणामी स्ट्रिंग के लिए मेमोरी आवंटित करें
  2. आउटपुट स्ट्रिंग की शुरुआत में पहले स्ट्रिंग की सामग्री को कॉपी करें
  3. दूसरी स्ट्रिंग की सामग्री को आउटपुट स्ट्रिंग के अंत में कॉपी करें।

यदि आप दो जोड़ करते हैं, तो इसे दो बार डेटा कॉपी करना होगा, और इसी तरह।

StringBuilder अंतरिक्ष के साथ एक बफर को छोड़ देता है, इसलिए डेटा को मूल स्ट्रिंग की प्रतिलिपि बनाए बिना जोड़ा जा सकता है। जैसा कि बफर में जगह बची है, एपेंडेड स्ट्रिंग को सीधे बफर में लिखा जा सकता है। फिर इसे पूरे स्ट्रिंग को एक बार, अंत में कॉपी करना होगा।


1
लेकिन String.Join अग्रिम में जानता है कि कितना आवंटित करना है, जबकि StringBuilder नहीं। अधिक स्पष्टीकरण के लिए कृपया मेरा उत्तर देखें।
होसम ऐली

@erikkallen: आप String.Join के लिए कोड को रिफ्लेक्टर में देख सकते हैं। red-gate.com/products/reflector/index.htm
ऐली
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.