web-dev-qa-db-fra.com

Comment analyser rapidement des flottants séparés par des espaces en C ++?

J'ai un fichier avec des millions de lignes, chaque ligne a 3 flottants séparés par des espaces. La lecture du fichier prend beaucoup de temps, j'ai donc essayé de les lire en utilisant des fichiers mappés en mémoire uniquement pour découvrir que le problème ne venait pas de la vitesse de IO mais de la vitesse de la analyse.

Mon analyse actuelle consiste à prendre le flux (appelé fichier) et à effectuer les opérations suivantes

float x,y,z;
file >> x >> y >> z;

Quelqu'un dans Stack Overflow a recommandé d'utiliser Boost.Spirit mais je n'ai trouvé aucun tutoriel simple pour expliquer comment l'utiliser.

J'essaie de trouver un moyen simple et efficace d'analyser une ligne qui ressemble à ceci:

"134.32 3545.87 3425"

J'apprécierai vraiment de l'aide. Je voulais utiliser strtok pour le diviser, mais je ne sais pas comment convertir des chaînes en flottants, et je ne suis pas sûr que ce soit la meilleure façon.

Cela ne me dérange pas si la solution sera Boost ou non. Cela ne me dérange pas si ce ne sera jamais la solution la plus efficace, mais je suis sûr qu'il est possible de doubler la vitesse.

Merci d'avance.

34
OopsUser

Si la conversion est le col de la bouteille (ce qui est tout à fait possible), vous devez commencer par utiliser les différentes possibilités de la norme. Logiquement, on s'attendrait à ce qu'ils soient très proches, mais pratiquement, ils ne le sont pas toujours:

  • Vous avez déjà déterminé que std::ifstream Est trop lent.

  • La conversion de vos données mappées en mémoire en un std::istringstream Est presque certainement pas une bonne solution; vous devrez d'abord créer une chaîne, qui copiera toutes les données.

  • Écrire votre propre streambuf pour lire directement depuis la mémoire, sans copier (ou utiliser le std::istrstream Déconseillé) pourrait être une solution, bien que si le problème est vraiment la conversion ... cela utilise toujours le mêmes routines de conversion.

  • Vous pouvez toujours essayer fscanf ou scanf sur votre flux mappé en mémoire. Selon l'implémentation, elles peuvent être plus rapides que les différentes implémentations istream.

  • Il est probablement plus rapide que n'importe lequel d'entre eux d'utiliser strtod. Pas besoin de tokenize pour cela: strtod saute les espaces blancs en tête (y compris '\n'), Et a un paramètre out où il met l'adresse du premier caractère non lu. La condition de fin est un peu délicate, votre boucle devrait probablement ressembler un peu à:

 char * begin; // Définir pour pointer vers les données mmap'ed ... 
 // Vous devrez également organiser un '\ 0' 
 // pour suivre les données. C'est probablement 
 // le problème le plus difficile. 
 Char * end; 
 Errno = 0; 
 Double tmp = strtod (begin, & end); 
 while (errno == 0 && end! = begin) {
 // faire quoi que ce soit avec tmp ... 
 begin = end; 
 tmp = strtod (begin, &fin );
    }

Si aucun de ces éléments n'est assez rapide, vous devrez tenir compte des données réelles. Il a probablement une sorte de contraintes supplémentaires, ce qui signifie que vous pouvez potentiellement écrire une routine de conversion plus rapide que les plus générales; par exemple. strtod doit gérer à la fois fixe et scientifique, et il doit être précis à 100% même s'il y a 17 chiffres significatifs. Il doit également être spécifique aux paramètres régionaux. Tout cela est une complexité supplémentaire, ce qui signifie du code ajouté à exécuter. Mais attention: l'écriture d'une routine de conversion efficace et correcte, même pour un ensemble d'entrée restreint, n'est pas anodine; vous devez vraiment savoir ce que vous faites.

ÉDITER:

Par simple curiosité, j'ai effectué quelques tests. En plus des solutions mentionnées ci-dessus, j'ai écrit un simple convertisseur personnalisé, qui ne gère que le point fixe (non scientifique), avec au plus cinq chiffres après la décimale, et la valeur avant la décimale doit tenir dans un int :

double
convert( char const* source, char const** endPtr )
{
    char* end;
    int left = strtol( source, &end, 10 );
    double results = left;
    if ( *end == '.' ) {
        char* start = end + 1;
        int right = strtol( start, &end, 10 );
        static double const fracMult[] 
            = { 0.0, 0.1, 0.01, 0.001, 0.0001, 0.00001 };
        results += right * fracMult[ end - start ];
    }
    if ( endPtr != nullptr ) {
        *endPtr = end;
    }
    return results;
}

