Эффективность памяти и производительность String.Replace .NET Framework - PullRequest
36 голосов
/ 30 декабря 2008
 string str1 = "12345ABC...\\...ABC100000"; 
 // Hypothetically huge string of 100000 + Unicode Chars
 str1 = str1.Replace("1", string.Empty);
 str1 = str1.Replace("22", string.Empty);
 str1 = str1.Replace("656", string.Empty);
 str1 = str1.Replace("77ABC", string.Empty);

 // ...  this replace anti-pattern might happen with upto 50 consecutive lines of code.

 str1 = str1.Replace("ABCDEFGHIJD", string.Empty);

Я унаследовал некоторый код, который делает то же самое, что и фрагмент выше. Он принимает огромную строку и заменяет (удаляет) постоянные меньшие строки из большой строки.

Я полагаю, что это очень интенсивный процесс, учитывая, что новые большие неизменяемые строки выделяются в памяти для каждой замены, ожидая смерти через GC.

1. Какой самый быстрый способ заменить эти значения, игнорируя проблемы с памятью?

2. Какой самый эффективный способ памяти для достижения того же результата?

Я надеюсь, что это один и тот же ответ!

Практические решения, которые находятся где-то посередине между этими целями, также приветствуются.

Допущения:

  • Все замены постоянны и известны заранее
  • Базовые символы содержат символы unicode [non-ascii]

Ответы [ 9 ]

23 голосов
/ 30 декабря 2008

Все символы в строке .NET являются "символами Юникода". Вы имеете в виду, что они не Ascii? Это не должно иметь никаких шансов - если только вы не столкнетесь с проблемами композиции, например «e + острый акцент» не заменяется при попытке заменить «е острый».

Вы можете попробовать использовать регулярное выражение с Regex.Replace или StringBuilder.Replace. Вот пример кода, выполняющий одно и то же с обоими:

using System;
using System.Text;
using System.Text.RegularExpressions;

class Test
{
    static void Main(string[] args)
    {
        string original = "abcdefghijkl";

        Regex regex = new Regex("a|c|e|g|i|k", RegexOptions.Compiled);

        string removedByRegex = regex.Replace(original, "");
        string removedByStringBuilder = new StringBuilder(original)
            .Replace("a", "")
            .Replace("c", "")
            .Replace("e", "")
            .Replace("g", "")
            .Replace("i", "")
            .Replace("k", "")
            .ToString();

        Console.WriteLine(removedByRegex);
        Console.WriteLine(removedByStringBuilder);
    }
}

Мне бы не хотелось догадываться, что является более эффективным - вам придется сравнивать с вашим конкретным приложением. Путь регулярного выражения может быть в состоянии сделать все это за один проход, но этот проход будет относительно загружен процессором по сравнению с каждой из множества замен в StringBuilder.

13 голосов
/ 30 декабря 2008

Если вы хотите быть по-настоящему быстрым, а я имею в виду по-настоящему быстрым, вам придется выйти за рамки StringBuilder и просто написать хорошо оптимизированный код.

Одна вещь, которую ваш компьютер не любит делать, это ветвление, если вы можете написать метод замены, который работает с фиксированным массивом (char *) и не ветвится, у вас отличная производительность.

Что вы будете делать, так это то, что операция замены будет искать последовательность символов, и если она найдет любую подобную подстроку, она заменит ее. Фактически вы скопируете строку, и при этом предварительно выполните поиск и замену.

Вы будете полагаться на эти функции для выбора индекса некоторого буфера для чтения / записи. Цель состоит в том, чтобы подготовить метод замены так, чтобы, когда ничего не нужно было менять, вы пишете ненужные файлы вместо ветвления.

Вы должны быть в состоянии выполнить это без единого оператора if и не забывать использовать небезопасный код. В противном случае вы будете платить за проверку индекса для каждого доступа к элементу.

unsafe
{
    fixed( char * p = myStringBuffer )
    {
        // Do fancy string manipulation here
    }
}

Я написал такой код на C # для забавы и увидел значительное улучшение производительности, почти на 300% быстрее, чем поиск и замена. Несмотря на то, что .NET BCL (библиотека базовых классов) работает довольно хорошо, он изобилует ветвящимися конструкциями, и обработка исключений замедлит ваш код, если вы используете встроенный материал. Кроме того, JIT-компилятор не обеспечивает идеальную оптимизацию, и вам придется запускать код как сборку релиза без подключенного отладчика, чтобы можно было наблюдать значительное увеличение производительности.

