LINUX.ORG.RU

C++ медленное чтение файла с помощью std::ifstream

 


2

5

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

std::ifstream infile(fname);
 
while (std::getline(infile, line)) {
    // обрабатываем строку
}

Все это дело работает достаточно медленно. Как показывает профилирование - основная часть времени - std::getline, если убрать всю обработку и оставить только чтение файла - время выполнения программы практически не меняется. Если заменить ifstream и std::getline на fopen и fgets - то скорость возрастает примерно в 10 раз. Подскажите, как правильно готовить ifstream, чтобы скорость была сопоставима с fgets.

C vs C++? давай, давно не было

dib2 ★★★★★ ()

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

Lavos ★★★★★ ()

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

char buf[1024];
std::ifstream infile(fname);
infile.rdbuf()->pubsetbuf(buf, sizeof(buf));

archie ()
Ответ на: комментарий от archie

Линукс при первом чтении помещает файл в дисковый кэш, поэтому обращение к диску в случае небольшого файла происходит 1 раз (если происходит, если файл уже не в кеше).

yoghurt ★★★★★ ()
Ответ на: комментарий от placement_new

нет, просто я так себе плюсовик, но думаю, что проблема такая не только у меня есть и ее как-то более элегантно решают, чем отказом от потоков

Goganchic ★★ ()
Ответ на: комментарий от Mixail

std::ios::sync_with_stdio(false); - втыкал, но у меня ж не стандартные потоки (cin, cout) - поэтому эффекта никакого нет

Goganchic ★★ ()
Ответ на: комментарий от yoghurt

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

Сискол тоже стоит не дёшево, если у стрима слишком маленький юзерспейсный буфер, то тормозить будет не меньше чем от простоя при IO.

mashina ★★★★★ ()

А зачем тебе вообще в своём коде это делать? Запускай grep через popen(), пусть он за тебя парсит, юниксвейно будет.

Harald ★★★★★ ()

Если заменить ifstream и std::getline на fopen и fgets - то скорость возрастает примерно в 10 раз. Подскажите, как правильно готовить ifstream, чтобы скорость была сопоставима с fgets.

Минимальные примеры есть? Какие параметры оптимизации задавались?

JackYF ★★★★ ()
Ответ на: комментарий от Reset

Размер буфера влияет только на число системных вызовов.

О чем и спич. Чем больше системных вызовов, тем больше тормоза. Если файл большой и целиком не помещается в кэше, то более частые сисколлы будут приводить к более частым обращениям к диску. За примерами далеко ходить не надо - скорость того же dd весьма сильно зависит от параметра bs. И даже если файл полностью кэширован, то сисколлы не бесплатны все равно, как заметили выше.

archie ()
Ответ на: комментарий от Goganchic

так колхоз же

Для оптимизации боттлнеков небольшие Си-стайл вставки вполне православны.

А вообще потоки C++ широко известны низкой производительностью, тут ничего странного нет. Удивляет только разница в 10 раз. У меня fgets примерно на 25% быстрее std::getline на 4 Гб кешированном текстовом файле. Но уж точно не на порядок.

archie ()

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

bogus_result ()

Если заменить ifstream и std::getline на fopen и fgets - то скорость возрастает примерно в 10 раз. Подскажите, как правильно готовить ifstream, чтобы скорость была сопоставима с fgets.

лично я этот вопрос решил просто: я использую fread(3). Да, в C++ коде. Да, в своих велосипедных классах.

ИМХО пихать в STL работу с файлами было не самой лучшей идеей.

emulek ()

да, в sed используется следующий алгоритм:

1. выделяем буфер в n=50 байт

2. читаем n байтов

3. если строка прочиталась целиком, обрабатываем её, и сдвигаем необработанный хвост в начало, и пишем в буфер с конца хвоста

4. иначе выделяем буфер размером в 2*n→n, и перебрасываем туда старый буфер, и на п2

