String.Replace () проти StringBuilder.Replace ()


79

У мене є рядок, у якому мені потрібно замінити маркери значеннями зі словника. Це повинно бути максимально ефективним. Виконання циклу з рядком. Replace просто споживає пам'ять (рядки незмінні, пам'ятайте). Чи був би StringBuilder.Replace () кращим, оскільки він був розроблений для роботи з маніпуляціями з рядками?

Я сподівався уникнути витрат RegEx, але якщо це буде більш ефективним, тоді нехай буде.

Примітка: Мені не важлива складність коду, а лише те, наскільки швидко він працює і пам'ять, яку він споживає.

Середня статистика: 255-1024 символів довжиною, 15-30 ключів у словнику.


Який шаблон (довжина) маркерів та значень?
Henk Holterman

Короткий. Маркери 5-15, значення 5-25
Дастін Девіс

Відповіді:


73

Використання RedGate Profiler за допомогою наступного коду

class Program
    {
        static string data = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
        static Dictionary<string, string> values;

        static void Main(string[] args)
        {
            Console.WriteLine("Data length: " + data.Length);
            values = new Dictionary<string, string>()
            {
                { "ab", "aa" },
                { "jk", "jj" },
                { "lm", "ll" },
                { "yz", "zz" },
                { "ef", "ff" },
                { "st", "uu" },
                { "op", "pp" },
                { "x", "y" }
            };

            StringReplace(data);
            StringBuilderReplace1(data);
            StringBuilderReplace2(new StringBuilder(data, data.Length * 2));

            Console.ReadKey();
        }

        private static void StringReplace(string data)
        {
            foreach(string k in values.Keys)
            {
                data = data.Replace(k, values[k]);
            }
        }

        private static void StringBuilderReplace1(string data)
        {
            StringBuilder sb = new StringBuilder(data, data.Length * 2);
            foreach (string k in values.Keys)
            {
                sb.Replace(k, values[k]);
            }
        }

        private static void StringBuilderReplace2(StringBuilder data)
        {
            foreach (string k in values.Keys)
            {
                data.Replace(k, values[k]);
            }
        }
    }
  • String.Replace = 5.843ms
  • StringBuilder. Замінити # 1 = 4,059 мс
  • Stringbuilder. Замініть # 2 = 0,461 мс

Довжина рядка = 1456

stringbuilder # 1 створює конструктор рядків у методі, тоді як # 2 не робить цього, тому різниця в продуктивності в підсумку виявиться однаковою, швидше за все, оскільки ви просто переносите цю роботу з методу. Якщо ви починаєте з конструктора рядків замість рядка, то замість цього може піти # 2.

Що стосується пам'яті, використовуючи профайлер RedGateMemory, немає про що турбуватися, поки ви не ввійдете в БАГАТО операцій заміни, в яких конструктор рядків має загальну перемогу.


Якби компілятор оптимізував цю можливість, чи змінить він рядок StringBuilderReplace1 (дані); до StringBuilderReplace2 (новий StringBuilder (data, data.Length * 2)) ;? Просто цікаво. Я розумію різницю, мені було просто цікаво, якби ви знали.
pqsk

1
Я не розумію, чому метод 2 SB набагато швидший - JIT повинен оптимізувати як SB # 1, так і SB # 2, щоб вони були однаковими під час виконання.
Дай

@Dai майте на увазі, це було в 2011 році. З того часу, можливо, ситуація змінилася.
Дастін Девіс,

7
@Dai - (пізня відповідь) - як зазначено у відповіді, профайлер вимірює лише минулий час фактичної функції. Оскільки оголошення будівельника рядків у Replace # 2 знаходиться поза функцією, час побудови не враховується за минулий час.
Stirrblig

Отже, ви говорите, що 89% часу заміни StringBuilderReplace1 просто ініціалізує екземпляр StringBuilder? І приблизно лише 11% (0,461 з 4,059) часу витрачається на заміну ключів, як це виділено в StringBuilderReplace2? Якщо так ... Я б виділив буфер StringBuilder і обмежився певною мірою паралельності для пакетної обробки за допомогою існуючих примірників. Тепер питання в тому, що ... що додає StringBuilder.ToString до часу? Тому що, чесно кажучи, ваш цільовий результат - це все-таки рядок, і лише перший метод насправді створює рядок.
Трійко

9

Це може допомогти:

http://blogs.msdn.com/b/debuggingtoolbox/archive/2008/04/02/comparing-regex-replace-string-replace-and-stringbuilder-replace-which-has-better-performance.aspx

Короткою відповіддю є те, що String.Replace швидше, хоча це може мати більший вплив на розмір пам'яті / збір сміття.


цікаво. На основі їх тесту, string.replace був кращим. Я думав, що через невеликий розмір рядків string.replace буде краще розглядати будь-які накладні витрати на створення конструктора рядків
Дастін Девіс,

6

Так, StringBuilderце дасть вам обом приріст у швидкості та пам’яті (в основному тому, що він не створюватиме екземпляр рядка кожного разу, коли ви будете з ним маніпулювати - StringBuilderзавжди працює з одним і тим же об’єктом). Ось посилання MSDN із деякими деталями.


але чи варто накладні витрати на створення конструктора рядків?
Дастін Девіс,

1
@Dustin: з 15-30 замінами це, мабуть, є.
Хенк Холтерман