Я мог бы предоставить вам более полный код, но это значительный объем работы. Тем не менее, я могу гарантировать вам, что это будет быстрее, чем что-либо еще предложенное.

4 голосов
/ 31 марта 2016

1. Какой самый быстрый способ заменить эти значения, игнорируя проблемы с памятью?

Самый быстрый способ - создать пользовательский компонент, соответствующий вашему варианту использования. Начиная с .NET 4.6, в BCL нет класса, предназначенного для замены нескольких строк.

Если вам нужно что-то быстрое из BCL, StringBuilder - это самый быстрый компонент BCL для простой замены строки. Исходный код можно найти здесь : он достаточно эффективен для замены одной строки. Используйте Regex только в том случае, если вам действительно нужна сила сопоставления с образцом регулярных выражений. Это медленнее и немного громоздче, даже когда компилируется.

2. Каков наиболее эффективный способ памяти для достижения того же результата?

Наиболее эффективный способ памяти - выполнить отфильтрованную потоковую копию из источника в место назначения (объяснено ниже). Потребление памяти будет ограничено вашим буфером, однако это будет более ресурсоемким; как правило, вы собираетесь обменять производительность процессора на потребление памяти.

Технические подробности

Заменить строку сложно. Даже при замене строки в изменяемой области памяти (например, с StringBuilder ) это дорого. Если строка замены отличается от исходной строки, вы будете перемещать каждый символ, следующий за строкой замены, чтобы вся строка была смежной. Это приводит к большому количеству операций записи в память, и даже в случае StringBuilder заставляет переписывать большую часть строки в памяти при каждом вызове Replace.

Так, каков самый быстрый способ замены строк? Запишите новую строку, используя один проход: не позволяйте вашему коду вернуться назад, и вам придется что-то переписывать. Пишет дороже, чем читает. Тебе придется написать это самому для достижения наилучших результатов.

Решение с высокой памятью

Класс, который я написал, генерирует строки на основе шаблонов. Я помещаю токены ($ ReplaceMe $) в шаблон, который отмечает места, где я хочу вставить строку позже. Я использую его в тех случаях, когда XmlWriter слишком обременителен для XML, который в значительной степени статичен и повторяется, и мне нужно создавать большие потоки данных XML (или JSON).

Класс работает, разбивая шаблон на части и помещая каждую часть в пронумерованный словарь. Параметры также перечислены. Порядок, в котором части и параметры вставляются в новую строку, помещается в целочисленный массив. Когда генерируется новая строка, детали и параметры выбираются из словаря и используются для создания новой строки.

Он не является ни полностью оптимизированным, ни пуленепробиваемым, но отлично работает для генерации очень больших потоков данных из шаблонов.

Решение с низким объемом памяти

Вам нужно будет прочитать небольшие порции из исходной строки в буфер, выполнить поиск в буфере с использованием оптимизированного алгоритма поиска, а затем записать новую строку в целевой поток / строку. Здесь есть много потенциальных предостережений, но это будет эффективным для использования памятью и лучшим решением для динамических данных, которые невозможно кэшировать, таких как перевод на целую страницу или исходные данные, которые слишком велики для разумного кэширования. У меня нет примера решения для этой под рукой.

Пример кода

Желаемые результаты

<DataTable source='Users'>
  <Rows>
    <Row id='25' name='Administrator' />
    <Row id='29' name='Robert' />
    <Row id='55' name='Amanda' />
  </Rows>
</DataTable>

Template

<DataTable source='$TableName$'>
  <Rows>
    <Row id='$0$' name='$1$'/>
  </Rows>
</DataTable>

Контрольный пример

class Program
{
  static string[,] _users =
  {
    { "25", "Administrator" },
    { "29", "Robert" },
    { "55", "Amanda" },
  };

  static StringTemplate _documentTemplate = new StringTemplate(@"<DataTable source='$TableName$'><Rows>$Rows$</Rows></DataTable>");
  static StringTemplate _rowTemplate = new StringTemplate(@"<Row id='$0$' name='$1$' />");
  static void Main(string[] args)
  {
    _documentTemplate.SetParameter("TableName", "Users");
    _documentTemplate.SetParameter("Rows", GenerateRows);

    Console.WriteLine(_documentTemplate.GenerateString(4096));
    Console.ReadLine();
  }

