Przejdź do treści

Zamiana tekstu w C#

W tym artykule chciałbym porównać sposoby w jaki możemy zamienić tekst w tekście w C#. Jak zwykle użyję Benchmark.NET do sprawdzenia szybkości działania oraz pochłoniętej pamięci 😉

Kod żródłowy do przykładów z artykułu znajdziesz tutaj https://github.com/letyshub/string-replace-benchmark

Tekst źródłowy, czyli ten w którym zamierzam coś usunąć umieściłem w klasie Texts:

namespace StringReplaceBenchmark;

public static class Texts
{
    public const string LoremIpsum =
        @"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut eget viverra augue. Sed ac rhoncus lectus. Sed sollicitudin, felis quis volutpat imperdiet, leo ligula facilisis dolor, ut bibendum lorem tortor quis eros. Fusce et leo id ligula tempus porttitor eu in nisl. Ut non venenatis augue. Vivamus eget mattis leo. Aliquam auctor nibh quis sapien ornare, in gravida sapien pulvinar. In tincidunt turpis ac feugiat iaculis. Cras a feugiat massa. Pellentesque porta eros magna, at venenatis sem porttitor ut. Etiam nisl eros, sollicitudin eget commodo nec, fermentum id risus. Vestibulum at fermentum sem. Nulla ornare felis eget libero bibendum sagittis quis in nisl. Sed diam diam, efficitur ut varius at, egestas nec lorem.
          Vestibulum volutpat non felis a pellentesque. Nullam dignissim, est vitae fringilla facilisis, enim massa tincidunt augue, eu luctus dui elit a urna. Curabitur sed diam a neque molestie fermentum. Cras ultrices dolor sed porta consectetur. Nulla volutpat justo tincidunt purus finibus, quis aliquet nisl tincidunt. Suspendisse potenti. Etiam ac mi ac quam placerat viverra at non felis. Nulla sit amet luctus ante. Donec nisl velit, lobortis non commodo non, commodo in enim.
          Nunc risus libero, vehicula pulvinar risus ut, hendrerit fermentum felis. Nam justo leo, ultricies eget leo vitae, lacinia ullamcorper eros. Curabitur aliquet, quam vel aliquet suscipit, nulla nulla auctor neque, eget ornare nunc augue vel nunc. Mauris dapibus, magna mattis tempus rhoncus, libero ipsum venenatis ex, sit amet tincidunt enim sem id eros. Proin vestibulum dui at massa scelerisque, non rhoncus dolor posuere. Duis sit amet condimentum dui. Donec quis ullamcorper enim, a ultricies est. Vivamus porttitor, augue eget eleifend ornare, odio mauris dignissim tellus, non dapibus velit neque sit amet tellus.
          Fusce pellentesque ut eros vel dapibus. Curabitur feugiat a orci condimentum molestie. Integer tincidunt posuere orci, a aliquet mauris laoreet ut. Suspendisse quis tincidunt erat. Suspendisse et consequat massa, vel fringilla urna. Ut vehicula ante ut libero tempus, non venenatis risus luctus. Pellentesque suscipit suscipit urna, nec posuere nibh rutrum sit amet.
          Sed velit orci, auctor malesuada euismod in, hendrerit eu libero. Nam congue turpis nec eros varius vehicula eget ac odio. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed fermentum turpis ut fringilla laoreet. Vivamus orci elit, tristique et placerat ut, fringilla non nisl. Proin tempus mattis arcu eu sollicitudin. In vitae tellus vestibulum, blandit neque in, rutrum tellus. Mauris sodales orci at augue feugiat, nec consequat neque sollicitudin. In imperdiet ac elit malesuada lobortis. Nullam rutrum nulla vel odio vulputate, faucibus tristique odio cursus.";
}

Zamierzam zastępować kilka słów w tekście źródłowym (słownik destionationWords). Klasa z metodami zastępowania tekstu wygląda tak:

using System.Text;
using System.Text.RegularExpressions;
using BenchmarkDotNet.Attributes;

namespace StringReplaceBenchmark;

[MemoryDiagnoser]
[ShortRunJob]
public class StringReplaceBenchmarks
{
    private readonly Dictionary<string, string> destionationWords = new Dictionary<string, string>
    {
        {"vehicula", "aaa"},
        {"Lorem", "bbb"},
        {"risus", "ccc"},
        {"libero", "ddd"},
        {"condimentum", "eee"}
    };

    [Benchmark]
    public void UseForEachToReplace()
    {
        var result = Texts.LoremIpsum;
        foreach (var key in destionationWords.Keys)
        {
            result = result.Replace(key, destionationWords[key]);
        }
    }

    [Benchmark]
    public void UseStringBuilderToReplace()
    {
        var sb = new StringBuilder(Texts.LoremIpsum);
        foreach (var key in destionationWords.Keys)
        {
            sb.Replace(key, destionationWords[key]);
        }
    }

    [Benchmark]
    public void UseRegExToReplace()
    {
        var result = Texts.LoremIpsum;
        foreach (var key in destionationWords.Keys)
        {
            result = Regex.Replace(result, key, destionationWords[key]);
        }
    }
}

A więc przetestuję 3 sposoby podmiany tekstu, które znajdują się odpowiednio w metodach UseForEachToReplace(), UseStringBuilderToReplace() oraz UseRegExToReplace().

Dla powyższego kodu sprawa wygląda jak poniżej (na moim komputerze 😉 ). Tak jak można przypuszczać StringBuilderjest najlepszy jeśli chodzi o pamięciożerność ale jest też najwolniejszy (trochę mnie to zaskoczyło, że aż o tyle wolniejszy).

|                    Method |      Mean |    Error |    StdDev |   Gen0 |   Gen1 | Allocated |
|-------------------------- |----------:|---------:|----------:|-------:|-------:|----------:|
|       UseForEachToReplace |  3.229 us | 2.031 us | 0.1113 us | 4.4518 | 0.0725 |  27.27 KB |
| UseStringBuilderToReplace | 68.474 us | 7.272 us | 0.3986 us | 0.8545 |      - |   5.56 KB |
|         UseRegExToReplace |  5.611 us | 1.276 us | 0.0699 us | 4.4479 | 0.0687 |  27.27 KB |

A co jeśli do zamiany mamy tylko jeden tekst? W tym celu uruchomię benchmark z poniższą zmianą:

private readonly Dictionary<string, string> source = new Dictionary<string, string>
    {
        {"vehicula", "aaa"},
        //{"Lorem", "bbb"},
        //{"risus", "ccc"},
        //{"libero", "ddd"},
        //{"condimentum", "eee"}
    };

Tak jak można przypuszczać pamięciowo jest podobnie dla każdej z metod ale String.Replace() jest najszybszy.

|                    Method |        Mean |    Error |   StdDev |   Gen0 | Allocated |
|-------------------------- |------------:|---------:|---------:|-------:|----------:|
|       UseForEachToReplace |    621.4 ns | 275.9 ns | 15.12 ns | 0.8945 |   5.48 KB |
| UseStringBuilderToReplace | 14,726.7 ns | 970.2 ns | 53.18 ns | 0.9003 |   5.56 KB |
|         UseRegExToReplace |  1,050.1 ns | 546.9 ns | 29.98 ns | 0.8945 |   5.48 KB |

To ile pamięci użyjemy w metodzie UseForEachToReplace() czy UseRegExToReplace() oczywiście zależy od tego jak duży jest tekst. Im większy tym więcej zaalokujemy. Więc powinniśmy użyć StringBuilder.Replace() jeśli pracujemy na dużym tekście i mamy do podmienienia więcej niż 1 tekst.