(Si vous l'utilisez réellement, vous devez absolument ajouter une gestion des erreurs. Cela a été rapidement assimilé à des fins expérimentales, pour lire le fichier de test que j'avais généré, et rien sinon.)

L'interface est exactement celle de strtod, pour simplifier le codage.

J'ai exécuté les tests de performance dans deux environnements (sur des machines différentes, donc les valeurs absolues de tous les temps ne sont pas pertinentes). J'ai obtenu les résultats suivants:

Sous Windows 7, compilé avec VC 11 (/ O2):

Testing Using fstream directly (5 iterations)...
    6.3528e+006 microseconds per iteration
Testing Using fscan directly (5 iterations)...
    685800 microseconds per iteration
Testing Using strtod (5 iterations)...
    597000 microseconds per iteration
Testing Using manual (5 iterations)...
    269600 microseconds per iteration

Sous Linux 2.6.18, compilé avec g ++ 4.4.2 (-O2, IIRC):

Testing Using fstream directly (5 iterations)...
    784000 microseconds per iteration
Testing Using fscanf directly (5 iterations)...
    526000 microseconds per iteration
Testing Using strtod (5 iterations)...
    382000 microseconds per iteration
Testing Using strtof (5 iterations)...
    360000 microseconds per iteration
Testing Using manual (5 iterations)...
    186000 microseconds per iteration

Dans tous les cas, je lis 554000 lignes, chacune avec 3 virgule flottante générée aléatoirement dans la plage [0...10000).

La chose la plus frappante est l'énorme différence entre fstream et fscan sous Windows (et la différence relativement faible entre fscan et strtod). La deuxième chose est de savoir combien la fonction de conversion personnalisée simple gagne, sur les deux plates-formes. La gestion des erreurs nécessaire le ralentirait un peu, mais la différence est toujours significative. Je m'attendais à une certaine amélioration, car il ne gère pas beaucoup de choses que les routines de conversion standard font (comme le format scientifique, les très, très petits nombres, Inf et NaN, i18n, etc.), mais pas autant.

18
James Kanze

METTRE À JOUR

Étant donné que Spirit X3 est disponible pour les tests, j'ai mis à jour les benchmarks. Pendant ce temps, j'ai utilisé Nonius pour obtenir des repères statistiquement solides.

Tous les graphiques ci-dessous sont disponibles interactif en ligne

Le projet de référence CMake + les données de test utilisées sont sur github: https://github.com/sehe/bench_float_parsing

enter image description here

Résumé:

Les analyseurs spirituels sont les plus rapides. Si vous pouvez utiliser C++ 14, envisagez la version expérimentale Spirit X3:

enter image description here

Ce qui précède est des mesures utilisant des fichiers mappés en mémoire. En utilisant IOstreams, ce sera plus lent sur toute la carte,

enter image description here

mais pas aussi lent que scanf en utilisant C/POSIX FILE* appels de fonction:

enter image description here


Ce qui suit est des parties de l'ancienne réponse


J'ai implémenté la version Spirit et j'ai effectué un benchmark comparé aux autres réponses suggérées.

Voici mes résultats, tous les tests s'exécutent sur le même corps d'entrée (515 Mo de input.txt). Voir ci-dessous pour les spécifications exactes.


(temps d'horloge murale en secondes, moyenne de 2+ courses)

À ma grande surprise, Boost Spirit se révèle être le plus rapide et le plus élégant:

  • gère/signale les erreurs
  • prend en charge +/- Inf et NaN et les espaces blancs variables
  • aucun problème à détecter la fin de l'entrée (contrairement à l'autre réponse mmap)
  • à l'air cool:

    bool ok = phrase_parse(f,l,               // source iterators
         (double_ > double_ > double_) % eol, // grammar
         blank,                               // skipper
         data);                               // output attribute
    

Notez que boost::spirit::istreambuf_iterator était incroyablement plus lent (15s +). J'espère que ça aide!

Détails de référence

Toutes les analyses effectuées dans vector de struct float3 { float x,y,z; }.

Générer un fichier d'entrée à l'aide

od -f -A none --width=12 /dev/urandom | head -n 11000000

