Comment améliorer IOStream?

La plupart des utilisateurs de C ++ qui ont appris C préfèrent utiliser la famille de fonctions printf / scanf même lorsqu’ils codent en C ++.

Bien que j’admette que je trouve l’interface beaucoup mieux (en particulier le format et la localisation de type POSIX), il semble qu’une préoccupation majeure est la performance.

En regardant cette question:

Comment puis-je accélérer la lecture ligne par ligne d’un fichier

Il semble que la meilleure réponse soit d’utiliser fscanf et que le ifstream C ++ soit constamment 2 ou 3 fois plus lent.

Je pensais que ce serait génial si nous pouvions comstackr un référentiel de “conseils” pour améliorer les performances de IOStreams, ce qui fonctionne, ce qui ne fonctionne pas.

Points à considérer

  • mise en mémoire tampon ( rdbuf()->pubsetbuf(buffer, size) )
  • synchronisation ( std::ios_base::sync_with_stdio )
  • gestion des parameters régionaux (pouvons-nous utiliser un environnement local réduit ou le supprimer complètement?)

Bien sûr, d’autres approches sont les bienvenues.

Note: une “nouvelle” implémentation, de Dietmar Kuhl, a été mentionnée, mais je n’ai pas pu trouver beaucoup de détails à ce sujet. Les références précédentes semblent être des liens morts.

Voici ce que j’ai rassemblé jusqu’ici:

Mise en mémoire tampon :

Si par défaut le tampon est très petit, augmenter la taille du tampon peut certainement améliorer les performances:

  • cela réduit le nombre de hits du disque dur
  • cela réduit le nombre d’appels système

Le tampon peut être défini en accédant à l’implémentation streambuf sous-jacente.

 char Buffer[N]; std::ifstream file("file.txt"); file.rdbuf()->pubsetbuf(Buffer, N); // the pointer reader by rdbuf is guaranteed // to be non-null after successful constructor 

Avertissement de @iavr: selon cppreference, il est préférable d’appeler pubsetbuf avant d’ouvrir le fichier. Diverses implémentations de bibliothèques standard ont des comportements différents.

Gestion locale:

Les parameters régionaux permettent d’effectuer des conversions de caractères, du filtrage et des astuces plus intelligentes lorsque des nombres ou des dates sont impliqués. Ils passent par un système complexe de répartition dynamic et d’appels virtuels, de sorte que leur suppression peut aider à réduire les pénalités.

Les parameters régionaux par défaut de C sont pas conçus pour effectuer des conversions et pour être uniformes sur les ordinateurs. C’est un bon défaut à utiliser.

Synchronisation:

Je ne pouvais voir aucune amélioration de performance en utilisant cette installation.

On peut accéder à un paramètre global (membre statique de std::ios_base ) en utilisant la fonction statique sync_with_stdio .

Des mesures:

En jouant avec cela, j’ai joué avec un programme simple, compilé en utilisant gcc 3.4.2 sur SUSE 10p3 avec -O2 .

C: 7.76532e + 06
C ++: 1.0874e + 07

Ce qui représente un ralentissement d’environ 20% … pour le code par défaut. En effet, la falsification du tampon (en C ou C ++) ou des parameters de synchronisation (C ++) n’a apporté aucune amélioration.

Résultats par d’autres:

@Irfy sur g ++ 4.7.2-2ubuntu1, -O3, Ubuntu virtualisé 11.10, 3.5.0-25-generic, x86_64, suffisamment de ram / cpu, 196Mo de plusieurs exécutions “find / >> largefile.txt”

C: 634572 C ++: 473222

C ++ 25% plus rapide

@Matteo Italia sur g ++ 4.4.5, -O3, Ubuntu Linux 10.10 x86_64 avec un fichier aléatoire de 180 Mo

C: 910390
C ++: 776016

C ++ 17% plus rapide

@Bogatyr sur g ++ i686-apple-darwin10-g ++ – 4.2.1 (GCC) 4.2.1 (Apple Inc. build 5664), mac mini, 4 Go de RAM, inactif sauf pour ce test avec un fichier de 168 Mo

C: 4.34151e + 06
C ++: 9.14476e + 06

C ++ 111% plus lent

@Asu sur clang ++ 3.8.0-2ubuntu4, Kubuntu 16.04 Linux 4.8-rc3, 8 Go de mémoire vive, i5 Haswell, SSD Crucial, fichier de données 88 Mo (archive tar.xz)

C: 270895 C ++: 162799

C ++ 66% plus rapide

La réponse est donc la suivante: il s’agit d’un problème de qualité d’implémentation et dépend vraiment de la plate-forme: /

