web-dev-qa-db-fra.com

Lecture rapide de fichiers texte en c ++

J'écris actuellement un programme en c ++ qui comprend la lecture de nombreux fichiers texte volumineux. Chacune comporte ~ 400 000 lignes avec, dans les cas extrêmes, 4 000 caractères ou plus par ligne. Juste pour tester, j'ai lu l'un des fichiers utilisant ifstream et l'implémentation proposée par cplusplus.com. Cela a pris environ 60 secondes, ce qui est beaucoup trop long. Maintenant, je me demandais s'il existe un moyen simple d'améliorer la vitesse de lecture.

edit: Le code que j'utilise est plus ou moins le suivant:

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

edit 2: Le fichier que j'ai lu ne fait que 82 Mo de taille. J'ai principalement dit qu'il pourrait atteindre 4 000 parce que je pensais qu'il serait peut-être nécessaire de savoir pour pouvoir mettre en mémoire tampon.

edit 3: Merci à tous pour vos réponses, mais il semble qu'il n'y ait pas beaucoup de place pour l'amélioration compte tenu de mon problème. Je dois utiliser readline, car je veux compter le nombre de lignes. L'instanciation de ifstream en tant que binaire n'a pas rendu la lecture plus rapide non plus. Je vais essayer de le mettre autant en parallèle que possible, cela devrait au moins fonctionner.

edit 4: Donc, apparemment, il y a des choses que je peux faire. Un grand merci à toi de m'avoir consacré tant de temps, j'apprécie beaucoup! =)

58
Arne

Mises à jour: Assurez-vous de vérifier les mises à jour (surprenantes) en dessous de la réponse initiale


Les fichiers mappés en mémoire m'ont bien servi1:

#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";
}

Cela devrait être plutôt rapide.

Mise à jour

Au cas où cela vous aiderait à tester cette approche, voici une version tilisant mmap directement au lieu d'utiliser Boost: voir en direct sur Colir

#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;
}

Mise à jour

La dernière performance que j’ai pu extraire a été trouvée en regardant la source de GNU coreutils wc. _. À ma grande surprise, en utilisant le code suivant (très simplifié) adapté de wc s'exécute dans environ 84% du temps pris avec le fichier mappé en mémoire ci-dessus:

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 voir par exemple le repère ici: Comment analyser rapidement les flottants séparés par des espaces en C++?

71
sehe

4000 * 400 000 = 1,6 Go si votre disque dur n'est pas un disque SSD, vous obtiendrez probablement une lecture séquentielle d'environ 100 Mo/s. Cela fait 16 secondes seulement en I/O.

Puisque vous n’expliquez pas le code spécifique que vous utilisez ou la manière dont vous avez besoin d’analyser ces fichiers (avez-vous besoin de le lire ligne par ligne, le système dispose-t-il de beaucoup de RAM pourriez-vous lire le fichier entier dans un grand RAM tampon puis l’analyser?) Il n’ya rien que vous puissiez faire pour accélérer le processus.

Les fichiers mappés en mémoire n'apportent aucune amélioration des performances lors de la lecture séquentielle d'un fichier. Peut-être qu'analyser manuellement de gros morceaux pour de nouvelles lignes plutôt que d'utiliser "getline" offrirait une amélioration.

EDIT Après avoir appris (merci @sehe). Voici la solution mappée en mémoire que j'utiliserais probablement.

#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;
}
10
Louis Ricci

Neil Kirk, malheureusement, je ne peux pas répondre à votre commentaire (réputation insuffisante), mais j’ai fait un test de performance sur ifstream et une performance, lire un fichier texte ligne par ligne est exactement la même chose.

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

Cela prend 1426ms sur un fichier de 106 Mo.

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

Cela prend 1433ms sur le même fichier.

Le code suivant est plus rapide à la place:

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

Cela prend 884ms sur le même fichier. C’est un peu délicat, car vous devez définir la taille maximale de votre tampon (c’est-à-dire la longueur maximale pour chaque ligne du fichier d’entrée).

4
user2434119

Devez-vous lire tous les fichiers en même temps? (au début de votre application par exemple)

Si vous le faites, envisagez de mettre en parallèle l'opération.

Dans les deux cas, envisagez l’utilisation de flux binaires, ou sans décalage lecture pour les blocs de données.

2
utnapistim

Utilisation Random file access Ou utiliser binary mode. pour séquentiel, c'est grand mais cela dépend quand même de ce que vous lisez.

1
Shumail

En tant qu'expert en programmation compétitive, je peux vous dire: du moins pour des choses simples comme l'analyse des entiers, le coût principal en C est le verrouillage des flux de fichiers (ce qui est fait par défaut pour le multi-threading). Utilisez les versions unlocked_stdio À la place (fgetc_unlocked(), fread_unlocked()). Pour C++, la tradition est d'utiliser std::ios::sync_with_stdio(false) mais je ne sais pas si c'est aussi rapide que unlocked_stdio.

Pour référence, voici mon code d'analyse entier standard. C'est un beaucoup plus rapide que scanf, comme je l'ai dit principalement en raison de ne pas verrouiller le flux. Pour moi, c’était aussi rapide que la meilleure version à mémoire de forme mmap ou tamponnée personnalisée que j’avais utilisée auparavant, sans la dette de maintenance insensée.

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

(Remarque: celui-ci ne fonctionne que s'il y a précisément un caractère non numérique entre deux entiers).

Et bien sûr, évitez si possible l'allocation de mémoire ...

1
Jo So