如何测量 .NET 内存缓存 4.0 的当前大小?

Posted

技术标签:

【中文标题】如何测量 .NET 内存缓存 4.0 的当前大小?【英文标题】:How to measure current size of .NET Memory Cache 4.0? 【发布时间】:2014-03-13 22:49:00 【问题描述】:

目前我们正在使用 .NET Memory Cache 4.0 来满足缓存要求。 (不是 ASP.NET 缓存,不是任何外部缓存)

查看“.NET Memory Cache 4.0”性能计数器,有关于缓存命中、未命中、条目、修剪等的数据,但与大小无关。

有没有一种方法可以测量/了解生产应用程序使用的缓存的当前大小

我希望能够在不同的时间点捕获这些数据并获得缓存的平均大小。

【问题讨论】:

这可能有某种关联***.com/questions/1128315/… 感谢链接“核糖”。但我正在寻找一种性能计数器/外部命令行开关(应用程序结构统计)的方式来获取这些数据。不使用内联代码自己测量内存。 另外,到目前为止,我发现进程的“私有工作集 64”已经给出了内存使用量的估计值。这不是最准确的数字,但是我现在能得到的最好的数字。我正在寻找更可靠和准确的指针。 【参考方案1】:

这是一个丑陋的实现细节,微软根本不想公开。在 .NET 中测量对象大小通常是不可能的。 MemoryCache 使用一个相当讨厌的后门来实现其内存限制触发器,它使用 CLR 的 DACCESS 组件,实际上是为了帮助实现内存分析器。

您可以使用调试器查看它,因此您不会无法访问它。你只需要编写非常丑陋的代码来挖掘私有字段:

using System;
using System.Reflection;
using System.Runtime.Caching;

public static class MemoryCacheHackExtensions 
    public static long GetApproximateSize(this MemoryCache cache) 
        var statsField = typeof(MemoryCache).GetField("_stats", BindingFlags.NonPublic | BindingFlags.Instance);
        var statsValue = statsField.GetValue(cache);
        var monitorField = statsValue.GetType().GetField("_cacheMemoryMonitor", BindingFlags.NonPublic | BindingFlags.Instance);
        var monitorValue = monitorField.GetValue(statsValue);
        var sizeField = monitorValue.GetType().GetField("_sizedRef", BindingFlags.NonPublic | BindingFlags.Instance);
        var sizeValue = sizeField.GetValue(monitorValue);
        var approxProp = sizeValue.GetType().GetProperty("ApproximateSize", BindingFlags.NonPublic | BindingFlags.Instance);
        return (long)approxProp.GetValue(sizeValue, null);
    

似乎在 .NET 4.6.1 上运行良好,未经过广泛测试。了解情况是可以的,只是不要依赖它,因为它可能会因任何 .NET 更新而中断。

【讨论】:

感谢@hans 的回答,但正如我在问题中提到的,我正在寻找一个外部工具来测量这种(非检测代码)类型的 appfabrics-stats 命令行开关,即使我必须在机器上运行它们。我想,作为内存缓存,以及性能计数器没有暴露的事实,采用代码方式可能是衡量大小的唯一方法。我们的代码是一个繁忙的生产代码,我真的不想用反射/快速修复代码戳它,因为我们的应用程序域中的多个层使用各种命名缓存并进行大量读/写。很高兴了解这种方法 这是不可能的,MemoryCache 不会使该测量值在外部可见。您只能在进程中进行。 我的 sizeref 始终为零。我可以在调试模式下看到缓存中的值,但是这段代码总是返回零 在 .net 4.7.2 上,代码中断必须将“_sizedRef”更改为“_sizedRefMultiple”,但我的尺寸为零 gpanagakis 修复将 _sizedRef 更改为 _sizedRefMultiple 对我有用。当您得到零时,请等待两分钟,因为这似乎是更新统计信息的轮询间隔。【参考方案2】:

我采用了原始代码并进行了小幅调整,我使用 "_sizedRefMultiple" 而不是 "_sizedRef" 使其适用于 .NET 4.6.