Il en résulte un fichier de 515 Mo contenant des données comme

     -2627.0056   -1.967235e-12  -2.2784738e+33
  -1.0664798e-27  -4.6421956e-23   -6.917859e+20
  -1.1080849e+36   2.8909405e-33   1.7888695e-12
  -7.1663235e+33  -1.0840628e+36   1.5343362e-12
  -3.1773715e-17  -6.3655537e-22   -8.797282e+31
    9.781095e+19   1.7378472e-37        63825084
  -1.2139188e+09  -5.2464635e-05  -2.1235992e-38
   3.0109424e+08   5.3939846e+30  -6.6146894e-20

Compilez le programme en utilisant:

g++ -std=c++0x -g -O3 -isystem -march=native test.cpp -o test -lboost_filesystem -lboost_iostreams

Mesurer le temps de l'horloge murale à l'aide

time ./test < input.txt 

Environnement:

  • Bureau Linux 4.2.0-42-générique # 49-Ubuntu SMP x86_64
  • Processeur Intel (R) Core (TM) i7-3770K à 3,50 GHz
  • 32 Go de RAM

Code complet

Le code complet de l'ancien benchmark se trouve dans le modifier l'historique de ce post , la dernière version est sur github

45
sehe

Avant de commencer, vérifiez qu'il s'agit de la partie lente de votre application et obtenez un faisceau de test autour d'elle afin de pouvoir mesurer les améliorations.

boost::spirit serait à mon avis exagéré. Essayez fscanf

FILE* f = fopen("yourfile");
if (NULL == f) {
   printf("Failed to open 'yourfile'");
   return;
}
float x,y,z;
int nItemsRead = fscanf(f,"%f %f %f\n", &x, &y, &z);
if (3 != nItemsRead) {
   printf("Oh dear, items aren't in the right format.\n");
   return;
}
13
Jeff Foster

Je voudrais consulter ce post connexe en utilisant ifstream pour lire les flottants ou comment puis-je tokenize une chaîne en C++ en particulier les messages liés à la bibliothèque de boîte à outils de chaîne C++. J'ai utilisé C strtok, les flux C++, le tokenizer Boost et le meilleur d'entre eux pour la facilité et l'utilisation est la bibliothèque C++ String Toolkit.

2
DannyK

Je crois qu'une règle la plus importante dans le traitement des chaînes est "lire une seule fois, un caractère à la fois". C'est toujours plus simple, plus rapide et plus fiable, je pense.

J'ai créé un programme de référence simple pour montrer à quel point c'est simple. Mon test indique que ce code s'exécute 40% plus rapidement que la version strtod.

#include <iostream>
#include <sstream>
#include <iomanip>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <sys/time.h>

using namespace std;

string test_generate(size_t n)
{
    srand((unsigned)time(0));
    double sum = 0.0;
    ostringstream os;
    os << std::fixed;
    for (size_t i=0; i<n; ++i)
    {
        unsigned u = Rand();
        int w = 0;
        if (u > UINT_MAX/2)
            w = - (u - UINT_MAX/2);
        else
            w = + (u - UINT_MAX/2);
        double f = w / 1000.0;
        sum += f;

        os << f;
        os << " ";
    }
    printf("generated %f\n", sum);
    return os.str();
}

void read_float_ss(const string& in)
{
    double sum = 0.0;
    const char* begin = in.c_str();
    char* end = NULL;
    errno = 0;
    double f = strtod( begin, &end );
    sum += f;

    while ( errno == 0 && end != begin )
    {
        begin = end;
        f = strtod( begin, &end );
        sum += f;
    }
    printf("scanned %f\n", sum);
}

double scan_float(const char* str, size_t& off, size_t len)
{
    static const double bases[13] = {
        0.0, 10.0, 100.0, 1000.0, 10000.0,
        100000.0, 1000000.0, 10000000.0, 100000000.0,
        1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0,
    };

    bool begin = false;
    bool fail = false;
    bool minus = false;
    int pfrac = 0;

    double dec = 0.0;
    double frac = 0.0;
    for (; !fail && off<len; ++off)
    {
        char c = str[off];
        if (c == '+')
        {
            if (!begin)
                begin = true;
            else
                fail = true;
        }
        else if (c == '-')
        {
            if (!begin)
                begin = true;
            else
                fail = true;
            minus = true;
        }
        else if (c == '.')
        {
            if (!begin)
                begin = true;
            else if (pfrac)
                fail = true;
            pfrac = 1;
        }
        else if (c >= '0' && c <= '9')
        {
            if (!begin)
                begin = true;
            if (pfrac == 0)
            {
                dec *= 10;
                dec += c - '0';
            }
            else if (pfrac < 13)
            {
                frac += (c - '0') / bases[pfrac];
                ++pfrac;
            }
        }
        else
        {
            break;
        }
    }

    if (!fail)
    {
        double f = dec + frac;
        if (minus)
            f = -f;
        return f;
    }

    return 0.0;
}

