web-dev-qa-db-fra.com

Méthode la plus rapide pour trouver le nombre de lignes dans un texte (C ++)

J'ai besoin de lire le nombre de lignes dans un fichier avant d'effectuer certaines opérations sur ce fichier. Lorsque j'essaie de lire le fichier et d'incrémenter la variable line_count à chaque itération jusqu'à ce que j'atteigne eof. Ce n'était pas si rapide dans mon cas. J'ai utilisé à la fois ifstream et fgets. Ils étaient tous les deux lents. Existe-t-il un moyen hacky de le faire, qui est également utilisé par exemple par BSD, le noyau Linux ou berkeley db (peut être en utilisant des opérations au niveau du bit).

Comme je l'ai déjà dit, il y a des millions de lignes dans ce fichier et il s'agrandit, chaque ligne a environ 40 ou 50 caractères. J'utilise Linux.

Remarque: je suis sûr qu'il y aura des gens qui pourraient dire utiliser un idiot DB. Mais brièvement dans mon cas, je ne peux pas utiliser de base de données.

22
systemsfault

La seule façon de trouver le nombre de lignes est de lire le fichier entier et de compter le nombre de caractères de fin de ligne. Le moyen le plus rapide de le faire est probablement de lire le fichier entier dans un grand tampon avec une seule opération de lecture, puis de parcourir le tampon en comptant les caractères '\ n'.

Comme la taille actuelle de votre fichier semble être d'environ 60 Mo, ce n'est pas une option intéressante. Vous pouvez obtenir une certaine vitesse en ne lisant pas le fichier entier, mais en le lisant par morceaux., Disons de taille 1 Mo. Vous dites également qu'une base de données est hors de question, mais elle semble vraiment être la meilleure solution à long terme.

Edit: Je viens de lancer un petit benchmark à ce sujet et l'utilisation de l'approche tamponnée (taille du tampon 1024K) semble être un peu plus de deux fois plus rapide que la lecture d'une ligne à la fois avec getline (). Voici le code - mes tests ont été effectués avec g ++ en utilisant le niveau d'optimisation -O2:

#include <iostream>
#include <fstream>
#include <vector>
#include <ctime>
using namespace std;

unsigned int FileRead( istream & is, vector <char> & buff ) {
    is.read( &buff[0], buff.size() );
    return is.gcount();
}

unsigned int CountLines( const vector <char> & buff, int sz ) {
    int newlines = 0;
    const char * p = &buff[0];
    for ( int i = 0; i < sz; i++ ) {
        if ( p[i] == '\n' ) {
            newlines++;
        }
    }
    return newlines;
}

int main( int argc, char * argv[] ) {
    time_t now = time(0);
    if ( argc == 1  ) {
        cout << "lines\n";
        ifstream ifs( "lines.dat" );
        int n = 0;
        string s;
        while( getline( ifs, s ) ) {
            n++;
        }
        cout << n << endl;
    }
    else {
        cout << "buffer\n";
        const int SZ = 1024 * 1024;
        std::vector <char> buff( SZ );
        ifstream ifs( "lines.dat" );
        int n = 0;
        while( int cc = FileRead( ifs, buff ) ) {
            n += CountLines( buff, cc );
        }
        cout << n << endl;
    }
    cout << time(0) - now << endl;
}
17
anon

N'utilisez pas les chaînes stl C++ et getline (ou les fgets de C), juste des pointeurs bruts de style C et bloquez la lecture en segments de taille de page ou mappez le fichier.

