web-dev-qa-db-fra.com

Pourquoi std :: pair est-il plus rapide que std :: tuple

Voici le code de test.

Test de tuple:

using namespace std;

int main(){

    vector<Tuple<int,int>> v;


    for (int var = 0; var < 100000000; ++var) {
        v.Push_back(make_Tuple(var, var));
    }
}

Test de paire:

#include <vector>

using namespace std;

int main(){

    vector<pair<int,int>> v;


    for (int var = 0; var < 100000000; ++var) {
        v.Push_back(make_pair(var, var));
    }
}

J'ai fait la mesure du temps via la commande de temps Linux. Les résultats sont:

|       |   -O0   |    -O2   |
|:------|:-------:|:--------:|
| Pair  |   8.9 s |  1.60 s  |
| Tuple |  19.8 s |  1.96 s  |

Je me demande pourquoi il y a une si grande différence entre ces deux structures de données dans O0, car elles devraient être très similaires. Il y a juste une petite différence en 02.

Pourquoi la différence en O0 est-elle si grande et pourquoi y a-t-il une différence?

ÉDITER:

Le code avec v.resize ()

Paire:

#include <vector>

using namespace std;

int main(){

    vector<pair<int,int>> v;

    v.resize(100000000);

    for (int var = 0; var < 100000000; ++var) {
        v[var] = make_pair(var, var);
    }
}

Tuple:

#include<Tuple>
#include<vector>

using namespace std;

int main(){

    vector<Tuple<int,int>> v;

    v.resize(100000000);

    for (int var = 0; var < 100000000; ++var) {
        v[var] = make_Tuple(var, var);
    }
}

Résultats:

|       |   -O0   |    -O2   |
|:------|:-------:|:--------:|
| Pair  |  5.01 s |  0.77 s  |
| Tuple |  10.6 s |  0.87 s  |

ÉDITER:

Mon système

g++ (GCC) 4.8.3 20140911 (Red Hat 4.8.3-7)
GLIBCXX_3.4.19
42
Aleksandar

Il vous manque des informations cruciales: quel compilateur utilisez-vous? Qu'est-ce que vous utilisez pour mesurer les performances de la micro-référence? Quelle implémentation de bibliothèque standard utilisez-vous?

Mon système:

g++ (GCC) 4.9.1 20140903 (prerelease)
GLIBCXX_3.4.20

Quoi qu'il en soit, j'ai exécuté vos exemples, mais j'ai d'abord réservé la taille appropriée des vecteurs pour se débarrasser de la surcharge d'allocation de mémoire. Avec cela, j'observe drôlement le contraire quelque chose d'intéressant - l'inverse de ce que vous voyez:

g++ -std=c++11 -O2 pair.cpp -o pair
perf stat -r 10 -d ./pair
Performance counter stats for './pair' (10 runs):

      1647.045151      task-clock:HG (msec)      #    0.993 CPUs utilized            ( +-  1.94% )
              346      context-switches:HG       #    0.210 K/sec                    ( +- 40.13% )
                7      cpu-migrations:HG         #    0.004 K/sec                    ( +- 22.01% )
          182,978      page-faults:HG            #    0.111 M/sec                    ( +-  0.04% )
    3,394,685,602      cycles:HG                 #    2.061 GHz                      ( +-  2.24% ) [44.38%]
    2,478,474,676      stalled-cycles-frontend:HG #   73.01% frontend cycles idle     ( +-  1.24% ) [44.55%]
    1,550,747,174      stalled-cycles-backend:HG #   45.68% backend  cycles idle     ( +-  1.60% ) [44.66%]
    2,837,484,461      instructions:HG           #    0.84  insns per cycle        
                                                  #    0.87  stalled cycles per insn  ( +-  4.86% ) [55.78%]
      526,077,681      branches:HG               #  319.407 M/sec                    ( +-  4.52% ) [55.82%]
          829,623      branch-misses:HG          #    0.16% of all branches          ( +-  4.42% ) [55.74%]
      594,396,822      L1-dcache-loads:HG        #  360.887 M/sec                    ( +-  4.74% ) [55.59%]
        20,842,113      L1-dcache-load-misses:HG  #    3.51% of all L1-dcache hits    ( +-  0.68% ) [55.46%]
        5,474,166      LLC-loads:HG              #    3.324 M/sec                    ( +-  1.81% ) [44.23%]
  <not supported>      LLC-load-misses:HG       

      1.658671368 seconds time elapsed                                          ( +-  1.82% )

contre:

g++ -std=c++11 -O2 Tuple.cpp -o Tuple
perf stat -r 10 -d ./Tuple
Performance counter stats for './Tuple' (10 runs):

        996.090514      task-clock:HG (msec)      #    0.996 CPUs utilized            ( +-  2.41% )
              102      context-switches:HG       #    0.102 K/sec                    ( +- 64.61% )
                4      cpu-migrations:HG         #    0.004 K/sec                    ( +- 32.24% )
          181,701      page-faults:HG            #    0.182 M/sec                    ( +-  0.06% )
    2,052,505,223      cycles:HG                 #    2.061 GHz                      ( +-  2.22% ) [44.45%]
    1,212,930,513      stalled-cycles-frontend:HG #   59.10% frontend cycles idle     ( +-  2.94% ) [44.56%]
      621,104,447      stalled-cycles-backend:HG #   30.26% backend  cycles idle     ( +-  3.48% ) [44.69%]
    2,700,410,991      instructions:HG           #    1.32  insns per cycle        
                                                  #    0.45  stalled cycles per insn  ( +-  1.66% ) [55.94%]
      486,476,408      branches:HG               #  488.386 M/sec                    ( +-  1.70% ) [55.96%]
          959,651      branch-misses:HG          #    0.20% of all branches          ( +-  4.78% ) [55.82%]
      547,000,119      L1-dcache-loads:HG        #  549.147 M/sec                    ( +-  2.19% ) [55.67%]
        21,540,926      L1-dcache-load-misses:HG  #    3.94% of all L1-dcache hits    ( +-  2.73% ) [55.43%]
        5,751,650      LLC-loads:HG              #    5.774 M/sec                    ( +-  3.60% ) [44.21%]
  <not supported>      LLC-load-misses:HG       

      1.000126894 seconds time elapsed                                          ( +-  2.47% )

