Быстрое чтение текстовых файлов в C++

В настоящее время я пишу программу на C++, которая включает чтение большого количества текстовых файлов. У каждой ~400.000 строк, в крайнем случае 4000 или более символов в строке. Просто для тестирования я прочитал один из файлов, используя ifstream и реализацию, предлагаемую cplusplus.com. Это заняло около 60 секунд, что слишком долго. Теперь мне было интересно, есть ли простой способ улучшить скорость чтения?

редактировать: код, который я использую, более или менее это:

string tmpString;
ifstream txtFile(path);
if(txtFile.is_open())
{
    while(txtFile.good())
    {
        m_numLines++;
        getline(txtFile, tmpString);
    }
    txtFile.close();
}

редактировать 2: файл, который я читаю, имеет размер всего 82 МБ. Я в основном говорил, что она может достигать 4000, потому что я думал, что это может быть необходимо для буферизации.

редактировать 3: Спасибо всем за ваши ответы, но, похоже, не так много места для улучшения, учитывая мою проблему. Я должен использовать readline, так как я хочу посчитать количество строк. Реализация ifstream как двоичного также не делала чтения быстрее. Я постараюсь распараллелить это как можно больше, чтобы это работало по крайней мере.

редактировать 4: Так что, очевидно, я могу кое-что сделать. Большое спасибо sehe за то, что вы уделили мне столько времени, я это очень ценю! знак равно

6 ответов

Решение

Обновления: не забудьте проверить (удивительно) обновления ниже первоначального ответа


Файлы с отображенной памятью хорошо послужили мне1:

#include <boost/iostreams/device/mapped_file.hpp> // for mmap
#include <algorithm>  // for std::find
#include <iostream>   // for std::cout
#include <cstring>

int main()
{
    boost::iostreams::mapped_file mmap("input.txt", boost::iostreams::mapped_file::readonly);
    auto f = mmap.const_data();
    auto l = f + mmap.size();

    uintmax_t m_numLines = 0;
    while (f && f!=l)
        if ((f = static_cast<const char*>(memchr(f, '\n', l-f))))
            m_numLines++, f++;

    std::cout << "m_numLines = " << m_numLines << "\n";
}

Это должно быть довольно быстро.

Обновить

В случае, если это поможет вам проверить этот подход, вот версия, использующаяmmap напрямую вместо Boost: посмотрите вживую на Coliru

#include <algorithm>
#include <iostream>
#include <cstring>

// for mmap:
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>

const char* map_file(const char* fname, size_t& length);

int main()
{
    size_t length;
    auto f = map_file("test.cpp", length);
    auto l = f + length;

    uintmax_t m_numLines = 0;
    while (f && f!=l)
        if ((f = static_cast<const char*>(memchr(f, '\n', l-f))))
            m_numLines++, f++;

    std::cout << "m_numLines = " << m_numLines << "\n";
}

void handle_error(const char* msg) {
    perror(msg); 
    exit(255);
}

const char* map_file(const char* fname, size_t& length)
{
    int fd = open(fname, O_RDONLY);
    if (fd == -1)
        handle_error("open");

    // obtain file size
    struct stat sb;
    if (fstat(fd, &sb) == -1)
        handle_error("fstat");

    length = sb.st_size;

    const char* addr = static_cast<const char*>(mmap(NULL, length, PROT_READ, MAP_PRIVATE, fd, 0u));
    if (addr == MAP_FAILED)
        handle_error("mmap");

    // TODO close fd at some point in time, call munmap(...)
    return addr;
}

Обновить

Последняя часть производительности, которую я смог вытащить из этого, я нашел, посмотрев на источник GNU coreutils wc, К моему удивлению, используя следующий (очень упрощенный) код, адаптированный из wc работает примерно в 84% времени, которое занимает файл с отображенной памятью выше:

