Je travaille avec la classe .NET 4.0 MemoryCache dans une application et j'essaie de limiter la taille maximale du cache, mais dans mes tests, il ne semble pas que le cache obéit réellement aux limites.
J'utilise les paramètres qui, selon MSDN , sont censés limiter la taille du cache:
- CacheMemoryLimitMegabytes : taille maximale de la mémoire, en mégaoctets, à laquelle une instance d'un objet peut atteindre. "
- PhysicalMemoryLimitPercentage : "Le pourcentage de mémoire physique que le cache peut utiliser, exprimé sous la forme d'un entier compris entre 1 et 100. La valeur par défaut est zéro, ce qui indique que lesinstances MemoryCache gèrent leur propre mémoire 1 en fonction de la quantité de mémoire installée sur le ordinateur." 1. Ce n'est pas tout à fait correct - toute valeur inférieure à 4 est ignorée et remplacée par 4.
Je comprends que ces valeurs sont approximatives et non des limites strictes car le thread qui purge le cache est déclenché toutes les x secondes et dépend également de l'intervalle d'interrogation et d'autres variables non documentées. Cependant, même en tenant compte de ces variations, je constate des tailles de cache extrêmement incohérentes lorsque le premier élément est expulsé du cache après avoir défini CacheMemoryLimitMegabytes et PhysicalMemoryLimitPercentage ensemble ou individuellement dans une application de test. Pour être sûr, j'ai exécuté chaque test 10 fois et calculé le chiffre moyen.
Voici les résultats du test de l'exemple de code ci-dessous sur un PC Windows 7 32 bits avec 3 Go de RAM. La taille du cache est prise après le premier appel à CacheItemRemoved () à chaque test. (Je suis conscient que la taille réelle du cache sera plus grande que cela)
MemLimitMB MemLimitPct AVG Cache MB on first expiry
1 NA 84
2 NA 84
3 NA 84
6 NA 84
NA 1 84
NA 4 84
NA 10 84
10 20 81
10 30 81
10 39 82
10 40 79
10 49 146
10 50 152
10 60 212
10 70 332
10 80 429
10 100 535
100 39 81
500 39 79
900 39 83
1900 39 84
900 41 81
900 46 84
900 49 1.8 GB approx. in task manager no mem errros
200 49 156
100 49 153
2000 60 214
5 60 78
6 60 76
7 100 82
10 100 541
Voici l'application de test:
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
namespace FinalCacheTest
{
internal class Cache
{
private Object Statlock = new object();
private int ItemCount;
private long size;
private MemoryCache MemCache;
private CacheItemPolicy CIPOL = new CacheItemPolicy();
public Cache(long CacheSize)
{
CIPOL.RemovedCallback = new CacheEntryRemovedCallback(CacheItemRemoved);
NameValueCollection CacheSettings = new NameValueCollection(3);
CacheSettings.Add("CacheMemoryLimitMegabytes", Convert.ToString(CacheSize));
CacheSettings.Add("physicalMemoryLimitPercentage", Convert.ToString(49)); //set % here
CacheSettings.Add("pollingInterval", Convert.ToString("00:00:10"));
MemCache = new MemoryCache("TestCache", CacheSettings);
}
public void AddItem(string Name, string Value)
{
CacheItem CI = new CacheItem(Name, Value);
MemCache.Add(CI, CIPOL);
lock (Statlock)
{
ItemCount++;
size = size + (Name.Length + Value.Length * 2);
}
}
public void CacheItemRemoved(CacheEntryRemovedArguments Args)
{
Console.WriteLine("Cache contains {0} items. Size is {1} bytes", ItemCount, size);
lock (Statlock)
{
ItemCount--;
size = size - 108;
}
Console.ReadKey();
}
}
}
namespace FinalCacheTest
{
internal class Program
{
private static void Main(string[] args)
{
int MaxAdds = 5000000;
Cache MyCache = new Cache(1); // set CacheMemoryLimitMegabytes
for (int i = 0; i < MaxAdds; i++)
{
MyCache.AddItem(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
}
Console.WriteLine("Finished Adding Items to Cache");
}
}
}
Pourquoi MemoryCache ne respecte- t- il pas les limites de mémoire configurées?
la source
Réponses:
Wow, j'ai donc passé trop de temps à fouiller dans le CLR avec réflecteur, mais je pense que j'ai enfin une bonne idée de ce qui se passe ici.
Les paramètres sont lus correctement, mais il semble y avoir un problème profond dans le CLR lui-même qui semble rendre le paramètre de limite de mémoire essentiellement inutile.
Le code suivant est reflété dans la DLL System.Runtime.Caching, pour la classe CacheMemoryMonitor (il existe une classe similaire qui surveille la mémoire physique et traite l'autre paramètre, mais c'est le plus important):
protected override int GetCurrentPressure() { int num = GC.CollectionCount(2); SRef ref2 = this._sizedRef; if ((num != this._gen2Count) && (ref2 != null)) { this._gen2Count = num; this._idx ^= 1; this._cacheSizeSampleTimes[this._idx] = DateTime.UtcNow; this._cacheSizeSamples[this._idx] = ref2.ApproximateSize; IMemoryCacheManager manager = s_memoryCacheManager; if (manager != null) { manager.UpdateCacheSize(this._cacheSizeSamples[this._idx], this._memoryCache); } } if (this._memoryLimit <= 0L) { return 0; } long num2 = this._cacheSizeSamples[this._idx]; if (num2 > this._memoryLimit) { num2 = this._memoryLimit; } return (int) ((num2 * 100L) / this._memoryLimit); }
La première chose que vous remarquerez peut-être est qu'il n'essaie même pas de regarder la taille du cache avant un garbage collection Gen2, mais simplement de revenir sur la valeur de taille stockée existante dans cacheSizeSamples. Vous ne pourrez donc jamais atteindre la cible directement, mais si le reste fonctionnait, nous aurions au moins une mesure de la taille avant d'avoir de vrais problèmes.
Donc, en supposant qu'un GC Gen2 s'est produit, nous nous heurtons au problème 2, à savoir que ref2.ApproximateSize fait un travail horrible pour approximer la taille du cache. En parcourant les ordures CLR, j'ai trouvé qu'il s'agissait d'un System.SizedReference, et c'est ce qu'il fait pour obtenir la valeur (IntPtr est un handle de l'objet MemoryCache lui-même):
[SecurityCritical] [MethodImpl(MethodImplOptions.InternalCall)] private static extern long GetApproximateSizeOfSizedRef(IntPtr h);
Je suppose que la déclaration externe signifie qu'elle plonge dans des fenêtres non gérées à ce stade, et je n'ai aucune idée de comment commencer à découvrir ce qu'elle fait là-bas. D'après ce que j'ai observé, cela fait un travail horrible en essayant d'approcher la taille de l'ensemble.
La troisième chose notable est l'appel à manager.UpdateCacheSize qui semble devoir faire quelque chose. Malheureusement, dans tout exemple normal de la façon dont cela devrait fonctionner, s_memoryCacheManager sera toujours nul. Le champ est défini à partir du membre statique public ObjectCache.Host. Ceci est exposé pour que l'utilisateur puisse jouer avec s'il le souhaite, et j'ai été en mesure de faire fonctionner cette chose comme il est censé le faire en assemblant ma propre implémentation IMemoryCacheManager, en la définissant sur ObjectCache.Host, puis en exécutant l'exemple . À ce stade cependant, il semble que vous pourriez aussi bien créer votre propre implémentation de cache et même pas vous soucier de tout cela, d'autant plus que je n'ai aucune idée si vous définissez votre propre classe sur ObjectCache.
Je dois croire qu'au moins une partie de cela (sinon quelques parties) est juste un bug. Ce serait bien d'entendre quelqu'un de MS quel était l'accord avec cette chose.
Version TLDR de cette réponse géante: supposons que CacheMemoryLimitMegabytes est complètement interrompu à ce stade. Vous pouvez le définir sur 10 Mo, puis remplir le cache à ~ 2 Go et supprimer une exception de mémoire insuffisante sans déclencher la suppression de l'élément.
la source
Je sais que cette réponse est folle tardivement, mais mieux vaut tard que jamais. Je voulais vous faire savoir que j'ai écrit une version de
MemoryCache
qui résout automatiquement les problèmes de la collection Gen 2 pour vous. Il coupe donc chaque fois que l'intervalle d'interrogation indique une pression de mémoire. Si vous rencontrez ce problème, essayez-le!http://www.nuget.org/packages/SharpMemoryCache
Vous pouvez également le trouver sur GitHub si vous êtes curieux de savoir comment je l'ai résolu. Le code est assez simple.
https://github.com/haneytron/sharpmemorycache
la source
2n + 20
taille approximative par rapport aux octets, oùn
est la longueur de la chaîne. Cela est principalement dû au support Unicode.J'ai fait quelques tests avec l'exemple de @Canacourse et la modification de @woany et je pense qu'il y a des appels critiques qui bloquent le nettoyage du cache mémoire.
public void CacheItemRemoved(CacheEntryRemovedArguments Args) { // this WriteLine() will block the thread of // the MemoryCache long enough to slow it down, // and it will never catch up the amount of memory // beyond the limit Console.WriteLine("..."); // ... // this ReadKey() will block the thread of // the MemoryCache completely, till you press any key Console.ReadKey(); }
Mais pourquoi la modification de @woany semble-t-elle maintenir la mémoire au même niveau? Premièrement, le RemovedCallback n'est pas défini et il n'y a pas de sortie de console ou d'attente d'entrée qui pourrait bloquer le thread du cache mémoire.
Deuxièmement...
public void AddItem(string Name, string Value) { // ... // this WriteLine will block the main thread long enough, // so that the thread of the MemoryCache can do its work more frequently Console.WriteLine("..."); }
Un Thread.Sleep (1) tous les ~ 1000ème AddItem () aurait le même effet.
Eh bien, ce n'est pas une enquête très approfondie sur le problème, mais il semble que le thread du MemoryCache ne dispose pas de suffisamment de temps CPU pour le nettoyage, alors que de nombreux nouveaux éléments sont ajoutés.
la source
J'ai également rencontré ce problème. Je mets en cache des objets qui sont lancés dans mon processus des dizaines de fois par seconde.
J'ai trouvé que la configuration suivante et l'utilisation libère les éléments toutes les 5 secondes la plupart du temps .
App.config:
Prenez note de cacheMemoryLimitMegabytes . Lorsqu'il était réglé sur zéro, le programme de purge ne se déclenche pas dans un délai raisonnable.
<system.runtime.caching> <memoryCache> <namedCaches> <add name="Default" cacheMemoryLimitMegabytes="20" physicalMemoryLimitPercentage="0" pollingInterval="00:00:05" /> </namedCaches> </memoryCache> </system.runtime.caching>
Ajout au cache:
MemoryCache.Default.Add(someKeyValue, objectToCache, new CacheItemPolicy { AbsoluteExpiration = DateTime.Now.AddSeconds(5), RemovedCallback = cacheItemRemoved });
Confirmer que la suppression du cache fonctionne:
void cacheItemRemoved(CacheEntryRemovedArguments arguments) { System.Diagnostics.Debug.WriteLine("Item removed from cache: {0} at {1}", arguments.CacheItem.Key, DateTime.Now.ToString()); }
la source
Je suis (heureusement) tombé sur ce post utile hier lors de la première tentative d'utilisation du MemoryCache. Je pensais que ce serait un simple cas de définition de valeurs et d'utilisation des classes, mais j'ai rencontré des problèmes similaires décrits ci-dessus. Pour essayer de voir ce qui se passait, j'ai extrait la source en utilisant ILSpy, puis mis en place un test et parcouru le code. Mon code de test était très similaire au code ci-dessus, je ne le publierai donc pas. D'après mes tests, j'ai remarqué que la mesure de la taille du cache n'était jamais particulièrement précise (comme mentionné ci-dessus) et que l'implémentation actuelle ne fonctionnerait jamais de manière fiable. Cependant, la mesure physique était bonne et si la mémoire physique était mesurée à chaque sondage, il me semblait que le code fonctionnerait de manière fiable. J'ai donc supprimé la vérification de la récupération de place de la génération 2 dans MemoryCacheStatistics;
Dans un scénario de test, cela fait évidemment une grande différence car le cache est constamment touché afin que les objets n'aient jamais la chance d'accéder à la génération 2. Je pense que nous allons utiliser la version modifiée de cette dll sur notre projet et utiliser le MS officiel build lorsque .net 4.5 sort (qui, selon l'article de connexion mentionné ci-dessus, devrait contenir le correctif). Logiquement, je peux voir pourquoi la vérification de la génération 2 a été mise en place, mais en pratique, je ne sais pas si cela a beaucoup de sens. Si la mémoire atteint 90% (ou quelle que soit la limite à laquelle elle a été définie), peu importe si une collection de génération 2 s'est produite ou non, les éléments doivent être expulsés malgré tout.
J'ai laissé mon code de test s'exécuter pendant environ 15 minutes avec un physicalMemoryLimitPercentage réglé à 65%. J'ai vu l'utilisation de la mémoire rester entre 65 et 68% pendant le test et j'ai vu les choses être expulsées correctement. Dans mon test, j'ai défini pollingInterval sur 5 secondes, physicalMemoryLimitPercentage sur 65 et physicalMemoryLimitPercentage sur 0 par défaut.
Suivre les conseils ci-dessus; une implémentation de IMemoryCacheManager pourrait être faite pour expulser des choses du cache. Il souffrirait cependant du problème de vérification de la génération 2 mentionné. Cependant, selon le scénario, cela peut ne pas être un problème dans le code de production et peut fonctionner suffisamment pour les gens.
la source
Il s'est avéré que ce n'est pas un bogue, tout ce que vous devez faire est de définir la durée de mise en commun pour appliquer les limites, il semble que si vous laissez le regroupement non défini, il ne se déclenchera jamais. ou tout code supplémentaire:
private static readonly NameValueCollection Collection = new NameValueCollection { {"CacheMemoryLimitMegabytes", "20"}, {"PollingInterval", TimeSpan.FromMilliseconds(60000).ToString()}, // this will check the limits each 60 seconds };
Définissez la valeur de "
PollingInterval
" en fonction de la vitesse de croissance du cache, si elle augmente trop rapidement, augmentez la fréquence des contrôles d'interrogation, sinon gardez les contrôles peu fréquents pour ne pas entraîner de surcharge.la source
Si vous utilisez la classe modifiée suivante et que vous surveillez la mémoire via le Gestionnaire des tâches, elle est en fait réduite:
internal class Cache { private Object Statlock = new object(); private int ItemCount; private long size; private MemoryCache MemCache; private CacheItemPolicy CIPOL = new CacheItemPolicy(); public Cache(double CacheSize) { NameValueCollection CacheSettings = new NameValueCollection(3); CacheSettings.Add("cacheMemoryLimitMegabytes", Convert.ToString(CacheSize)); CacheSettings.Add("pollingInterval", Convert.ToString("00:00:01")); MemCache = new MemoryCache("TestCache", CacheSettings); } public void AddItem(string Name, string Value) { CacheItem CI = new CacheItem(Name, Value); MemCache.Add(CI, CIPOL); Console.WriteLine(MemCache.GetCount()); } }
la source
MemoryCache
. Je me demande pourquoi cet exemple fonctionne.