2017-08-29 5 views
1

je rencontre ce problème bizarre performance:application C# les performances du processeur ralentit de façon drastique lorsque le programme crée un grand nombre d'objets

  1. J'ai une application C# qui crée des millions d'objets C#.

  2. Dans une partie non liée du code, l'application ne une œuvre qui ne dépend pas des données allouées à l'étape 1.

Le temps CPU semblent être en corrélation avec le nombre d'objets créé à l'étape 1.

J'ai écrit un simple cas C# qui reproduit mon problème. slowdown La commande est appelée avec le nombre de millions d'objets chaîne créés avant l'appel de la méthode DoMyWork(). Comme vous pouvez le voir, la même méthode DoMyWork() peut prendre jusqu'à 3 s si 200M de chaînes sont instanciées.

  • Quelque chose me manque dans la langue?
  • Supposons que la limite de mémoire physique n'est pas atteinte, y a-t-il un nombre maximum d'objets qui ne devraient pas être atteints sinon le CLR ralentirait?

j'ai couru mon test sous Windows 10 sur Intel Core i7-6700 et mon programme est une version de la console intégrée en mode 32 bits (VS 2017 - fw 4.6.1):

slowdown 0 
Allocating 40000 hashtables: 2 ms 
Allocating 40000 hashtables: 4 ms 
Allocating 40000 hashtables: 15 ms 
Allocating 40000 hashtables: 2 ms 
Allocating 40000 hashtables: 5 ms 
Allocating 40000 hashtables: 5 ms 
Allocating 40000 hashtables: 2 ms 
Allocating 40000 hashtables: 18 ms 
Allocating 40000 hashtables: 10 ms 
Allocating 40000 hashtables: 19 ms

ralentissement 0 utilise ~ 30M

slowdown 200 
Allocating 40000 hashtables: 392 ms 
Allocating 40000 hashtables: 1120 ms 
Allocating 40000 hashtables: 3067 ms 
Allocating 40000 hashtables: 2 ms 
Allocating 40000 hashtables: 31 ms 
Allocating 40000 hashtables: 418 ms 
Allocating 40000 hashtables: 15 ms 
Allocating 40000 hashtables: 2 ms 
Allocating 40000 hashtables: 18 ms 
Allocating 40000 hashtables: 416 ms

ralentissement 200 utilisations ~ 800M


using System; 
using System.Diagnostics; 
using System.Collections; 

namespace SlowDown 
{ 
    class Program 
    { 
    static string[] arr; 

    static void CreateHugeStringArray(long size) 
    { 
     arr = new string[size * 1000000]; 
     for (int i = 0; i < arr.Length; i++) arr[i] = ""; 
    } 


    static void DoMyWork() 
    { 
     int n = 40000; 
     Console.Write("Allocating " + n + " hashtables: "); 
     Hashtable[] aht = new Hashtable[n]; 

     for (int i = 0; i < n; i++) 
     { 
     aht[i] = new Hashtable(); 
     } 
    } 


    static void Main(string[] args) 
    { 
     if (0 == args.Length) return; 
     CreateHugeStringArray(Convert.ToInt64(args[0])); 

     for (int i = 0; i < 10 ; i++) 
     { 
     Stopwatch sw = Stopwatch.StartNew(); 
     DoMyWork(); 
     sw.Stop(); 
     Console.Write(sw.ElapsedMilliseconds + " ms\n"); 
     } 
    } 
    } 
} 
+2

Dans votre exemple, vous ne créez pas « millions d'objets C# ». Vous créez un grand nombre de chaînes, et chaque élément du tableau pointe vers le même objet chaîne (voir string interning). – dymanoid

+0

Comment cela fonctionne si vous essayez d'ajouter un entier à integer dans 'DoMyWork', car il y a une opération de boxe impliquée dans la logique existante. Vous pouvez également utiliser 'StringBuilder' tout en manipulant plusieurs fois la même chaîne. –

+1

Ajoutez 'GC.Collect()' juste après 'CreateHugeStringArray' et voyez si vous obtenez toujours les mêmes heures. –

Répondre

1

probable choses désagréables Garbage Collector, qui peut vous geler thread principal, même si elle travaille principalement sur un fil d'arrière-plan, comme mentionné ici: Garbage Collector Thread

Si vous collectez, les restes de temps (dans mon cas) autour 90ms indépendamment de la taille du tableau "non apparenté".