static uintmax_t wc(char const *fname)
{
    static const auto BUFFER_SIZE = 16*1024;
    int fd = open(fname, O_RDONLY);
    if(fd == -1)
        handle_error("open");

    /* Advise the kernel of our access pattern.  */
    posix_fadvise(fd, 0, 0, 1);  // FDADVICE_SEQUENTIAL

    char buf[BUFFER_SIZE + 1];
    uintmax_t lines = 0;

    while(size_t bytes_read = read(fd, buf, BUFFER_SIZE))
    {
        if(bytes_read == (size_t)-1)
            handle_error("read failed");
        if (!bytes_read)
            break;

        for(char *p = buf; (p = (char*) memchr(p, '\n', (buf + bytes_read) - p)); ++p)
            ++lines;
    }

    return lines;
}

1 см., Например, тест здесь: Как быстро проанализировать разделенные пробелами числа в C++?

4000 * 400 000 = 1,6 ГБ, если ваш жесткий диск не является твердотельным накопителем, вы, вероятно, получаете последовательное чтение ~100 МБ / с. Это 16 секунд только в I/O.

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

Файлы с отображением в памяти не обеспечат никакого улучшения производительности при последовательном чтении файла. Возможно, ручной синтаксический анализ больших кусков для новых строк вместо использования "getline" даст улучшение.

РЕДАКТИРОВАТЬ После некоторого обучения (спасибо @sehe). Вот решение с отображением памяти, которое я бы, вероятно, использовал.

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <errno.h>

int main() {
    char* fName = "big.txt";
    //
    struct stat sb;
    long cntr = 0;
    int fd, lineLen;
    char *data;
    char *line;
    // map the file
    fd = open(fName, O_RDONLY);
    fstat(fd, &sb);
    //// int pageSize;
    //// pageSize = getpagesize();
    //// data = mmap((caddr_t)0, pageSize, PROT_READ, MAP_PRIVATE, fd, pageSize);
    data = mmap((caddr_t)0, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    line = data;
    // get lines
    while(cntr < sb.st_size) {
        lineLen = 0;
        line = data;
        // find the next line
        while(*data != '\n' && cntr < sb.st_size) {
            data++;
            cntr++;
            lineLen++;
        }
        /***** PROCESS LINE *****/
        // ... processLine(line, lineLen);
    }
    return 0;
}

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

std::stringstream stream;
std::string line;
while(std::getline(stream, line)) {
}

Это занимает 1426мс в файле размером 106 МБ.

std::ifstream stream;
std::string line;
while(ifstream.good()) {
    getline(stream, line);
}

Это занимает 1433 мс на том же файле.

Следующий код быстрее вместо этого:

const int MAX_LENGTH = 524288;
char* line = new char[MAX_LENGTH];
while (iStream.getline(line, MAX_LENGTH) && strlen(line) > 0) {
}

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

Как человек с небольшим опытом работы в конкурентном программировании, я могу вам сказать: по крайней мере для простых вещей, таких как целочисленный анализ, основной ценой в C является блокировка потоков файлов (что по умолчанию выполняется для многопоточности). Использовать unlocked_stdio версии вместо (fgetc_unlocked(), fread_unlocked()). Для C++ распространенным знанием является использование std::ios::sync_with_stdio(false) но я не знаю, так ли это быстро, как unlocked_stdio,

Для справки вот мой стандартный код целочисленного анализа. Это намного быстрее, чем scanf, как я сказал, в основном из-за отсутствия блокировки потока. Для меня это было так же быстро, как лучшая ручная кодировка mmap или пользовательских версий с буферизацией, которые я использовал ранее, без безумного долга за обслуживание.

int readint(void)
{
        int n, c;
        n = getchar_unlocked() - '0';
        while ((c = getchar_unlocked()) > ' ')
                n = 10*n + c-'0';
        return n;
}

(Примечание: этот работает только в том случае, если между любыми двумя целыми числами находится ровно один нецифровый символ).

И конечно избегайте выделения памяти, если это возможно...

Вы должны читать все файлы одновременно? (в начале вашего приложения, например)

Если вы это сделаете, подумайте о распараллеливании операции.

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

Использование Random file access или использовать binary mode, для последовательного, это большой, но все же это зависит от того, что вы читаете.

Другие вопросы по тегам