Ensuite, scannez le bloc à la taille Word native de votre système (c'est-à-dire uint32_t Ou uint64_t) En utilisant l'un des algorithmes magiques 'SIMD dans un registre (SWAR) Opérations 'pour tester les octets dans Word. Un exemple est ici ; la boucle contenant le 0x0a0a0a0a0a0a0a0aLL recherche les sauts de ligne. (ce code atteint environ 5 cycles par octet d'entrée correspondant à une expression régulière sur chaque ligne d'un fichier)

Si le fichier ne fait que quelques dizaines ou une centaine de mégaoctets et qu'il continue de croître (c'est-à-dire que quelque chose continue d'écrire dessus), il y a de fortes chances pour que Linux l'ait mis en cache en mémoire, donc ce ne sera pas le disque IO limité, mais bande passante mémoire limitée.

Si le fichier est uniquement ajouté, vous pouvez également vous souvenir du nombre de lignes et de la longueur précédente, et commencer à partir de là.


Il a été souligné que vous pouviez utiliser mmap avec les algorithmes C++ stl et créer un foncteur à passer à std :: foreach. J'ai suggéré que vous ne devriez pas le faire non pas parce que vous ne pouvez pas le faire de cette façon, mais il n'y a aucun gain à écrire le code supplémentaire pour le faire. Ou vous pouvez utiliser l'itérateur mmappé de boost, qui gère tout cela pour vous; mais pour le problème, le code auquel j'ai lié a été écrit pour cela était beaucoup, beaucoup plus lent, et la question portait sur la vitesse et non sur le style.

9
Pete Kirkham

Vous avez écrit qu'il ne cesse de s'agrandir. Cela ressemble à un fichier journal ou quelque chose de similaire où de nouvelles lignes sont ajoutées mais les lignes existantes ne sont pas modifiées. Si tel est le cas, vous pouvez essayer une approche incrémentale.

Analyser à la fin du fichier. N'oubliez pas le nombre de lignes et le décalage de l'EOF. Lorsque le fichier s'agrandit fseek jusqu'à l'offset, analysez à EOF et mettez à jour le nombre de lignes et l'offset.

9

Il y a une différence entre les lignes de comptage et les séparateurs de lignes de comptage. Quelques pièges courants à surveiller s'il est important d'obtenir un nombre de lignes exact:

  1. Quel est l'encodage du fichier? Les solutions octet par octet fonctionneront pour ASCII et UTF-8, mais attention si vous avez UTF-16 ou un codage multi-octets qui ne garantit pas qu'un octet avec la valeur de un saut de ligne code nécessairement un saut de ligne.

  2. De nombreux fichiers texte n'ont pas de séparateur de ligne à la fin de la dernière ligne. Donc, si votre fichier dit "Hello, World!", Vous pourriez vous retrouver avec un compte de 0 au lieu de 1. Plutôt que de simplement compter les séparateurs de ligne, vous aurez besoin d'une machine d'état simple pour garder une trace.

  3. Certains fichiers très obscurs utilisent Unicode U+2028 LINE SEPARATOR (Ou même U+2029 PARAGRAPH SEPARATOR) Comme séparateurs de ligne au lieu du retour chariot et/ou du saut de ligne plus courant. Vous pouvez également faire attention à U+0085 NEXT LINE (NEL).

  4. Vous devrez déterminer si vous souhaitez compter certains autres caractères de contrôle comme des sauts de ligne. Par exemple, un U+000C FORM FEED Ou U+000B LINE TABULATION (Onglet vertical a.k.a.) devrait-il être considéré comme allant sur une nouvelle ligne?

  5. Les fichiers texte des anciennes versions de Mac OS (avant OS X) utilisent des retours chariot (U+000D) Plutôt que des sauts de ligne (U+000A) Pour séparer les lignes. Si vous lisez les octets bruts dans un tampon (par exemple, avec votre flux en mode binaire) et que vous les analysez, vous obtiendrez un compte de 0 sur ces fichiers. Vous ne pouvez pas compter à la fois les retours chariot et les sauts de ligne, car les fichiers PC terminent généralement une ligne avec les deux. Encore une fois, vous aurez besoin d'une simple machine à états. (Vous pouvez également lire le fichier en mode texte plutôt qu'en mode binaire. Les interfaces de texte normaliseront les séparateurs de ligne à '\n' Pour les fichiers conformes à la convention utilisée sur votre plate-forme. Si vous lisez des fichiers à partir d'autres vous reviendrez en mode binaire avec une machine à états.)

  6. Si vous avez déjà une très longue ligne dans le fichier, l'approche getline() peut lever une exception provoquant l'échec de votre compteur de ligne simple sur un petit nombre de fichiers. (Cela est particulièrement vrai si vous lisez un ancien fichier Mac sur une plate-forme non Mac, ce qui fait que getline() voit le fichier entier comme une seule ligne gigantesque.) En lisant des morceaux dans un tampon de taille fixe et en utilisant une machine d'état, vous pouvez le rendre à l'épreuve des balles.

Le code dans la réponse acceptée souffre de la plupart de ces pièges. Faites-le juste avant de le faire rapidement.

6
Adrian McCarthy

N'oubliez pas que tous les flux sont mis en mémoire tampon. Donc, en fait, ils lisent en fait des morceaux, vous n'avez donc pas à recréer cette fonctionnalité. Il vous suffit donc d'analyser le tampon. N'utilisez pas getline () car cela vous obligera à dimensionner une chaîne. Donc, je voudrais simplement utiliser les itérateurs STL :: count et stream.

#include <iostream>
#include <fstream>
#include <iterator>
#include <algorithm>


struct TestEOL
{
    bool operator()(char c)
    {
        last    = c;
        return last == '\n';
    }
    char    last;
};

int main()
{
    std::fstream  file("Plop.txt");

    TestEOL       test;
    std::size_t   count   = std::count_if(std::istreambuf_iterator<char>(file),
                                          std::istreambuf_iterator<char>(),
                                          test);

    if (test.last != '\n')  // If the last character checked is not '\n'
    {                       // then the last line in the file has not been 
        ++count;            // counted. So increement the count so we count
    }                       // the last line even if it is not '\n' terminated.
}
4
Martin York

Ce n'est pas lent à cause de votre algorithme, c'est lent parce que IO sont lentes. Je suppose que vous utilisez un algorithme simple O(n) qui est simplement en train de parcourir le fichier séquentiellement. Dans ce cas, il existe un algorithme no plus rapide qui peut optimiser votre programme.

Cependant, j'ai dit qu'il n'y a pas d'algorithme plus rapide, mais il y a un mécanisme plus rapide qui s'appelle "Fichier mappé en mémoire", Il y a quelques inconvénients pour les fichiers mappés et cela pourrait ne pas convenir à votre cas, Donc vous Je vais devoir lire à ce sujet et comprendre par vous-même.

Les fichiers mappés en mémoire ne vous permettront pas d'implémenter un algorithme mieux que O(n) mais cela mai réduira IO temps d'accès .

3
user88637

Vous ne pouvez obtenir une réponse définitive qu'en analysant l'intégralité du fichier à la recherche de caractères de nouvelle ligne. Il n'y a aucun moyen de contourner cela.

Cependant, il y a quelques possibilités que vous voudrez peut-être envisager.

1/Si vous utilisez une boucle simpliste, lire un caractère à la fois en vérifiant les nouvelles lignes, ne le faites pas. Même si les E/S peuvent être mises en mémoire tampon, les appels de fonction eux-mêmes sont coûteux, en termes de temps.

Une meilleure option consiste à lire de gros morceaux du fichier (disons 5M) en mémoire avec une seule opération d'E/S, puis à traiter cela. Vous n'avez probablement pas à vous soucier trop des instructions d'assemblage spéciales, car la bibliothèque d'exécution C sera de toute façon optimisée - une simple strchr() devrait le faire.

2/Si vous dites que la longueur de ligne générale est d'environ 40 à 50 caractères et que vous n'avez pas besoin d'un nombre de lignes exact, saisissez simplement la taille du fichier et divisez par 45 (ou la moyenne que vous juger à utiliser).

3/Si cela ressemble à un fichier journal et que vous n'avez pas avoir pour le conserver dans un fichier (peut nécessiter une retouche sur d'autres parties du système), envisagez de diviser le fichier périodiquement.

