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.
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.
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
Les analyseurs spirituels sont les plus rapides. Si vous pouvez utiliser C++ 14, envisagez la version expérimentale Spirit X3:
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,
mais pas aussi lent que scanf
en utilisant C/POSIX FILE*
appels de fonction:
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
destruct 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
Le code complet de l'ancien benchmark se trouve dans le modifier l'historique de ce post , la dernière version est sur github
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;
}
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.
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
[~ # ~] 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
utiliser C va être la solution la plus rapide. Fractionner en jetons en utilisant convertir en flottant avec strtok
puisstrtof
. Ou si vous connaissez le format exact, utilisez fscanf
.
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