Большая производительность синтаксического анализа csv csv

У меня есть большой файл csv (25 мб), который представляет собой симметричный граф (около 18kX18k). При анализе его на массив векторов я проанализировал код (с VS2012 ANALYZER), и он показывает, что проблема с эффективностью анализа (около 19 секунд) возникает при чтении каждого символа (getline:: basic_string:: operator + =) как показано на рисунке ниже: enter image description here

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

В моем коде используется только библиотека STL:

int allColumns = initFirstRow(file,secondRow);
// secondRow has initialized with one value
int column = 1; // dont forget, first column is 0
VertexSet* rows = new VertexSet[allColumns];
rows[1] = secondRow;
string vertexString;
long double vertexDouble;
for (int row = 1; row < allColumns; row ++){
    // dont do the last row
    for (; column < allColumns; column++){
        //dont do the last column
        getline(file,vertexString,','); 
        vertexDouble = stold(vertexString);
        if (vertexDouble > _TH){
            rows[row].add(column);
        }
    }
    // do the last in the column
    getline(file,vertexString);
    vertexDouble = stold(vertexString);
    if (vertexDouble > _TH){
        rows[row].add(++column);
    }
    column = 0;
}
initLastRow(file,rows[allColumns-1],allColumns);

init первая и последняя строка в основном делает то же самое, что и цикл выше, но initFirstRow также подсчитывает количество столбцов.

VertexSet - это в основном вектор индексов (int). Каждая прочитанная вершина (разделенная символом ',') имеет длину не более 7 символов (значения находятся между -1 и 1).

Ответы

Ответ 1

В 25 мегабайтах я собираюсь предположить, что ваш файл сгенерирован машиной. Таким образом, вам (возможно) не нужно беспокоиться о таких вещах, как проверка формата (например, каждая запятая на месте).

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

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

#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <time.h>
#include <stdlib.h>
#include <locale>
#include <sstream>
#include <algorithm>
#include <iterator>

class my_ctype : public std::ctype<char> {
    std::vector<mask> my_table;
public:
    my_ctype(size_t refs=0):
        my_table(table_size),
        std::ctype<char>(my_table.data(), false, refs) 
    {
        std::copy_n(classic_table(), table_size, my_table.data());
        my_table[',']=(mask)space;
    }
};

template <class T>
class converter {
    std::stringstream buffer;
    my_ctype *m;
    std::locale l;
public:
    converter() : m(new my_ctype), l(std::locale::classic(), m) { buffer.imbue(l); }

    std::vector<T> operator()(std::string const &in) {
        buffer.clear();
        buffer<<in;
        return std::vector<T> {std::istream_iterator<T>(buffer),
            std::istream_iterator<T>()};        
    }
};

int main() {
    std::ifstream in("somefile.csv");
    std::vector<std::vector<double>> numbers;

    std::string line;
    converter<double> cvt;

    clock_t start=clock();
    while (std::getline(in, line))
        numbers.push_back(cvt(line));
    clock_t stop=clock();
    std::cout<<double(stop-start)/CLOCKS_PER_SEC << " seconds\n";
}

Чтобы проверить это, я сгенерировал файл CSV размером 1,8 К х 1,8 Кбайт псевдослучайных удвоений, например:

#include <iostream>
#include <stdlib.h>

int main() {
    for (int i=0; i<1800; i++) {
        for (int j=0; j<1800; j++)
            std::cout<<rand()/double(RAND_MAX)<<",";
        std::cout << "\n";
    }
}

Это создало файл размером около 27 мегабайт. После компиляции кода чтения/разбора с помощью gcc (g++ -O2 trash9.cpp), быстрый тест на моем ноутбуке показал, что он работает примерно от 0,18 до 0,19 секунды. Кажется, он никогда не использовал (даже близко) все одно ядро ​​процессора, указывая на то, что он связан с I/O, поэтому на настольном/серверном компьютере (с более быстрым жестким диском) я ожидаю, что он будет работать быстрее.

Ответ 2

Неэффективность здесь заключается в реализации Microsoft std::getline, которая используется в двух местах в коде. Ключевыми проблемами с этим являются:

  • Он считывает из потока один символ за раз
  • Он добавляет к строке один символ за раз

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

Я написал больше о неэффективности std::getline здесь.

Реализация GNU std::getline, то есть версия в libstdС++, намного лучше.

К сожалению, если вы хотите, чтобы ваша программа была быстрой, и вы создаете ее с помощью Visual С++, вам придется использовать функции более низкого уровня, чем std::getline.

Ответ 3

Отладочная библиотека времени выполнения в VS очень медленна, потому что она выполняет множество проверок отладки (из-за не связанных с ними доступов и т.д.) и вызывает множество очень маленьких функций, которые не встроены при компиляции в Debug.

Запуск вашей программы в выпуске должен удалить все эти накладные расходы.

Моя ставка на следующее узкое место - это распределение строк.

Ответ 4

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

Ответ 5

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

Сначала получите данные с помощью boost mmap. Затем вы можете использовать boost-поток, чтобы быстрее выполнять обработку на const char *, который возвращает mmap. Что-то вроде этого: (многопоточность отличается в зависимости от вашей реализации, поэтому я исключил эту часть)

#include <boost/iostreams/device/mapped_file.hpp>
#include <boost/thread/thread.hpp>
#include <boost/lockfree/queue.hpp>

foo(string path)
{
    boost::iostreams::mapped_file mmap(path,boost::iostreams::mapped_file::readonly);
    auto chars = mmap.const_data(); // set data to char array
    auto eofile = chars + mmap.size(); // used to detect end of file
    string next = ""; // used to read in chars
    vector<double> data; // store the data
    for (; chars && chars != eofile; chars++) {
        if (chars[0] == ',' || chars[0] == '\n') { // end of value
            data.push_back(atof(next.c_str())); // add value
            next = ""; // clear
        }
        else
            next += chars[0]; // add to read string
    }
}