  private static void GenerateRows(StreamWriter writer)
  {
    for (int i = 0; i <= _users.GetUpperBound(0); i++)
      _rowTemplate.GenerateString(writer, _users[i, 0], _users[i, 1]);
  }
}

Источник StringTemplate

public class StringTemplate
{
  private string _template;
  private string[] _parts;
  private int[] _tokens;
  private string[] _parameters;
  private Dictionary<string, int> _parameterIndices;
  private string[] _replaceGraph;
  private Action<StreamWriter>[] _callbackGraph;
  private bool[] _graphTypeIsReplace;

  public string[] Parameters
  {
    get { return _parameters; }
  }

  public StringTemplate(string template)
  {
    _template = template;
    Prepare();
  }

  public void SetParameter(string name, string replacement)
  {
    int index = _parameterIndices[name] + _parts.Length;
    _replaceGraph[index] = replacement;
    _graphTypeIsReplace[index] = true;
  }

  public void SetParameter(string name, Action<StreamWriter> callback)
  {
    int index = _parameterIndices[name] + _parts.Length;
    _callbackGraph[index] = callback;
    _graphTypeIsReplace[index] = false;
  }

  private static Regex _parser = new Regex(@"\$(\w{1,64})\$", RegexOptions.Compiled);
  private void Prepare()
  {
    _parameterIndices = new Dictionary<string, int>(64);
    List<string> parts = new List<string>(64);
    List<object> tokens = new List<object>(64);
    int param_index = 0;
    int part_start = 0;

    foreach (Match match in _parser.Matches(_template))
    {
      if (match.Index > part_start)
      {
        //Add Part
        tokens.Add(parts.Count);
        parts.Add(_template.Substring(part_start, match.Index - part_start));
      }


      //Add Parameter
      var param = _template.Substring(match.Index + 1, match.Length - 2);
      if (!_parameterIndices.TryGetValue(param, out param_index))
        _parameterIndices[param] = param_index = _parameterIndices.Count;
      tokens.Add(param);

      part_start = match.Index + match.Length;
    }

    //Add last part, if it exists.
    if (part_start < _template.Length)
    {
      tokens.Add(parts.Count);
      parts.Add(_template.Substring(part_start, _template.Length - part_start));
    }

    //Set State
    _parts = parts.ToArray();
    _tokens = new int[tokens.Count];

    int index = 0;
    foreach (var token in tokens)
    {
      var parameter = token as string;
      if (parameter == null)
        _tokens[index++] = (int)token;
      else
        _tokens[index++] = _parameterIndices[parameter] + _parts.Length;
    }

    _parameters = _parameterIndices.Keys.ToArray();
    int graphlen = _parts.Length + _parameters.Length;
    _callbackGraph = new Action<StreamWriter>[graphlen];
    _replaceGraph = new string[graphlen];
    _graphTypeIsReplace = new bool[graphlen];

    for (int i = 0; i < _parts.Length; i++)
    {
      _graphTypeIsReplace[i] = true;
      _replaceGraph[i] = _parts[i];
    }
  }

  public void GenerateString(Stream output)
  {
    var writer = new StreamWriter(output);
    GenerateString(writer);
    writer.Flush();
  }

  public void GenerateString(StreamWriter writer)
  {
    //Resolve graph
    foreach(var token in _tokens)
    {
      if (_graphTypeIsReplace[token])
        writer.Write(_replaceGraph[token]);
      else
        _callbackGraph[token](writer);
    }
  }

  public void SetReplacements(params string[] parameters)
  {
    int index;
    for (int i = 0; i < _parameters.Length; i++)
    {
      if (!Int32.TryParse(_parameters[i], out index))
        continue;
      else
        SetParameter(index.ToString(), parameters[i]);
    }
  }

  public string GenerateString(int bufferSize = 1024)
  {
    using (var ms = new MemoryStream(bufferSize))
    {
      GenerateString(ms);
      ms.Position = 0;
      using (var reader = new StreamReader(ms))
        return reader.ReadToEnd();
    }
  }

  public string GenerateString(params string[] parameters)
  {
    SetReplacements(parameters);
    return GenerateString();
  }

  public void GenerateString(StreamWriter writer, params string[] parameters)
  {
    SetReplacements(parameters);
    GenerateString(writer);
  }
}
4 голосов
/ 19 мая 2011

