12 votes

Pourquoi l'allocation répétée de mémoire dans Windows ralentit-elle ?

Je veux comprendre pourquoi le code suivant se comporte différemment sur mes machines linux et Windows 7 : Sur linux, il faut ~120ms par itération. Sur Windows 7, la première itération prend déjà 0,4 seconde et les itérations suivantes prennent beaucoup plus de temps. L'itération 8 prend déjà environ 11 secondes, l'itération 22 prend environ 1 minute.

J'ai observé ce comportement sur différents matériels. Il semble être lié à Windows.

#include <iostream>
#include <time.h>
#include <chrono>

void iteration() {
  int n = 25000;
  // Allocate memory
  long** blocks = new long*[n];
  for( int i = 0; i<n; ++i )
  {
    blocks[i] = new long[100008];
  }
  // Free all allocated memory
  for( int i = 0; i<n; ++i )
  {
    delete[] blocks[i];
  }
  delete[] blocks;
}

int main(int argc, char **argv) {
  int nbIter = 30;
  for( int i = 0; i < nbIter; ++i )
  {
    auto start = std::chrono::system_clock::now();
    iteration();
    auto end = std::chrono::system_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "Iteration #" << i << ": time=" << elapsed.count() << "ms" << std::endl;
  }
  return 0;
}

Quelqu'un peut-il me dire ce qui se passe ici, et comment faire pour que le code fonctionne de manière stable sous Windows ?

edit : J'ai fait un release build dans VS2013 sur Windows, j'ai exécuté le programme en dehors de VS. Voici quelques temps d'exécution plus exacts (en secondes) :

Iteration #0: time=0.381000
Iteration #1: time=0.391000
Iteration #2: time=0.451000
Iteration #3: time=1.507000
Iteration #4: time=1.711000
Iteration #5: time=2.938000
Iteration #6: time=4.770000
Iteration #7: time=7.840000
Iteration #8: time=10.563000
Iteration #9: time=14.606000
Iteration #10: time=20.732000
Iteration #11: time=24.948000
Iteration #12: time=30.255000
Iteration #13: time=34.608000
Iteration #14: time=38.114000
Iteration #15: time=43.217000
Iteration #16: time=39.926000
Iteration #17: time=43.506000
Iteration #18: time=43.660000
Iteration #19: time=45.291000
Iteration #20: time=50.003000

5voto

txtechhelp Points 1313

Creuser autour de dans le références sur le amas sous Windows (ainsi que un peu de infosec articles sur le sujet), il y a quelques informations sur ralentissements courants du tas qui stipule que quelques-uns sont

  • Ralentissement suite à des opérations de répartition.
  • Ralentissement suite à des opérations libres.
  • Ralentissement dû à des allocations et réallocations fréquentes.