void read_float_direct(const string& in)
{
    double sum = 0.0;
    size_t len = in.length();
    const char* str = in.c_str();
    for (size_t i=0; i<len; ++i)
    {
        double f = scan_float(str, i, len);
        sum += f;
    }
    printf("scanned %f\n", sum);
}

int main()
{
    const int n = 1000000;
    printf("count = %d\n", n);

    string in = test_generate(n);    
    {
        struct timeval t1;
        gettimeofday(&t1, 0);
        printf("scan start\n");

        read_float_ss(in);

        struct timeval t2;
        gettimeofday(&t2, 0);
        double elapsed = (t2.tv_sec - t1.tv_sec) * 1000000.0;
        elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;
        printf("elapsed %.2fms\n", elapsed);
    }

    {
        struct timeval t1;
        gettimeofday(&t1, 0);
        printf("scan start\n");

        read_float_direct(in);

        struct timeval t2;
        gettimeofday(&t2, 0);
        double elapsed = (t2.tv_sec - t1.tv_sec) * 1000000.0;
        elapsed += (t2.tv_usec - t1.tv_usec) / 1000.0;
        printf("elapsed %.2fms\n", elapsed);
    }
    return 0;
}

Vous trouverez ci-dessous la sortie de la console d'i7 Mac Book Pro (compilée dans XCode 4.6).

count = 1000000
generated -1073202156466.638184
scan start
scanned -1073202156466.638184
elapsed 83.34ms
scan start
scanned -1073202156466.638184
elapsed 53.50ms
0
9dan