comme vous pouvez le voir, dans mon cas, la raison en est le nombre beaucoup plus élevé de cycles bloqués, à la fois dans le frontend et dans le backend.

D'où cela vient-il maintenant? Je parie que cela revient à un échec de l'inline, similaire à ce qui est expliqué ici: régression des performances std :: vector lors de l'activation de C++ 11

En effet, l'activation de -flto égalise les résultats pour moi:

Performance counter stats for './pair' (10 runs):

      1021.922944      task-clock:HG (msec)      #    0.997 CPUs utilized            ( +-  1.15% )
                63      context-switches:HG       #    0.062 K/sec                    ( +- 77.23% )
                5      cpu-migrations:HG         #    0.005 K/sec                    ( +- 34.21% )
          195,396      page-faults:HG            #    0.191 M/sec                    ( +-  0.00% )
    2,109,877,147      cycles:HG                 #    2.065 GHz                      ( +-  0.92% ) [44.33%]
    1,098,031,078      stalled-cycles-frontend:HG #   52.04% frontend cycles idle     ( +-  0.93% ) [44.46%]
      701,553,535      stalled-cycles-backend:HG #   33.25% backend  cycles idle     ( +-  1.09% ) [44.68%]
    3,288,420,630      instructions:HG           #    1.56  insns per cycle        
                                                  #    0.33  stalled cycles per insn  ( +-  0.88% ) [55.89%]
      672,941,736      branches:HG               #  658.505 M/sec                    ( +-  0.80% ) [56.00%]
          660,278      branch-misses:HG          #    0.10% of all branches          ( +-  2.05% ) [55.93%]
      474,314,267      L1-dcache-loads:HG        #  464.139 M/sec                    ( +-  1.32% ) [55.73%]
        19,481,787      L1-dcache-load-misses:HG  #    4.11% of all L1-dcache hits    ( +-  0.80% ) [55.51%]
        5,155,678      LLC-loads:HG              #    5.045 M/sec                    ( +-  1.69% ) [44.21%]
  <not supported>      LLC-load-misses:HG       

      1.025083895 seconds time elapsed                                          ( +-  1.03% )

et pour Tuple:

Performance counter stats for './Tuple' (10 runs):

      1018.980969      task-clock:HG (msec)      #    0.999 CPUs utilized            ( +-  0.47% )
                8      context-switches:HG       #    0.008 K/sec                    ( +- 29.74% )
                3      cpu-migrations:HG         #    0.003 K/sec                    ( +- 42.64% )
          195,396      page-faults:HG            #    0.192 M/sec                    ( +-  0.00% )
    2,103,574,740      cycles:HG                 #    2.064 GHz                      ( +-  0.30% ) [44.28%]
    1,088,827,212      stalled-cycles-frontend:HG #   51.76% frontend cycles idle     ( +-  0.47% ) [44.56%]
      697,438,071      stalled-cycles-backend:HG #   33.15% backend  cycles idle     ( +-  0.41% ) [44.76%]
    3,305,631,646      instructions:HG           #    1.57  insns per cycle        
                                                  #    0.33  stalled cycles per insn  ( +-  0.21% ) [55.94%]
      675,175,757      branches:HG               #  662.599 M/sec                    ( +-  0.16% ) [56.02%]
          656,205      branch-misses:HG          #    0.10% of all branches          ( +-  0.98% ) [55.93%]
      475,532,976      L1-dcache-loads:HG        #  466.675 M/sec                    ( +-  0.13% ) [55.69%]
        19,430,992      L1-dcache-load-misses:HG  #    4.09% of all L1-dcache hits    ( +-  0.20% ) [55.49%]
        5,161,624      LLC-loads:HG              #    5.065 M/sec                    ( +-  0.47% ) [44.14%]
  <not supported>      LLC-load-misses:HG       

      1.020225388 seconds time elapsed                                          ( +-  0.48% )

Alors souviens-toi, -flto est votre ami et l'échec de l'inline peut avoir des résultats extrêmes sur du code fortement basé sur des modèles. Utilisation perf stat pour savoir ce qui se passe.

64
milianw

milianw n'a pas abordé le -O0 contre. -O2, je voudrais donc ajouter une explication à cela.

Il est pleinement prévu que std::Tuple sera plus lent que std::pair quand pas optimisé, car c'est un objet plus compliqué. Une paire a exactement deux membres, donc ses méthodes sont simples à définir. Mais Tuple a un nombre arbitraire de membres et la seule façon de parcourir la liste d'arguments de modèle est avec la récursivité. Par conséquent, la plupart des fonctions pour Tuple gèrent un membre, puis récurrent pour gérer le reste, donc pour 2-Tuple, vous avez deux fois plus d'appels de fonction.

Maintenant, quand ils sont optimisés, le compilateur insérera cette récursivité et il ne devrait pas y avoir de différence significative. Ce que les tests confirment clairement. Cela s'applique en général aux trucs fortement basés sur des modèles. Les modèles peuvent être écrits pour fournir une abstraction sans ou très peu de temps d'exécution, mais cela repose sur des optimisations pour incorporer toutes les fonctions triviales.

37
Jan Hudec