Ну это конечно упрощённо.

Читать можно read(2) или fread(3) по вкусу(разницы по скорости почти нет, но fread(3) удобнее).

emulek ()
Ответ на: комментарий от bogus_result

Зачем разбивать весь файл на строки, когда надо найти соответствие регекспу

а как быть если в регекспе есть

^ — начало строки

$ — конец строки

??

emulek ()
Ответ на: комментарий от emulek

Так я же и говорю, что не знаю насколько это реализуемо в с++.

Тут смотря какой смысл в ваших регекспах имеют символы ^ и $.

Возьмём к примеру строку abe\nabc\nbec\n

Одно дело, когда регекспу ^a.*c$ соответствует вся эта строка, другое дело, когда только abc.

bogus_result ()
Ответ на: комментарий от bogus_result

Так я же и говорю, что не знаю насколько это реализуемо в с++.

при чём тут C++?

Возьмём к примеру строку abe\nabc\nbec\n

это не строка в том смысле, который вкладывает в этот термин ТС.

Одно дело, когда регекспу ^a.*c$ соответствует вся эта строка, другое дело, когда только abc.

в sed есть для этого multiline mode и «\`» с «\'»

`M' `m' The `M' modifier to regular-expression matching is a GNU `sed' extension which directs GNU `sed' to match the regular expression in `multi-line' mode. The modifier causes `^' and `$' to match respectively (in addition to the normal behavior) the empty string after a newline, and the empty string before a newline. There are special character sequences (`\`' and `\") which always match the beginning or the end of the buffer. In addition, the period character does not match a new-line character in multi-line mode.

в других фронтендах к регекспам тоже есть нечто подобное.

emulek ()
Ответ на: комментарий от emulek

Ну если в других фронтендах к регекспам подобное есть, в том числе и для c++ (ТС вроде решает задачу именно на нем), тогда вообще проблем с символами ^ и $ в моем алгоритме не возникнет.

Парсим буфер, содержащий как обычные символы так и символы перевода строки, находим соответствие регекспу, ищем начало и конец строки, содержащей это соответствие, и выкусываем.

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

bogus_result ()
Ответ на: комментарий от bogus_result

в том числе и для c++

я не знаю, что там в STL, говорят УГ.

Сам я юзаю glibc, но там вроде-бы такого нету, надо самому делать. Т.е. \n там — обычный символ (точнее обычный пробельный символ, матчится с [[:space:]])

emulek ()
Ответ на: комментарий от JackYF

с getline - пример в теме, с fgets:

char buf[512];
FILE *f = fopen(fname, "r");

while (fgets(buf, 512, f)) {
  // обрабатываем строку
}

параметров оптимизации при компиляции не задавал

Goganchic ★★ ()
Ответ на: комментарий от Goganchic

параметров оптимизации при компиляции не задавал

STL — библиотека шаблонов. std::getline — шаблонная функция, компилирующаеся вместе с твоей программой. Иными словами, ты сравниваешь оптимизированный fgets с неоптимизированным std::getline.

Задай -O2 на оба варианта и сравни.

JackYF ★★★★ ()
Ответ на: комментарий от JackYF

без оптимизаций: fgets: 34 секунды, std::getline - 7 минут 39 секунд, итого разница в 15 раз -O2: fgets - 33 секунды, std::getline - 2 минуты, итого разница в 4 раза

Вывод: оптимизации сократили разрыв, но он все равно очень и очень большой (я надеюсь на какую-нибудь магию, чтобы отставание было, ну скаем в 10% - 15% но не в 4 раза).

P.s. я всегда считал что шаблонные функции - это когда указываешь в треугольных скобках тип, а в std::getline - вроде ничего не указываю, объясни как более опытный товарищ, в чем я ошибаюсь?

Goganchic ★★ ()
Ответ на: комментарий от archie

попробовал - не помогло (сделал то что выше написано, а дальше использовал std::getline как и было раньше), время выполнения примерно такое же как и без изменения размера буфера: без оптимизаций 7 минут, с оптимизациями - 2 минуты, дальнейшее увеличение буфера до 4кб - не дает никакого эффекта

может быть я что-то делаю не так?

Goganchic ★★ ()
Ответ на: комментарий от four_str_sam

нет, там этот самый результат (4-х кратная разница) был до магического std::ios::sync_with_stdio(false), а после этого - скорость работы scanf и cin была примерно одинаковой

у меня же разница лишь в том, что я читаю не с cin а из файла

Goganchic ★★ ()
Ответ на: комментарий от Goganchic

А какой файл ты читаешь? В статье в каждой строке по целому числу. Т.е. для getline буфер string может с первой-второй попытки выделиться нужного размера. В случае длинных строк перевыделений памяти может быть достаточно много. Ну и вообще, в статье cin сразу пишет в int без дополнительных затрат на динамическую память.

four_str_sam ()
Ответ на: комментарий от four_str_sam

текстовый файл с логами читаю, может в этом дело, да

Goganchic ★★ ()
Ответ на: комментарий от Goganchic

я всегда считал что шаблонные функции - это когда указываешь в треугольных скобках тип, а в std::getline - вроде ничего не указываю, объясни как более опытный товарищ, в чем я ошибаюсь?

Шаблонная функция может «вывести» тип(ы) автоматически, по тем параметрам, которые ей передали. Вот: http://en.cppreference.com/w/cpp/string/basic_string/getline .

JackYF ★★★★ ()
Ответ на: комментарий от Goganchic

без оптимизаций: fgets: 34 секунды, std::getline - 7 минут 39 секунд, итого разница в 15 раз -O2: fgets - 33 секунды, std::getline - 2 минуты, итого разница в 4 раза

Ты меня заинтриговал. Вот мои замеры (файл текстовый, 6+ миллионов строк, 1+ GiB):

jackyf@debian-w500:~/code/smalltests/fgets-vs-stdgetline.cpp$ ./run.sh 
+ gcc -O2 fgets.c -o fgets.e
+ g++ -O2 -std=c++11 stdgetline.cpp -o stdgetline.e
+ ./fgets.e
2727561
real 3.72
user 2.96
sys 0.53
+ ./stdgetline.e
2727561
real 3.83
user 3.25
sys 0.50
jackyf@debian-w500:~/code/smalltests/fgets-vs-stdgetline.cpp$ cat run.sh
#!/bin/bash

INPUT=~/code/cupt/cupt.git/build/release/cpp/debug.log

set -x
# set -e

gcc -O2 fgets.c -o fgets.e
g++ -O2 -std=c++11 stdgetline.cpp -o stdgetline.e

time -p ./fgets.e < $INPUT
time -p ./stdgetline.e < $INPUT
jackyf@debian-w500:~/code/smalltests/fgets-vs-stdgetline.cpp$ cat fgets.c
#include <stdio.h>

int main()
{
        char buf[8192];
        long r = 0;

        while (fgets(buf, sizeof(buf), stdin))
        {
                const char*p = buf;

                long a = 0;
                while (*p)
                {
                        if (*p == 'w')
                        {
                                ++a;
                                r += a;
                        }
                        ++p;
                }
        }

        printf("%lu\n", r);
}

jackyf@debian-w500:~/code/smalltests/fgets-vs-stdgetline.cpp$ cat stdgetline.cpp 
#include <iostream>

int main()
{
        std::ios::sync_with_stdio(false);

        long r = 0;

        std::string line;
        while (std::getline(std::cin, line))
        {
                long a = 0;
                for (char c: line)
                {
                        if (c == 'w')
                        {
                                ++a;
                                r += a;
                        }
                }
        }

        std::cout << r << std::endl;
}

Разница в пределах 10%.

JackYF ★★★★ ()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.