Par exemple, lorsqu'il atteint 5M, déplacez-le (par exemple, x.log) à un nom de fichier daté (par exemple, x_20090101_1022.log) et déterminez le nombre de lignes à ce stade (en le stockant dans x_20090101_1022.count, puis lancez une nouvelle x.log fichier journal. Les caractéristiques des fichiers journaux signifient que cette section datée qui a été créée ne changera jamais, vous n'aurez donc jamais à recalculer le nombre de lignes.

Pour traiter le "fichier" du journal, il vous suffit de cat x_*.log via un tube de traitement plutôt que cat x.log. Pour obtenir le nombre de lignes du "fichier", faites un wc -l sur le x.log actuel (relativement rapide) et l'ajouter à la somme de toutes les valeurs dans le x_*.count des dossiers.

3
paxdiablo

La chose qui prend du temps est de charger plus de 40 Mo dans la mémoire. Le moyen le plus rapide de le faire est de le mapper en mémoire ou de le charger en une seule fois dans un grand tampon. Une fois que vous l'avez en mémoire, d'une manière ou d'une autre, une boucle parcourant les données à la recherche de \n les caractères sont presque instantanés, quelle que soit la façon dont ils sont implémentés.

Donc, vraiment, l'astuce la plus importante est de charger le fichier en mémoire aussi rapidement que possible. Et le moyen le plus rapide de le faire est de le faire en une seule opération.

Sinon, de nombreuses astuces peuvent exister pour accélérer l'algorithme. Si des lignes sont uniquement ajoutées, jamais modifiées ou supprimées, et si vous lisez le fichier à plusieurs reprises, vous pouvez mettre en cache les lignes lues précédemment, et la prochaine fois que vous devrez lire le fichier, ne lisez que les lignes nouvellement ajoutées.

Ou peut-être pouvez-vous conserver un fichier d'index distinct indiquant l'emplacement des caractères "\ n" connus, afin que ces parties du fichier puissent être ignorées.

La lecture de grandes quantités de données depuis le disque dur est lente. Il n'y a aucun moyen de contourner cela.

1
jalf