public static class MemoryCacheHackExtensions

    public static long GetApproximateSize(this MemoryCache cache)
    
        var statsField = typeof(MemoryCache).GetField("_stats", BindingFlags.NonPublic | BindingFlags.Instance);
        var statsValue = statsField.GetValue(cache);
        var monitorField = statsValue.GetType().GetField("_cacheMemoryMonitor", BindingFlags.NonPublic | BindingFlags.Instance);
        var monitorValue = monitorField.GetValue(statsValue);
        var sizeField = monitorValue.GetType().GetField("_sizedRefMultiple", BindingFlags.NonPublic | BindingFlags.Instance);
        var sizeValue = sizeField.GetValue(monitorValue);
        var approxProp = sizeValue.GetType().GetProperty("ApproximateSize", BindingFlags.NonPublic | BindingFlags.Instance);
        return (long)approxProp.GetValue(sizeValue, null);
    

【讨论】:

我的 sizerefmultiple 始终为零。我可以在调试模式下看到缓存中的值,但是这段代码总是返回零 必须在 .NET 4.5 上使用相同的技巧 ' var sizeVarName = "_sizedRef"; #if NET45 sizeVarName = "_sizedRefMultiple"; #endif '【参考方案3】:

作为替代方案,您还可以实现IMemoryCacheManager 接口并将其分配给全局ObjectCache.Host 属性。 这要求您被允许这样做,即您的应用程序中没有其他组件已经这样做(想到 ASP.NET,但我不确定)。就个人而言,我在控制台/Windows 服务应用程序中使用该方法没有任何问题。

另请注意,您只会在大约完整的 GC 之后获得缓存大小,但这与 Hans' approach 应该没有任何不同。

另请注意,以下代码适用于命名的 MemoryCache,即不适用于实例本身。

相当“但是”的观点。但是,它不需要反射。

所以,这里是代码。

public static class MemoryCacheHelper

    private static readonly MemoryCacheServiceProvider s_serviceProvider = new MemoryCacheServiceProvider();

    static MemoryCacheHelper()
    
        try
        
            ObjectCache.Host = s_serviceProvider;
        
        catch (InvalidOperationException ex)
        
            // ObjectCache.Host can only be set once.
        
    

    public static MemoryCache Create(string name, NameValueCollection config) 
    
        return new MemoryCache(name, config);
    

    // Return approximate cache size and when that value was last determined.
    public static Tuple<long, DateTime> GetApproximateSize(string name)
    
        return s_serviceProvider.GetApproximateSize(cache.Name);
    

    private class MemoryCacheServiceProvider : IMemoryCacheManager, IServiceProvider
    
        private readonly object m_lock = new object();
        private readonly IDictionary<string, Tuple<long, DateTime>> m_sizes = new Dictionary<string, Tuple<long, DateTime>>();

        public Tuple<long, DateTime> GetApproximateSize(string name)
        
            lock (m_lock)
            
                Tuple<long, DateTime> info;
                if (m_sizes.TryGetValue(name, out info))
                    return info;
                return null;
            
        

        void IMemoryCacheManager.UpdateCacheSize(long size, MemoryCache cache)
        
            lock (m_lock)
            
                // The UpdateCacheSize() method will be called based on the configured "pollingInterval"
                // for the respective cache. That value defaults to 2 minutes. So this statement doesn't
                // fire to often and as a positive side effect we get some sort of "size-heartbeat" which
                // might help when troubleshooting.
                m_sizes[cache.Name] = Tuple.Create(size, DateTime.UtcNow);
            
        

        void IMemoryCacheManager.ReleaseCache(MemoryCache cache)
        
            lock (m_lock)
            
                m_sizes.Remove(cache.Name);
            
        

        object IServiceProvider.GetService(Type serviceType)
        
            if (serviceType == typeof(IMemoryCacheManager))
            
                return this;
            

            return null;
        
    

【讨论】:

感谢@christian.K 的回答,但正如我在问题中提到的那样,我正在寻找一个外部工具来测量这种(非检测代码)类型的 appfabrics-stats 命令行开关,即使我必须在机器上运行它们。我想,作为内存缓存,以及性能计数器没有暴露的事实,采用代码方式可能是衡量大小的唯一方法。我们的代码是一个繁忙的生产代码,我真的不想用单个主机戳它,因为我们的应用程序域中的多个层使用各种命名缓存。但很高兴知道这些代码级选项 正如@HansPassant 所说,实际上并没有外部工具。一个可能的选项,尽管相当复杂,是使用类似Microsoft.Runtime.Diagnostics(基本上是WinDbg的托管版本)并尝试在运行时(和/或从外部您使用此包编码的工具)。当然,您也可以在批处理模式下运行 WinDbg 和脚本来实现相同的目的。我不确定这是否是您在生产中应该做的事情(最终故障排除除外),所以我不会为此提供答案。 这正是我想要的!注意:当我在 LINQPad 中尝试上述代码时,起初它似乎不起作用。那是因为“UpdateCacheSize”被缓存on an interval调用,默认为2分钟。如果您希望看到它更频繁地更新(或者您不耐烦),您必须通过 app.config 或通过构造函数参数更改该间隔,例如:new MemoryCache("foo", new NameValueCollection "PollingInterval", "00:00:01" ) 当然你必须调用 GC.Collect() 来强制在测试应用程序中进行完整的 GC 才能看到 UpdateCacheSize 被调用,否则它可能永远不会被调用。不过,切勿在实际应用程序中调用 GC.Collect()【参考方案4】:

我编写了一个扩展方法,它遍历对象的公共属性并总结大小值。如果它是 IEnumerable 或 Array,它将遍历项目并计算它们的大小。这不是最好的方法,但对我来说足够好,因为我的缓存没有比这更复杂的要求。

注意:每个缓存条目似乎有大约 600 字节的开销,您应该将其添加到计算中。

    public static class ObjectMemorySizeCalculator
    
        static int OBJECT_SIZE = IntPtr.Size == 8 ? 24 : 12;
        static int POINTER_SIZE = IntPtr.Size;
        public static long GetMemorySize(this object obj)
        
            long memorySize = 0;
            Type objType = obj.GetType();

            if (objType.IsValueType)
            
                memorySize = Marshal.SizeOf(obj);
            
            else if (objType.Equals(typeof(string)))
            
                var str = (string)obj;
                memorySize = str.Length * 2 + 6 + OBJECT_SIZE;
            
            else if (objType.IsArray)
            
                var arr = (Array)obj;
                var elementType = objType.GetElementType();
                if (elementType.IsValueType)
                
                    long elementSize = Marshal.SizeOf(elementType);
                    long elementCount = arr.LongLength;
                    memorySize += elementSize * elementCount;
                                    
                else
                
                    foreach (var element in arr)
                    
                        memorySize += element != null ? element.GetMemorySize() + POINTER_SIZE : POINTER_SIZE;
                    
                

                memorySize += OBJECT_SIZE + 40;
            
            else if (obj is IEnumerable)
            
                var enumerable = (IEnumerable)obj;
                foreach(var item in enumerable)
                
                    var itemType = item.GetType();
                    memorySize += item != null ? item.GetMemorySize() : 0;
                    if (itemType.IsClass)
                        memorySize += POINTER_SIZE;
                
                memorySize += OBJECT_SIZE;
            
            else if (objType.IsClass)
            
                var properties = objType.GetProperties();
                foreach (var property in properties)
                
                    var valueObject = property.GetValue(obj);
                    memorySize += valueObject != null ? valueObject.GetMemorySize() : 0;
                    if (property.GetType().IsClass)
                        memorySize += POINTER_SIZE;
                
                memorySize += OBJECT_SIZE;
                            

            return memorySize;
        

【讨论】:

【参考方案5】:

在尝试获取 MemoryDistributedCache 的大小时,我受到 Hans Passant 的回答的启发。代码如下所示:

public static class MemoryDistributedCacheHackExtensions

    public static long GetCacheSize(this MemoryDistributedCache cache)
    
        var memCacheField = typeof(MemoryDistributedCache).GetField("_memCache", BindingFlags.NonPublic | BindingFlags.Instance);
        var memCacheValue = memCacheField.GetValue(cache);
        var cacheSizeField = memCacheValue.GetType().GetField("_cacheSize", BindingFlags.NonPublic | BindingFlags.Instance);
        var cacheSizeValue = cacheSizeField.GetValue(memCacheValue);
        return (long)cacheSizeValue;
    

因此,这不是原始问题的答案。我想把代码放在这里,以防其他人遇到我遇到的同样问题。

【讨论】:

【参考方案6】:

从 .NET 4.7 开始,您可以使用 GetLastSize()

https://docs.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache.getlastsize?view=dotnet-plat-ext-6.0

【讨论】:

以上是关于如何测量 .NET 内存缓存 4.0 的当前大小?的主要内容,如果未能解决你的问题,请参考以下文章

了解java垃圾收集中的足迹测量

如何测量当前 node.js 进程的峰值内存使用量

TensorFlow:如何测量每个张量占用多少 GPU 内存?

.NET 如何测量请求中分配的字节数?

这是测量对象内存大小的有效方法吗?

如何确定 ASP.Net 缓存的总大小?