Вот быстрый тест ...

        Stopwatch s = new Stopwatch();
        s.Start();
        string replace = source;
        replace = replace.Replace("$TS$", tsValue);
        replace = replace.Replace("$DOC$", docValue);
        s.Stop();

        Console.WriteLine("String.Replace:\t\t" + s.ElapsedMilliseconds);

        s.Reset();

        s.Start();
        StringBuilder sb = new StringBuilder(source);
        sb = sb.Replace("$TS$", tsValue);
        sb = sb.Replace("$DOC$", docValue);
        string output = sb.ToString();
        s.Stop();

        Console.WriteLine("StringBuilder.Replace:\t\t" + s.ElapsedMilliseconds);

Я не видел большой разницы на моей машине (string.replace был 85ms и stringbuilder.replace был 80), и это было против около 8 МБ текста в "source" ...

4 голосов
/ 30 декабря 2008

StringBuilder: http://msdn.microsoft.com/en-us/library/2839d5h5.aspx

Производительность самой операции замены должна быть примерно такой же, как и у строки. Заменить и, согласно Microsoft, мусор не должен производиться.

3 голосов
/ 30 декабря 2008
StringBuilder sb = new StringBuilder("Hello string");
sb.Replace("string", String.Empty);
Console.WriteLine(sb);  

StringBuilder , изменяемая строка.

1 голос
/ 01 августа 2018

Вот мой тест :

using System;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

internal static class MeasureTime
{
    internal static TimeSpan Run(Action func, uint count = 1)
    {
        if (count <= 0)
        {
            throw new ArgumentOutOfRangeException("count", "Must be greater than zero");
        }

        long[] arr_time = new long[count];
        Stopwatch sw = new Stopwatch();
        for (uint i = 0; i < count; i++)
        {
            sw.Start();
            func();
            sw.Stop();
            arr_time[i] = sw.ElapsedTicks;
            sw.Reset();
        }

        return new TimeSpan(count == 1 ? arr_time.Sum() : Convert.ToInt64(Math.Round(arr_time.Sum() / (double)count)));
    }
}

public class Program
{
    public static string RandomString(int length)
    {
        Random random = new Random();
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return new String(Enumerable.Range(1, length).Select(_ => chars[random.Next(chars.Length)]).ToArray());
    }

    public static void Main()
    {
        string rnd_str = RandomString(500000);
        Regex regex = new Regex("a|c|e|g|i|k", RegexOptions.Compiled);
        TimeSpan ts1 = MeasureTime.Run(() => regex.Replace(rnd_str, "!!!"), 10);
        Console.WriteLine("Regex time: {0:hh\\:mm\\:ss\\:fff}", ts1);

        StringBuilder sb_str = new StringBuilder(rnd_str);
        TimeSpan ts2 = MeasureTime.Run(() => sb_str.Replace("a", "").Replace("c", "").Replace("e", "").Replace("g", "").Replace("i", "").Replace("k", ""), 10);
        Console.WriteLine("StringBuilder time: {0:hh\\:mm\\:ss\\:fff}", ts2);

        TimeSpan ts3 = MeasureTime.Run(() => rnd_str.Replace("a", "").Replace("c", "").Replace("e", "").Replace("g", "").Replace("i", "").Replace("k", ""), 10);
        Console.WriteLine("String time: {0:hh\\:mm\\:ss\\:fff}", ts3);

        char[] ch_arr = {'a', 'c', 'e', 'g', 'i', 'k'};
        TimeSpan ts4 = MeasureTime.Run(() => new String((from c in rnd_str where !ch_arr.Contains(c) select c).ToArray()), 10);
        Console.WriteLine("LINQ time: {0:hh\\:mm\\:ss\\:fff}", ts4);
    }

}

Регулярное время: 00: 00: 00: 008

StringBuilder time: 00: 00: 00: 015

Строка времени: 00: 00: 00: 005

LINQ не может обработать rnd_str (фатальная ошибка: превышен лимит использования памяти)

Строка. Замена самая быстрая

0 голосов
/ 30 декабря 2008

Поскольку у вас есть несколько замен в одной строке, я рекомендую вам использовать RegEx вместо StringBuilder.

0 голосов
/ 30 декабря 2008

Если вы хотите встроенный класс в dotnet, я думаю, что StringBuilder - лучший. чтобы сделать это вручную, вы можете использовать небезопасный код с символом char *, перебирать вашу строку и заменять в соответствии с вашими критериями

...