Estoy trabajando con la clase MemoryCache .NET 4.0 en una aplicación y trato de limitar el tamaño máximo de caché, pero en mis pruebas no parece que la caché esté cumpliendo los límites.
Estoy usando la configuración que, según MSDN , se supone que limita el tamaño de la caché:
- CacheMemoryLimitMegabytes : el tamaño máximo de memoria, en megabytes, al que puede crecer una instancia de un objeto ".
- PhysicalMemoryLimitPercentage : "El porcentaje de memoria física que puede usar la caché, expresado como un valor entero de 1 a 100. El valor predeterminado es cero, lo que indica que las instancias de MemoryCache administran su propia memoria 1 según la cantidad de memoria que está instalada en el computadora." 1. Esto no es del todo correcto: cualquier valor por debajo de 4 se ignora y se reemplaza por 4.
Entiendo que estos valores son aproximados y no límites estrictos, ya que el hilo que purga el caché se activa cada x segundos y también depende del intervalo de sondeo y otras variables no documentadas. Sin embargo, incluso teniendo en cuenta estas variaciones, veo tamaños de caché tremendamente inconsistentes cuando el primer elemento se expulsa de la caché después de configurar CacheMemoryLimitMegabytes y PhysicalMemoryLimitPercentage juntos o de forma singular en una aplicación de prueba. Para estar seguro, ejecuté cada prueba 10 veces y calculé la cifra promedio.
Estos son los resultados de probar el código de ejemplo a continuación en una PC con Windows 7 de 32 bits con 3 GB de RAM. El tamaño de la caché se toma después de la primera llamada a CacheItemRemoved () en cada prueba. (Soy consciente de que el tamaño real de la caché será mayor que esto)
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
Aquí está la aplicación de prueba:
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");
}
}
}
¿Por qué MemoryCache no obedece los límites de memoria configurados?
fuente
Respuestas:
Vaya, pasé demasiado tiempo buscando en el CLR con reflector, pero creo que finalmente tengo una buena idea de lo que está pasando aquí.
La configuración se está leyendo correctamente, pero parece haber un problema profundamente arraigado en el CLR que parece que hará que la configuración del límite de memoria sea esencialmente inútil.
El siguiente código se refleja fuera de la DLL System.Runtime.Caching, para la clase CacheMemoryMonitor (hay una clase similar que monitorea la memoria física y se ocupa de la otra configuración, pero esta es la más importante):
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); }
Lo primero que puede notar es que ni siquiera intenta mirar el tamaño de la caché hasta después de una recolección de basura Gen2, sino que simplemente recurre al valor de tamaño almacenado existente en cacheSizeSamples. Por lo tanto, nunca podrá dar en el blanco directamente, pero si el resto funciona, al menos obtendríamos una medición del tamaño antes de tener problemas reales.
Entonces, asumiendo que se ha producido un GC Gen2, nos encontramos con el problema 2, que es que ref2.ApproximateSize hace un trabajo horrible al aproximarse realmente al tamaño de la caché. Trabajando a través de la basura CLR, encontré que esto es un System.SizedReference, y esto es lo que está haciendo para obtener el valor (IntPtr es un identificador del objeto MemoryCache en sí):
[SecurityCritical] [MethodImpl(MethodImplOptions.InternalCall)] private static extern long GetApproximateSizeOfSizedRef(IntPtr h);
Supongo que la declaración externa significa que se sumerge en la tierra de ventanas no administradas en este punto, y no tengo idea de cómo comenzar a descubrir qué hace allí. Sin embargo, por lo que he observado, hace un trabajo horrible al tratar de aproximar el tamaño del conjunto.
La tercera cosa notable es la llamada a manager.UpdateCacheSize, que parece que debería hacer algo. Desafortunadamente, en cualquier muestra normal de cómo debería funcionar, s_memoryCacheManager siempre será nulo. El campo se establece desde el miembro público estático ObjectCache.Host. Esto está expuesto para que el usuario se meta con él si así lo desea, y de hecho pude hacer que esto funcione como se supone que debe hacer al juntar mi propia implementación de IMemoryCacheManager, configurándola en ObjectCache.Host y luego ejecutando la muestra . Sin embargo, en ese punto, parece que también podría hacer su propia implementación de caché y ni siquiera molestarse con todas estas cosas, especialmente porque no tengo idea si configurar su propia clase en ObjectCache.Host (estático,
Tengo que creer que al menos parte de esto (si no un par de partes) es simplemente un error. Sería bueno saber de alguien de MS cuál fue el trato con esto.
Versión TLDR de esta respuesta gigante: suponga que CacheMemoryLimitMegabytes está completamente destruido en este momento. Puede configurarlo en 10 MB y luego proceder a llenar el caché a ~ 2 GB y hacer una excepción de falta de memoria sin que se interrumpa la eliminación del elemento.
fuente
Sé que esta respuesta es muy tarde, pero más vale tarde que nunca. Quería informarle que escribí una versión
MemoryCache
que resuelve los problemas de la Colección Gen 2 automáticamente para usted. Por lo tanto, recorta cada vez que el intervalo de sondeo indica presión de memoria. Si está experimentando este problema, ¡pruébelo!http://www.nuget.org/packages/SharpMemoryCache
También puede encontrarlo en GitHub si tiene curiosidad sobre cómo lo resolví. El código es algo simple.
https://github.com/haneytron/sharpmemorycache
fuente
2n + 20
tamaño aproximado con respecto a los bytes, donden
es la longitud de la cadena. Esto se debe principalmente a la compatibilidad con Unicode.He hecho algunas pruebas con el ejemplo de @Canacourse y la modificación de @woany y creo que hay algunas llamadas críticas que bloquean la limpieza de la memoria caché.
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(); }
Pero, ¿por qué la modificación de @woany parece mantener la memoria al mismo nivel? En primer lugar, RemovedCallback no está configurado y no hay salida de consola o esperando entrada que pueda bloquear el hilo de la memoria caché.
En segundo lugar...
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) cada ~ 1000th AddItem () tendría el mismo efecto.
Bueno, no es una investigación muy profunda del problema, pero parece que el hilo de MemoryCache no obtiene suficiente tiempo de CPU para limpiar, mientras que se agregan muchos elementos nuevos.
fuente
También encontré este problema. Estoy almacenando en caché objetos que se disparan en mi proceso docenas de veces por segundo.
He encontrado que la siguiente configuración y uso libera los elementos cada 5 segundos la mayor parte del tiempo .
App.config:
Tome nota de cacheMemoryLimitMegabytes . Cuando se establece en cero, la rutina de purga no se activa en un tiempo razonable.
<system.runtime.caching> <memoryCache> <namedCaches> <add name="Default" cacheMemoryLimitMegabytes="20" physicalMemoryLimitPercentage="0" pollingInterval="00:00:05" /> </namedCaches> </memoryCache> </system.runtime.caching>
Agregar al caché:
MemoryCache.Default.Add(someKeyValue, objectToCache, new CacheItemPolicy { AbsoluteExpiration = DateTime.Now.AddSeconds(5), RemovedCallback = cacheItemRemoved });
Confirmando que la eliminación de caché está funcionando:
void cacheItemRemoved(CacheEntryRemovedArguments arguments) { System.Diagnostics.Debug.WriteLine("Item removed from cache: {0} at {1}", arguments.CacheItem.Key, DateTime.Now.ToString()); }
fuente
(Afortunadamente) me encontré con esta útil publicación ayer cuando intenté por primera vez usar MemoryCache. Pensé que sería un caso simple de establecer valores y usar las clases, pero encontré problemas similares descritos anteriormente. Para intentar ver qué estaba pasando, extraje la fuente usando ILSpy y luego configuré una prueba y recorrí el código. Mi código de prueba era muy similar al código anterior, así que no lo publicaré. De mis pruebas noté que la medición del tamaño de la caché nunca fue particularmente precisa (como se mencionó anteriormente) y dada la implementación actual nunca funcionaría de manera confiable. Sin embargo, la medición física estuvo bien y si la memoria física se midió en cada encuesta, me pareció que el código funcionaría de manera confiable. Entonces, eliminé la verificación de recolección de basura gen 2 dentro de MemoryCacheStatistics;
En un escenario de prueba, esto obviamente hace una gran diferencia, ya que el caché se golpea constantemente, por lo que los objetos nunca tienen la oportunidad de llegar a la generación 2. Creo que usaremos la compilación modificada de este dll en nuestro proyecto y usaremos el MS oficial. compilar cuando salga .net 4.5 (que de acuerdo con el artículo de conexión mencionado anteriormente debería tener la solución). Lógicamente, puedo ver por qué se ha implementado la verificación de gen 2, pero en la práctica no estoy seguro de si tiene mucho sentido. Si la memoria alcanza el 90% (o cualquier límite en el que se haya establecido), entonces no debería importar si se ha producido una recopilación de generación 2 o no, los elementos deben ser desalojados independientemente.
Dejé mi código de prueba ejecutándose durante unos 15 minutos con un PhysicalMemoryLimitPercentage establecido en 65%. Vi que el uso de memoria se mantuvo entre 65-68% durante la prueba y vi que las cosas se desalojaban correctamente. En mi prueba configuré pollingInterval en 5 segundos, PhysicalMemoryLimitPercentage en 65 y PhysicalMemoryLimitPercentage en 0 para establecer esto por defecto.
Siguiendo el consejo anterior; Se podría realizar una implementación de IMemoryCacheManager para desalojar cosas de la caché. Sin embargo, sufriría el problema de verificación de gen 2 mencionado. Aunque, dependiendo del escenario, esto puede no ser un problema en el código de producción y puede funcionar suficientemente para las personas.
fuente
Resultó que no es un error, todo lo que necesita hacer es establecer el período de tiempo de agrupación para hacer cumplir los límites, parece que si deja la agrupación no configurada, nunca se activará. Lo acabo de probar y no es necesario utilizar envoltorios o cualquier código adicional:
private static readonly NameValueCollection Collection = new NameValueCollection { {"CacheMemoryLimitMegabytes", "20"}, {"PollingInterval", TimeSpan.FromMilliseconds(60000).ToString()}, // this will check the limits each 60 seconds };
Establezca el valor de "
PollingInterval
" en función de la rapidez con la que crece la caché; si crece demasiado rápido, aumente la frecuencia de las comprobaciones de sondeo; de lo contrario, mantenga las comprobaciones no muy frecuentes para no causar gastos generales.fuente
Si usa la siguiente clase modificada y monitorea la memoria a través del Administrador de tareas, de hecho se recorta:
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()); } }
fuente
MemoryCache
. Me pregunto por qué funciona esta muestra.