+0

En fait, dans le cas réel, mon application charge une énorme quantité de données qui est utilisée plus tard, donc il ne doit pas être collecté. J'ai mis à jour mon article en ajoutant un cas 2 où la référence de données est conservée dans une référence statique. Ce faisant, le tableau ne sera jamais collecté. Cependant, les performances d'exécution sont deux fois moins bonnes alors que le GC ne devrait rien faire avec les données. Cela me fait penser que le problème de ralentissement se produit lorsque l'ensemble des données reste alloué. Le thread GC peut regarder tous les objets actifs qui diminuent les performances de l'application globale. – guista

1

Le problème est dû au fait que Garbage Collector s'exécute en même temps que votre DoMyWork. La taille du tableau dont il a besoin pour nettoyer «interrompt» le vrai travail.

Pour voir l'impact du GC, ajoutez ces lignes avant StartNew appel - afin que le travail de GC se avant au calendrier:

GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; 
GC.Collect(); 
+0

Je viens de modifier le code de sorte que l'arr est un champ statique de la classe, donc une fois créé par CreateHugeStringArray il ne va pas être collecté - et ensuite mettre la section DoMyWork dans une boucle for. La première fois à travers la boucle for est typiquement de 350 ms, toutes les autres fois dans la boucle sont d'environ 50 ms - toute idée du temps supplémentaire requis dans ce cas. – PaulF

+1

Il s'agit probablement du JIT @PaulF. – mjwills

+0

Aurait dû y penser. – PaulF

0

Le code suivant crée 10000 nouveaux objets chaîne, forçant la collecte des ordures à exécuter:

string str = ""; 

for (int i = 0; i < 10000; i++) str += i; 

les performances du garbage collector est proportionnelle à

  • Le nombre d'objets qui ont été attribués
  • La quantité totale de mémoire utilisée

Votre CreateHugeStringArray() alloue des objets très grandes, ce qui augmente la quantité totale de mémoire utilisée. Dans des cas extrêmes, des parties de cette mémoire peuvent être sur le disque (paginé), ce qui ralentit encore plus le système.

La morale de votre histoire est - n'allouez pas de mémoire sauf si vous en avez besoin.

0

N'a pas encore trouvé la raison, mais il semble que le fait d'avoir un grand tableau dans LOH ralentit considérablement la récupération de place. Cependant, si nous créons de nombreux tableaux plus petits pour contenir la même quantité de données (ce qui va à la génération 2 au lieu de LOH), le GC ne ralentit pas beaucoup. Il semble que le tableau avec des pointeurs de chaîne 1kk occupe environ 4 millions d'octets de mémoire. Donc, pour éviter d'avoir LOH, le tableau doit occuper moins de 85 kilo-octets. C'est environ 50 fois moins. Vous pouvez utiliser vieux truc pour diviser grand tableau en plusieurs petits tableaux

private static string[][] arrayTwoDimentional; 

    private static int _arrayLength = 1000000; 

    private static int _sizeFromExample = 200; 

    static void CreateHugeStringArrayTwoDimentional() 
    { 
     // Make 50 times more smaller arrays 
     arrayTwoDimentional = new string[_sizeFromExample * 50][]; 

     for (long i = 0; i < arrayTwoDimentional.Length; i++) 
     { 
      // Make array smaller 50 times 
      arrayTwoDimentional[i] = new string[_arrayLength/50]; 
      for (var index = 0; index < arrayTwoDimentional[i].Length; index++) 
      { 
       arrayTwoDimentional[i][index] = ""; 
      } 
     } 
    } 

    static string GetByIndex(long index) 
    { 
     var arrayLenght = _arrayLength/50; 
     var firstIndex = index/arrayLenght; 
     var secondIndex = index % arrayLenght; 

     return arrayTwoDimentional[firstIndex][secondIndex]; 
    } 

La preuve que GC est goulot d'étranglement ici

Inside DoMyWork Après avoir remplacé la mise en page du tableau

After moving

Dans l'exemple, un tableau les tailles sont codées en dur. Il y a un bon exemple CodeProject comment vous pouvez calculer la taille de l'objet de type stocké, ce qui contribuera à ajuster la taille des tableaux: https://www.codeproject.com/Articles/129541/NET-memory-problem-with-uncontrolled-LOH-size-and