Подтвердить что ты не робот

Эффективность и производительность памяти String.Replace.NET Framework

 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]
4b9b3361

Ответ 1

Все символы в строке .NET являются символами "unicode". Вы имеете в виду, что они не-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.

Ответ 2

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

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

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

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

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

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

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

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

Ответ 3

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

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

Ответ 4

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

        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), и это было против около 8MB текста в "source"...

Ответ 5

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

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

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

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

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

Технические данные

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

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

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

Созданный мной класс генерирует строки на основе шаблонов. Я помещаю токены ($ 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>

Шаблон

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

Ответ 6

StringBuilder sb = new StringBuilder("Hello string");
sb.Replace("string", String.Empty);
Console.WriteLine(sb);  

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

Ответ 7

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

Ответ 8

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