web-dev-qa-db-fra.com

Comment améliorer les performances d'IOStream?

La plupart des utilisateurs 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 bien meilleure (en particulier le format et la localisation de type POSIX), il semble que la performance soit une préoccupation majeure.

En regardant cette question:

Comment accélérer la lecture ligne par ligne d'un fichier

Il semble que la meilleure réponse consiste à utiliser fscanf et que le C++ ifstream est constamment 2-3 fois plus lent.

Je pensais que ce serait génial si nous pouvions compiler un référentiel de "conseils" pour améliorer les performances 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 paramètres régionaux (pourrions-nous utiliser un paramètre régional réduit ou le supprimer complètement?)

Bien entendu, d'autres approches sont les bienvenues.

Remarque: une "nouvelle" implémentation, par 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.

61
Matthieu M.

Voici ce que j'ai rassemblé jusqu'à présent:

Mise en mémoire tampon :

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

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

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

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 gracieuseté 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 sinon des comportements différents.

Gestion des paramètres régionaux:

Les paramètres régionaux peuvent effectuer une conversion de caractères, un 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 dynamique et d'appels virtuels, donc les supprimer peut aider à réduire le coup de pénalité.

Les paramètres régionaux par défaut C sont destinés à ne pas effectuer de conversion et à être uniformes sur toutes les machines. C'est un bon défaut à utiliser.

Synchronisation:

Je n'ai pu voir aucune amélioration des performances en utilisant cette installation.

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

Mesures:

Jouer 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 paramètres 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-générique, x86_64, assez de ram/cpu, 196 Mo 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 données de 168 Mo

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

C++ 111% plus lent

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

C: 270895 C++: 162799

C++ 66% plus rapide

La réponse est donc: c'est un problème de qualité de mise en œuvre, et dépend vraiment de la plateforme: /

Le code complet ici pour ceux qui sont intéressés par l'analyse comparative:

#include <fstream>
#include <iostream>
#include <iomanip>

#include <cmath>
#include <cstdio>

#include <sys/time.h>

template <typename Func>
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::string 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;
}
47
Matthieu M.

Deux autres améliorations:

Émettez std::cin.tie(nullptr); avant une entrée/sortie importante.

Citation http://en.cppreference.com/w/cpp/io/cin :

Une fois std :: cin construit, std :: cin.tie () retourne & std :: cout, et de même, std :: wcin.tie () retourne & std :: wcout. Cela signifie que toute opération d'entrée 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éliant std::cin De std::cout. Ceci est pertinent avec plusieurs appels mixtes à std::cin Et std::cout. Notez que l'appel de std::cin.tie(std::nullptr); rend le programme impropre à une exécution interactive par l'utilisateur, car la sortie peut être retardée.

Référence pertinente:

Fichier test1.cpp:

#include <iostream>
using namespace std;

int main()
{
  ios_base::sync_with_stdio(false);

  int i;
  while(cin >> i)
    cout << i << '\n';
}

Fichier test2.cpp:

#include <iostream>
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 (ouais, je sais, assez 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 Se compose de 1179648 lignes chacune composée uniquement d'un seul 5. C'est 2,4 Mo, désolé de ne pas l'avoir affiché ici.).

Je me souviens avoir résolu une tâche algorithmique où le juge en ligne refusait sans cesse 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.

Citation http://en.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 comme si en appelant os.put (os.widen ('\ n')) suivi de os.flush ().

Vous pouvez éviter de vider le bufer en imprimant '\n' Au lieu de endl.

Référence pertinente:

Fichier test1.cpp:

#include <iostream>
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 <iostream>
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
15
gaazkam

Intéressant, vous dites que les programmeurs C préfèrent printf lors de l'écriture C++ car je vois beaucoup de code qui est C autre que l'utilisation de cout et iostream pour écrire la sortie.

Les utilisations peuvent souvent obtenir de meilleures performances en utilisant filebuf directement (Scott Meyers l'a mentionné dans Effective STL) mais il y a relativement peu de documentation pour utiliser filebuf direct et la plupart des développeurs préfèrent std::getline ce qui est plus simple la plupart du temps.

En ce qui concerne les paramètres régionaux, si vous créez des facettes, vous obtiendrez souvent de meilleures performances en créant une fois les paramètres régionaux avec toutes vos facettes, en les conservant stockées et en les insérant dans chaque flux que vous utilisez.

J'ai vu un autre sujet à ce sujet ici récemment, donc c'est presque un doublon.

1
CashCow