Ce qui aide à expliquer pourquoi il y a un ralentissement (c'est-à-dire des allocations et réallocations fréquentes), mais cela n'explique pas réellement pourquoi il y a un ralentissement.

La première chose à noter est que sizeof(long) != sizeof(long) c'est-à-dire, dans les tests que j'ai effectués avec des constructions 64 bits utilisant g++ et Visual Studio 12, sizeof(long) sur Windows était 4 et sous Linux, c'était 8 . Il s'agit d'une remarque importante lors de l'allocation/désallocation de la mémoire. Si vous modifiez votre code à partir de la version long à un type où sizeof(T) == 8 (comme long long ), le problème disparaît et les temps sont cohérents entre les itérations. Exemple :

void iteration() {
    int n = 25000;
    // Allocate memory
    long long** blocks = new long long*[n];
    for (int i = 0; i < n; ++i) {
        blocks[i] = new long long[100008];
    }
    // Free all allocated memory
    for (int i = 0; i < n; ++i) {
        delete[] blocks[i];
    }
    delete[] blocks;
}
// sizeof(long long) == 8 on my Linux/Unix and Windows 64-bit machines

Il convient également de noter que le problème de synchronisation ne disparaît que dans les cas suivants exactement ce code.

Si vous gardez le type de long long mais ajusté 100008 pour dire 16666 le problème se pose à nouveau ; en outre, si vous le changez en 16668 et a exécuté le long long immédiatement suivie par l'itération long version, le timing serait en haut pour le long long la fonction, alors en bas pour le long par exemple :

template < typename T >
void iteration() {
    int n = 25000;
    // Allocate memory
    T** blocks = new T*[n];
    for (int i = 0; i < n; ++i) {
        blocks[i] = new T[16668];
    }
    // Free all allocated memory
    for (int i = 0; i < n; ++i) {
        delete[] blocks[i];
    }
    delete[] blocks;
}

for (int i = 0; i < nbItrs; ++i) {
    iteration<long long>(); // time goes UP
}
for (int i = 0; i < nbItrs; ++i) {
    iteration<long>(); // time goes DOWN
}

De plus, le code affiché produit des résultats similaires en utilisant malloc / free , LocalAlloc / LocalFree et/ou HeapAlloc / HeapFree depuis new / malloc (sous Windows) appelle HeapAlloc . La raison est liée à la façon dont Windows gère sa mémoire de tas et les emplacements de la mémoire libérée. Lorsque des pages doivent être supprimées, un nettoyage de la liste des blocs libres doit être effectué et la liste doit être ajustée en conséquence.

C'est cet ajustement qui peut prendre du temps, pendant la recherche et le remplacement ou la suppression des anciens blocs de mémoire de la liste. Si les blocs ne tombent pas sur des frontières propres, il peut être nécessaire de procéder à des ajustements supplémentaires de la liste des blocs libres du tas.

Pour approfondir le pourquoi et le comment de la gestion du tas de Windows, il faudrait expliquer la conception du noyau de Windows et sa gestion de la mémoire. Cela dépasserait le cadre de cette question/réponse, un peu de de la articles J'ai fait le lien avec au-dessus de ont d'excellentes vues d'ensemble et expliquent très bien le comment et le pourquoi.

Cependant, vous avez demandé

comment faire pour que le code soit stable sous Windows ?

Comme indiqué plus haut, le changement de type permettrait d'obtenir un calendrier plus cohérent. réponse En supprimant la liste dans l'ordre inverse, vous obtiendrez également des temps plus cohérents ;

for (int i = n; i > 0; --i )
{
    delete[] blocks[i-1];
}

Cela est dû au fait que le gestionnaire de mémoire de Windows utilise une liste singulièrement liée pour maintenir les emplacements du tas, ce qui explique pourquoi les temps peuvent augmenter lorsque le programme est en cours d'exécution. delete C'est pourquoi les temps peuvent être plus lents sous Windows que sous Linux (bien que mes tests aient produit des temps similaires dans les deux cas).

J'espère que cela pourra vous aider.

2voto

Tony D Points 43962

Un problème intéressant. J'ai pu le reproduire.

J'obtiens des performances cohérentes - bien que toujours un peu lentes - en delete[] -les blocs dans l'ordre inverse de leur attribution :

for( int i = 0; i<n; ++i )
    delete[] blocks[n - 1 - i];

Je soupçonne que tout cela peut être lié à la fusion des frais généraux - du MSDN ici :

Ralentissement suite à des opérations libres. Les opérations libres consomment plus de cycles, principalement si la coalescence est activée. Pendant la coalescence, chaque opération de libération doit "trouver" ses voisins, les extraire pour construire un bloc plus grand, et réinsérer le bloc plus grand dans la liste de libération. Pendant cette recherche, la mémoire peut être touchée dans un ordre aléatoire, ce qui entraîne des manques dans le cache et un ralentissement des performances.

Il y a quand même quelques trucs bizarres :

  • mes mesures ont montré que si delete[] a pris ~80% du temps sur la première ou la troisième itération, après une demi-douzaine d'autres new[] prenait presque autant de temps.

  • le problème est apparu très soudainement lorsque je suis passé de new long[91134] à ... 91135 : c'est presque 356kb, mais je n'ai pas réussi à googler quoi que ce soit à ce sujet.

1voto

no one special Points 343

Un problème très intéressant. Je ne peux pas le reproduire sur Windows 10 avec MS Visual Studio Community 2013, cependant si vous allouez / désallouez beaucoup de blocs de mémoire avec une taille fixe, vous pourriez essayer de remplacer new/delete par un algorithme d'allocation de blocs de mémoire à taille fixe, également connu sous le nom de pool de mémoire. Il fonctionne beaucoup plus rapidement et à vitesse constante. Vous trouverez ici un exemple basé sur la licence BSD : https://github.com/intelmm/FixMemAlloc/blob/master/Sources/MemoryPool.h . Peut-être que ça pourrait aider.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X