@Henk - є 15-30 пошукових запитів, не обов'язково так багато замін. Очікувана середня кількість маркерів на рядок є значною.
Джо

@Joe: буде проведено 15-30 сканувань (крім, можливо, регулярного виразу).
Henk Holterman

@Henk, так, але продуктивність сканування 15-30 буде однаковою для String та StringBuilder. Будь-яка різниця у продуктивності, яка виправдовує накладні витрати на створення конструктора рядків, повинна відбуватися заміною, а не скануванням.
Джо

5

Чи був би stringbuilder.replace кращим [ніж String.Replace]

Так, набагато краще. І якщо ви можете оцінити верхню межу для нового рядка (схоже, ви можете), то це, мабуть, буде досить швидким.

Коли ви створюєте його, як:

  var sb = new StringBuilder(inputString, pessimisticEstimate);

тоді StringBuilder не доведеться перерозподіляти свій буфер.


1

Перетворення даних із рядка в StringBuilder і назад займе певний час. Якщо виконується лише одна операція заміни, цей час може не окупитись підвищенням ефективності, властивим StringBuilder. З іншого боку, якщо хтось перетворює рядок у StringBuilder, а потім виконує над ним багато операцій Replace і перетворює його назад наприкінці, підхід StringBuilder може бути швидшим.


1

Замість того, щоб виконувати операції заміни 15-30 на всьому рядку, можливо, було б ефективніше використовувати щось на зразок структури даних trie для зберігання вашого словника. Потім ви можете один раз прокрутити вхідний рядок, щоб здійснити весь пошук / заміну.


1

Це багато в чому буде залежати від того, скільки маркерів у даному рядку є в середньому.

Ефективність пошуку ключа, ймовірно, буде однаковою між StringBuilder і String, але StringBuilder виграє, якщо вам доведеться замінити багато маркерів в одному рядку.

Якщо ви в середньому очікуєте лише одного або двох маркерів на рядок, а ваш словник невеликий, я б просто вибрав String.Replace.

Якщо маркерів багато, можливо, ви захочете визначити власний синтаксис для ідентифікації маркерів - наприклад, вкладання в фігурні дужки з відповідним правилом екранування для буквальної дужки. Потім можна реалізувати алгоритм синтаксичного аналізу, який перебирає символи рядка один раз, розпізнаючи та замінюючи кожен маркер, який він знаходить. Або скористайтеся регулярним виразом.


+1 для регулярного виразу - зауважте, що якщо це зроблено, фактична заміна може використовувати a MatchEvaluatorдля фактичного пошуку словника.
Random832

1

Мої два центи тут, я просто написав кілька рядків коду, щоб перевірити, як працює кожен метод, і, як очікувалося, результат "це залежить".

Для довших рядків Regexздається, що вони працюють краще, для коротших - String.Replaceце. Я бачу, що використання StringBuilder.Replaceне дуже корисно, і якщо його неправильно використовувати, воно може бути смертельним у перспективі GC (я намагався поділитися одним екземпляром StringBuilder).

Перевірте моє репозиторій StringReplaceTests GitHub .


1

Проблема відповіді @DustinDavis полягає в тому, що він рекурсивно працює на одному рядку. Якщо ви не плануєте робити маніпуляції вперед-назад, ви дійсно повинні мати окремі об'єкти для кожного випадку маніпуляції у цьому виді тесту.

Я вирішив створити власний тест, оскільки знайшов суперечливі відповіді в усьому Інтернеті, і хотів бути повністю впевненим. Програма, над якою я працюю, займається великою кількістю тексту (у деяких випадках файлами з десятками тисяч рядків).

Отже, ось швидкий спосіб, який ви можете скопіювати та вставити та переконайтесь, що швидший. Можливо, вам доведеться створити власний текстовий файл для тестування, але ви можете легко скопіювати та вставити текст з будь-якого місця і зробити для себе досить великий файл:

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Windows;

void StringReplace_vs_StringBuilderReplace( string file, string word1, string word2 )
{
    using( FileStream fileStream = new FileStream( file, FileMode.Open, FileAccess.Read ) )
    using( StreamReader streamReader = new StreamReader( fileStream, Encoding.UTF8 ) )
    {
        string text = streamReader.ReadToEnd(),
               @string = text;
        StringBuilder @StringBuilder = new StringBuilder( text );
        int iterations = 10000;

        Stopwatch watch1 = new Stopwatch.StartNew();
        for( int i = 0; i < iterations; i++ )
            if( i % 2 == 0 ) @string = @string.Replace( word1, word2 );
            else @string = @string.Replace( word2, word1 );
        watch1.Stop();
        double stringMilliseconds = watch1.ElapsedMilliseconds;

        Stopwatch watch2 = new Stopwatch.StartNew();
        for( int i = 0; i < iterations; i++ )
            if( i % 2 == 0 ) @StringBuilder = @StringBuilder .Replace( word1, word2 );
            else @StringBuilder = @StringBuilder .Replace( word2, word1 );
        watch2.Stop();
        double StringBuilderMilliseconds = watch1.ElapsedMilliseconds;

        MessageBox.Show( string.Format( "string.Replace: {0}\nStringBuilder.Replace: {1}",
                                        stringMilliseconds, StringBuilderMilliseconds ) );
    }
}

Я отримав цей рядок. Replace () був швидшим приблизно на 20% щоразу, замінюючи 8-10 літерних слів. Спробуйте самі, якщо хочете власні емпіричні докази.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.