Какой самый быстрый способ удалить все символы в строке до совпадения с образцом в C ++? - PullRequest
2 голосов
/ 25 апреля 2020

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

    /// There is a tab before this text.
    Sample Text   There is a tab in between the word "Text" and "There" in this line.
    9919
    2250
    {
        ""
        5
        255
    }

В настоящее время я просто запускаю следующий код для замены вкладок (после загрузки файла в память) ...

void FileParser::ReplaceAll(
   std::string& the_string,
   const std::string& from,
   const std::string& to) const
{
   size_t start_pos = 0;
   while ((start_pos = the_string.find(from, start_pos)) != std::string::npos)
   {
      the_string.replace(start_pos, from.length(), to);
      start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx'
   }
}

С этим кодом есть две проблемы ...

  • Требуется 18 секунд, чтобы завершить замену этого текста.
  • Это заменяет ВСЕ вкладки, я просто хочу, чтобы вкладки были до первого символа без вкладок. Поэтому, если в строке есть символы табуляции после символов без табуляции ... они не будут удалены.

Может кто-нибудь предложить решение, которое ускорило бы процесс и удалило только начальные отступы табуляции каждой строки?

Ответы [ 2 ]

2 голосов
/ 25 апреля 2020

Я бы сделал это так:

std::string without_leading_chars(const std::string& in, char remove)
{
    std::string out;
    out.reserve(in.size());
    bool at_line_start = true;
    for (char ch : in)
    {
        if (ch == '\n')
            at_line_start = true;
        else if (at_line_start)
        {
            if (ch == remove)
                continue; // skip this char, do not copy
            else
                at_line_start = false;
        }
        out.push_back(ch);
    }
    return out;
}

Это одно выделение памяти и один проход, так что это довольно близко к оптимальному.

1 голос
/ 25 апреля 2020

как всегда. Мы часто можем набрать больше скорости, думая о хороших алгоритмах и создавая хороший дизайн.

Первый комментарий. Я проверил ваш подход с исходным файлом размером 100 МБ, и на моем компьютере в режиме Release со всеми оптимизациями потребовалось не менее 30 минут.

И, как вы сами отметили. Он возвращает все пробелы, а не только те, которые находятся в начале файла. Итак, нам нужно найти лучшее решение

Сначала мы подумаем о том, как мы можем определить пробелы в начале строки. Для этого нам понадобится логический флаг, который указывает, что мы находимся в начале строки. Мы назовем его beginOfLine и первоначально установим на true, потому что файл всегда начинается со строки.

Затем, далее, мы проверяем, является ли следующий символ пробелом ' ' или символ табуляции '\t'. В отличие от других решений, мы проверим оба.

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

Если символ не является пробелом или табуляцией, мы проверяем наличие новой строки. Если мы нашли один, то мы устанавливаем флаг beginOfLine в true, иначе в false. В любом случае мы хотим использовать символ.

Все это можно поместить в простую лямбду с состоянием

        auto check = [beginOfLine = true](const char c) mutable -> bool {
            if ((c == ' ') || (c == '\t')  ) 
                return !beginOfLine;    
            beginOfLine = (c == '\n'); 
            return true; };

или, более компактно:

auto check = [beginOfLine = true](const char c) mutable -> bool {
    if (c == ' ' || c == '\t') return !beginOfLine; beginOfLine = (c == '\n'); return true; };

Затем, следующий. Мы не будем стирать пробелы из исходной строки, потому что это огромная операция по смещению памяти, которая занимает очень много времени. Вместо этого мы копируем данные (символы) в новую строку, но только необходимые единицы.

И для этого мы можем использовать std::copy_if из стандартной библиотеки.

std::copy_if(data.begin(), data.end(), data2.begin(), check);

Это сделает работу. А для данных объемом 100 МБ требуется 160 мс. По сравнению с 30 минутами это огромная экономия.

Пожалуйста, посмотрите пример кода (который, конечно, нужно адаптировать для ваших нужд):

#include <iostream>
#include <fstream>
#include <filesystem>
#include <iterator>
#include <algorithm>
#include <string>

namespace fs = std::filesystem;
constexpr size_t SizeOfIOStreamBuffer = 1'000'000;
static char ioBuffer[SizeOfIOStreamBuffer];

int main() {

    // Path to text file
    const fs::path file{ "r:\\test.txt" };

    // Open the file and check, if it could be opened
    if (std::ifstream fileStream(file); fileStream) {

        // Lambda, that identifies, if we have a spece or tab at the begin of a line or not
        auto check = [beginOfLine = true](const char c) mutable -> bool {
            if (c == ' ' || c == '\t') return !beginOfLine; beginOfLine = (c == '\n'); return true; };

        // Huge string with all file data
        std::string data{};

        // Reserve space to spped up things and to avoid uncessary allocations
        data.resize(fs::file_size(file));

        // Used buffered IO with a huge iobuffer
        fileStream.rdbuf()->pubsetbuf(ioBuffer, SizeOfIOStreamBuffer);

        // Read file, Elimiate spaces and tabs at the beginning of the line and store in data
        std::copy_if(std::istreambuf_iterator<char>(fileStream), {}, data.begin(), check);
    }
    return 0;
}

Как видите, все кипит сделано с одним утверждением в коде. И это работает (на моей машине) в 160 мс для файла размером 100 МБ.

Что еще можно оптимизировать? Конечно, мы видим, что в нашем программном обеспечении 2 100 МБ std::string с. Какая трата. Окончательная оптимизация состояла бы в том, чтобы поместить 2 оператора для чтения файла и удаления пробелов и табуляций в начале строки в один оператор.

std::copy_if(std::istreambuf_iterator<char>(fileStream), {}, data.begin(), check);

Тогда у нас будет только 1 раз данные в памяти и устраните бессмысленность того, что мы читаем данные из файла, который нам не нужен. И прелесть этого в том, что при использовании современных элементов языка C ++ необходимы лишь незначительные модификации. Просто замените исходные итераторы:

Да, я знаю, что размер строки в конце слишком велик, но его можно легко установить на фактическое значение. Например, используя data.reserve (...) и back::inserter

...