[~ # ~] modifier [~ # ~] : Pour ceux qui craignent que crack_atof ne soit en aucun cas validé, veuillez consulter les commentaires en bas sur - Ry .

Voici une chaîne à grande vitesse plus complète (bien que n'étant "officielle" par aucune norme) pour doubler la routine, puisque la solution Nice C++ 17 from_chars() ne fonctionne que sur MSVC (pas clang ou gcc).

Rencontrer crack_atof

https://Gist.github.com/oschonrock/a410d4bec6ec1ccc5a3009f0907b3d15

Pas mon travail, je l'ai juste légèrement refactorisé. Et changé la signature. Le code est très facile à comprendre, et il est évident pourquoi il est rapide. Et c'est très très rapide, voir les repères ici:

https://www.codeproject.com/Articles/1130262/Cplusplus-string-view-Conversion-to-Integral-Types

Je l'ai exécuté avec 11 000 000 lignes de 3 flotteurs (précision à 15 chiffres dans le csv, ce qui compte!). Sur mon Core i7 2600 de 2e génération, il fonctionnait en 1,327 s. Compiler clang V8.0.0 -O2 sur Kubuntu 19.04.

Code complet ci-dessous. J'utilise mmap, car str-> float n'est plus le seul goulot d'étranglement grâce à crack_atof. J'ai emballé les trucs mmap dans une classe pour assurer la sortie RAII de la carte.


#include <iomanip>
#include <iostream>

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

class MemoryMappedFile {
public:
  MemoryMappedFile(const char* filename) {
    int fd = open(filename, O_RDONLY);
    if (fd == -1) throw std::logic_error("MemoryMappedFile: couldn't open file.");

    // obtain file size
    struct stat sb;
    if (fstat(fd, &sb) == -1) throw std::logic_error("MemoryMappedFile: cannot stat file size");
    m_filesize = sb.st_size;

    m_map = static_cast<const char*>(mmap(NULL, m_filesize, PROT_READ, MAP_PRIVATE, fd, 0u));
    if (m_map == MAP_FAILED) throw std::logic_error("MemoryMappedFile: cannot map file");
  }

  ~MemoryMappedFile() {
    if (munmap(static_cast<void*>(const_cast<char*>(m_map)), m_filesize) == -1)
      std::cerr << "Warnng: MemoryMappedFile: error in destructor during `munmap()`\n";
  }

  const char* start() const { return m_map; }
  const char* end() const { return m_map + m_filesize; }

private:
  size_t m_filesize = 0;
  const char* m_map = nullptr;
};

// high speed str -> double parser
double pow10(int n) {
  double ret = 1.0;
  double r   = 10.0;
  if (n < 0) {
    n = -n;
    r = 0.1;
  }

  while (n) {
    if (n & 1) {
      ret *= r;
    }
    r *= r;
    n >>= 1;
  }
  return ret;
}

double crack_atof(const char* start, const char* const end) {
  if (!start || !end || end <= start) {
    return 0;
  }

  int sign         = 1;
  double int_part  = 0.0;
  double frac_part = 0.0;
  bool has_frac    = false;
  bool has_exp     = false;

  // +/- sign
  if (*start == '-') {
    ++start;
    sign = -1;
  } else if (*start == '+') {
    ++start;
  }

  while (start != end) {
    if (*start >= '0' && *start <= '9') {
      int_part = int_part * 10 + (*start - '0');
    } else if (*start == '.') {
      has_frac = true;
      ++start;
      break;
    } else if (*start == 'e') {
      has_exp = true;
      ++start;
      break;
    } else {
      return sign * int_part;
    }
    ++start;
  }

  if (has_frac) {
    double frac_exp = 0.1;

    while (start != end) {
      if (*start >= '0' && *start <= '9') {
        frac_part += frac_exp * (*start - '0');
        frac_exp *= 0.1;
      } else if (*start == 'e') {
        has_exp = true;
        ++start;
        break;
      } else {
        return sign * (int_part + frac_part);
      }
      ++start;
    }
  }

  // parsing exponent part
  double exp_part = 1.0;
  if (start != end && has_exp) {
    int exp_sign = 1;
    if (*start == '-') {
      exp_sign = -1;
      ++start;
    } else if (*start == '+') {
      ++start;
    }

    int e = 0;
    while (start != end && *start >= '0' && *start <= '9') {
      e = e * 10 + *start - '0';
      ++start;
    }

    exp_part = pow10(exp_sign * e);
  }

  return sign * (int_part + frac_part) * exp_part;
}

int main() {
  MemoryMappedFile map  = MemoryMappedFile("FloatDataset.csv");
  const char* curr      = map.start();
  const char* start     = map.start();
  const char* const end = map.end();

  uintmax_t lines_n = 0;
  int cnt              = 0;
  double sum           = 0.0;
  while (curr && curr != end) {
    if (*curr == ',' || *curr == '\n') {
      // std::string fieldstr(start, curr);
      // double field = std::stod(fieldstr);
      // m_numLines = 11000000 cnt=33000000 sum=16498294753551.9
      // real 5.998s

      double field = crack_atof(start, curr);
      // m_numLines = 11000000 cnt=33000000 sum=16498294753551.9
      // real 1.327s

      sum += field;
      ++cnt;
      if (*curr == '\n') lines_n++;
      curr++;
      start = curr;
    } else {
      ++curr;
    }
  }

  std::cout << std::setprecision(15) << "m_numLines = " << lines_n << " cnt=" << cnt
            << " sum=" << sum << "\n";
}

Code également sur un github Gist:

https://Gist.github.com/oschonrock/67fc870ba067ebf0f369897a9d52c2dd

0
Oliver Schönrock

utiliser C va être la solution la plus rapide. Fractionner en jetons en utilisant strtok puis convertir en flottant avec strtof . Ou si vous connaissez le format exact, utilisez fscanf.

0
log0

une solution concrète serait de jeter plus de cœurs sur le problème, engendrant plusieurs threads. Si le goulot d'étranglement n'est que le processeur, vous pouvez réduire de moitié le temps d'exécution en générant deux threads (sur les processeurs multicœurs)

quelques autres conseils:

  • essayez d'éviter l'analyse des fonctions de la bibliothèque telles que boost et/ou std. Ils sont gonflés de conditions de vérification d'erreur et une grande partie du temps de traitement est consacré à ces vérifications. Pour quelques conversions, elles sont correctes mais échouent lamentablement lorsqu'il s'agit de traiter des millions de valeurs. Si vous savez déjà que vos données sont bien formatées, vous pouvez écrire (ou trouver) une fonction C optimisée personnalisée qui ne fait que la conversion des données

  • utilisez un grand tampon de mémoire (disons 10 Mo) dans lequel vous chargez des morceaux de votre fichier et effectuez la conversion là-bas

  • divide et impera: divisez votre problème en problèmes plus petits et plus simples: prétraitez votre fichier, faites-le flotter sur une seule ligne, divisez chaque ligne par le "." et convertissez des entiers au lieu de float, puis fusionnez les deux entiers pour créer le nombre float

0
Gianluca Ghettini