Большая производительность синтаксического анализа 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
}
}