Le code en entier ici pour ceux qui s’intéressent à l’parsing comparative:

 #include  #include  #include  #include  #include  #include  template  double benchmark(Func f, size_t iterations) { f(); timeval a, b; gettimeofday(&a, 0); for (; iterations --> 0;) { f(); } gettimeofday(&b, 0); return (b.tv_sec * (unsigned int)1e6 + b.tv_usec) - (a.tv_sec * (unsigned int)1e6 + a.tv_usec); } struct CRead { CRead(char const* filename): _filename(filename) {} void operator()() { FILE* file = fopen(_filename, "r"); int count = 0; while ( fscanf(file,"%s", _buffer) == 1 ) { ++count; } fclose(file); } char const* _filename; char _buffer[1024]; }; struct CppRead { CppRead(char const* filename): _filename(filename), _buffer() {} enum { BufferSize = 16184 }; void operator()() { std::ifstream file(_filename, std::ifstream::in); // comment to remove extended buffer file.rdbuf()->pubsetbuf(_buffer, BufferSize); int count = 0; std::ssortingng s; while ( file >> s ) { ++count; } } char const* _filename; char _buffer[BufferSize]; }; int main(int argc, char* argv[]) { size_t iterations = 1; if (argc > 1) { iterations = atoi(argv[1]); } char const* oldLocale = setlocale(LC_ALL,"C"); if (strcmp(oldLocale, "C") != 0) { std::cout << "Replaced old locale '" << oldLocale << "' by 'C'\n"; } char const* filename = "largefile.txt"; CRead cread(filename); CppRead cppread(filename); // comment to use the default setting bool oldSyncSetting = std::ios_base::sync_with_stdio(false); double ctime = benchmark(cread, iterations); double cpptime = benchmark(cppread, iterations); // comment if oldSyncSetting's declaration is commented std::ios_base::sync_with_stdio(oldSyncSetting); std::cout << "C : " << ctime << "\n" "C++: " << cpptime << "\n"; return 0; } 

Deux autres améliorations:

Issue std::cin.tie(nullptr); avant entrée / sortie lourde.

Citant http://fr.cppreference.com/w/cpp/io/cin :

Une fois que std :: cin est construit, std :: cin.tie () renvoie & std :: cout, et de même, std :: wcin.tie () renvoie & std :: wcout. Cela signifie que toute opération de saisie formatée sur std :: cin force un appel à std :: cout.flush () si des caractères sont en attente de sortie.

Vous pouvez éviter de vider le tampon en dé- std::cin de std::cout . Ceci est pertinent avec plusieurs appels mixtes vers std::cin et std::cout . Notez que l’appel std::cin.tie(std::nullptr); rend le programme impropre à l’exécution interactive par l’utilisateur, car la sortie peut être retardée.

Référence pertinente:

Fichier test1.cpp :

 #include  using namespace std; int main() { ios_base::sync_with_stdio(false); int i; while(cin >> i) cout << i << '\n'; } 

Fichier test2.cpp :

 #include  using namespace std; int main() { ios_base::sync_with_stdio(false); cin.tie(nullptr); int i; while(cin >> i) cout << i << '\n'; cout.flush(); } 

Les deux compilés par g++ -O2 -std=c++11 . Version du compilateur: g++ (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4 (oui, je sais, plutôt vieux).

Résultats de référence:

 work@mg-K54C ~ $ time ./test1 < test.in > test1.in real 0m3.140s user 0m0.581s sys 0m2.560s work@mg-K54C ~ $ time ./test2 < test.in > test2.in real 0m0.234s user 0m0.234s sys 0m0.000s 

( test.in compose de 1179648 lignes composées chacune d'un seul 5 Il est de 2,4 Mo, désolé de ne pas l'avoir posté ici.)

Je me souviens d'avoir résolu une tâche algorithmique où le juge en ligne refusait sans cin.tie(nullptr) mon programme sans cin.tie(nullptr) mais l'acceptait avec cin.tie(nullptr) ou printf / scanf au lieu de cin / cout .

Utilisez '\n' au lieu de std::endl .

Citant http://fr.cppreference.com/w/cpp/io/manip/endl :

Insère un caractère de nouvelle ligne dans la séquence de sortie os et le vide en appelant os.put (os.widen ('\ n')) suivi de os.flush ().

Vous pouvez éviter de vider le dossier en imprimant '\n' au lieu de endl .

Référence pertinente:

Fichier test1.cpp :

 #include  using namespace std; int main() { ios_base::sync_with_stdio(false); for(int i = 0; i < 1179648; ++i) cout << i << endl; } 

Fichier test2.cpp :

 #include  using namespace std; int main() { ios_base::sync_with_stdio(false); for(int i = 0; i < 1179648; ++i) cout << i << '\n'; } 

Les deux compilés comme ci-dessus.

Résultats de référence:

 work@mg-K54C ~ $ time ./test1 > test1.in real 0m2.946s user 0m0.404s sys 0m2.543s work@mg-K54C ~ $ time ./test2 > test2.in real 0m0.156s user 0m0.135s sys 0m0.020s 

Il est intéressant de noter que les programmeurs C préfèrent printf lors de l’écriture de C ++, car je vois beaucoup de code C, à part d’utiliser cout et iostream pour écrire la sortie.

Les applications peuvent souvent obtenir de meilleures performances en utilisant directement filebuf (Scott Meyers l’a mentionné dans Effective STL), mais l’utilisation de filebuf direct est relativement peu documentée et la plupart des développeurs préfèrent std::getline plus simple la plupart du temps.

En ce qui concerne les parameters régionaux, si vous créez des facettes, vous obtiendrez souvent de meilleures performances en créant une fois les parameters régionaux avec toutes vos facettes, en les conservant stockées et en les intégrant dans chaque stream que vous utilisez.

J’ai vu un autre sujet à ce sujet ici récemment, ce qui est presque un doublon.