Memory Cache .Net 4.0 性能测试:惊人的结果

Posted

技术标签:

【中文标题】Memory Cache .Net 4.0 性能测试:惊人的结果【英文标题】:Memory Cache .Net 4.0 performance test : astonishing result 【发布时间】:2012-07-28 13:33:45 【问题描述】:

此性能测试有误,或者系统缓存的性能异常出色?

这是我的结果: [13] 交互次数 100000 : 63 毫秒 [14] 交互次数 100000 : 139 毫秒 [12] 交互次数 100000 : 47 毫秒 [15] 交互次数 100000 : 44 毫秒 测试结束。

硬件: x86 Family 6 Model 23 Stepping GenuineIntel ~2992 Mhz 3.327 MB, 5.1.2600 Service Pack 3

using System;
using System.Collections.Generic;
using System.Runtime.Caching;
using System.Diagnostics;
using System.Threading;

namespace CacheNet40

    public class CacheTest
    
        private ObjectCache cache;

        public CacheTest()
        
            cache = MemoryCache.Default;
        

        public void AddItem(CacheItem item, double span)
        
            CacheItemPolicy cp = new CacheItemPolicy();
            cp.SlidingExpiration.Add(TimeSpan.FromMinutes(span));

            cache.Add(item, cp);
        
        public Object GetItem(string key)
        
            return cache.Get(key);
        
    

    class Program
            
        private static CacheTest Cache = new CacheTest();
        private static string allowedChars = "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ0123456789!@$?_-";
        private static int counter = 0;
        private static readonly object locker = new object();

        static string CreateRandomString(int passwordLength, int idx)
                    
            char[] chars = new char[passwordLength];
            Random rd = new Random((int)DateTime.Now.Ticks + idx);

            for (int i = 0; i < passwordLength; i++)
            
                chars[i] = allowedChars[rd.Next(0, allowedChars.Length)];
            
            return new string(chars);
        

        private static void CacheAccessTes()
        
            int span = 5;
            string key;
            string data;
            int itens = 1000;
            int interactions = 100000;
            int cont = 0;
            int index = 0;
            List<string> keys = new List<string>();

            lock (locker)
            
                counter++;
            

            cont = itens;

            //populates it with data in the cache
            do
                            
                key = CreateRandomString(127, Thread.CurrentThread.ManagedThreadId + cont);
                keys.Add(key);

                data = CreateRandomString(156000, Thread.CurrentThread.ManagedThreadId + cont + 1);

                CacheItem ci = new CacheItem(key, data);
                Cache.AddItem(ci, span);

                cont--;
            
            while (cont > 0);

            cont = interactions;
            index = 0;

            //test readings
            Stopwatch stopWatch = new Stopwatch();
            stopWatch.Start();            
            do
            
                Object ci = Cache.GetItem(keys[index]);

                ci = null;
                index++;
                if (index == itens)
                
                    index = 0;
                

                cont--;
            
            while (cont > 0);
            stopWatch.Stop();

            lock (locker)
            
                counter--;
            

            string outstring = String.Format("[0] number of interactions 1 : 2 milliseconds", Thread.CurrentThread.ManagedThreadId, interactions, stopWatch.ElapsedMilliseconds );
            Console.WriteLine(outstring);
        

        static void Main(string[] args)
        
            for (int threads = 0; threads < 4; threads++)
            
                Thread thread = new Thread(new ThreadStart(CacheAccessTes));
                thread.Start();
            

            Thread.Sleep(1000);

            while (true)
            
                lock (locker)
                
                    if (counter == 0) break;
                
                Thread.Sleep(100);
            

            Console.WriteLine("End of test.");
            Console.ReadLine();
        
    

【问题讨论】:

对我来说看起来不错。是什么让你怀疑结果? 这里的实际问题是什么?究竟什么是惊人的? 我发现了非凡的表现,这让我很怀疑。 【参考方案1】:

我刚刚在网上搜索了有关 MemoryCache 性能的信息,并偶然发现了这个 SO 问题。我问自己为什么没有使用合适的基准测试库,所以我最终通过非常懒惰来制作自己的基准测试(所有优秀的程序员都应该 :-) 并使用令人难以置信的 BenchmarkDotNet 库来检查效果如何(或不是)这个类的行为。

首先是结果

BenchmarkDotNet=v0.11.5, OS=Windows 10.0.17134.706 (1803/April2018Update/Redstone4)
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
Frequency=1757813 Hz, Resolution=568.8887 ns, Timer=TSC
  [Host]     : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0
  DefaultJob : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.3394.0



|                         Method |     N |      Mean |     Error |    StdDev |
|------------------------------- |------ |----------:|----------:|----------:|
|            FindDadosEmpInCache | 30000 | 231.40 ns | 0.4435 ns | 0.3703 ns |
|               FindDataAtTheEnd | 30000 | 429.90 ns | 1.1490 ns | 1.0186 ns |
|           FindDataInDictionary | 30000 |  24.09 ns | 0.2244 ns | 0.2099 ns |
| FindDataInConcurrentDictionary | 30000 |  29.66 ns | 0.0990 ns | 0.0926 ns |
|              FindDataInHashset | 30000 |  16.25 ns | 0.0077 ns | 0.0065 ns |

现在解释一下……

我最感兴趣的是看看MemoryCache 与具有数千个条目的散列列表(DictionaryHashset...)相比有多快,以及对这种“长”列表的最坏情况线性搜索。所以我添加了一些额外的测试并意识到虽然MemoryCache 不如简单或并发列表快,但速度仍然在纳秒级。从 30,000 长的缓存项目列表中检索项目甚至不需要一毫秒。

公平地说,MemoryCache 比那些简单的列表做得更多,因为它必须控制并发、项目过期/驱逐等。我相信它对于各种工作负载来说已经足够快了,但如果你不需要它添加的功能,例如驱逐策略,您应该更好地坚持使用更简单的哈希列表实现。

另一方面,由于它比哈希查找“慢”一个数量级,因此可能还有改进的余地。我猜设计师认为它已经足够好了,我有什么资格不同意 DOTNET 工程师的意见? :-)

这是基准程序的源代码:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;

namespace TestListPerformance

    class Program
    
        static void Main(string[] args) 
            var summary = BenchmarkRunner.Run<BenchmarkMemoryCache>();
        
    

    public class BenchmarkMemoryCache
    
        [Params(30000)]
        public int N  get; set; 
        public string FindStr;
        private IList<DadosEmp> data;
        private Dictionary<string, DadosEmp> dict;
        private ConcurrentDictionary<string, DadosEmp> concurrentDict;
        private HashSet<DadosEmp> hashset;
        private DadosEmp last;

        [GlobalSetup]
        public void BuildData() 
            FindStr = N.ToString();
            data = new List<DadosEmp>(N);
            dict = new Dictionary<string, DadosEmp>(N);
            concurrentDict = new ConcurrentDictionary<string, DadosEmp>();
            hashset = new HashSet<DadosEmp>();
            for (int i = 0; i <= N; i++) 
                DadosEmp d;
                data.Add(d = new DadosEmp 
                    Identificacao = i,
                    Pis = i * 100,
                    NumCartao = i * 1000,
                    Nome = "Nome " + i.ToString(),
                );
                MemoryCache.Default.Add(i.ToString(), d, 
                    new CacheItemPolicy  SlidingExpiration = TimeSpan.FromMinutes(30) );
                dict.Add(i.ToString(), d);
                concurrentDict.TryAdd(i.ToString(), d);
                hashset.Add(d);
                last = d;
            
        
        [Benchmark]
        public DadosEmp FindDadosEmpInCache() 
            var f = (DadosEmp)MemoryCache.Default.Get(FindStr);
            return f;
        
        [Benchmark]
        public DadosEmp FindDataAtTheEnd() 
            var f = data.FirstOrDefault(e => e.NumCartao == N || e.Pis == N || e.Identificacao == N);
            return f;
        
        [Benchmark]
        public DadosEmp FindDataInDictionary() 
            var f = dict[FindStr];
            return f;
        
        [Benchmark]
        public DadosEmp FindDataInConcurrentDictionary() 
            var f = concurrentDict[FindStr];
            return f;
        
        [Benchmark]
        public bool FindDataInHashset() 
            return hashset.Contains(last);
        

    

    public class DadosEmp : IEquatable<DadosEmp> 
    
        public const string BIO_EXCLUSAO = "xbio";

        public DadosEmp() 
            Biometrias = new List<string>();
        
        public long Identificacao  get; set; 
        public long Pis  get; set; 
        public long NumCartao  get; set; 
        public string Nome  get; set; 
        public int VersaoBio  get; set; 
        public string Unidade  get; set; 
        public IList<string> Biometrias  get; set; 
        public string Biometria  get; set;  
        public bool ExcluirBiometria  get  return Biometria == BIO_EXCLUSAO;  
        public DateTime DataEnvioRep  get; set;  
        public string SenhaTeclado  get; set; 
        public bool ExigeAutorizacaoSaida  get; set; 
        public bool BioRepPendente  get; set; 
        public override bool Equals(object obj) 
            DadosEmp e = obj as DadosEmp;
            if (ReferenceEquals(e, null))
                return false;
            return Equals(e);
        
        public bool Equals(DadosEmp e) 
            if (ReferenceEquals(e, null))
                return false;
            return e.Pis == this.Pis;
        
        public override int GetHashCode() 
            return Pis.GetHashCode();
        
        public override string ToString() 
            return string.Format("0 (1 - 2)", Nome, Pis, Identificacao);
        
    

【讨论】:

【参考方案2】:

看起来不错。虽然低于一秒的时间不是很可靠;您可能遇到了垃圾收集,您的 PC 可能会在短时间内执行其他操作,第一次 JIT 编译等。

所以增加计数。这也应该使每个线程的结果最终更接近。

我上周进行的一些测试使其达到每秒 800 万次迭代(不是很多,但仍然是)单线程。所以是的,这些天 PC 速度很快 ;-)

【讨论】:

【参考方案3】:

问题是不能在多核机器上使用的 StopWatch 类! (我假设您有一个多核 CPU)与当线程从一个内核移动到另一个内核时 Bios 如何处理该计数器有关(即使是单线程应用程序也会跳转内核!)。

编辑: 查看 - http://msdn.microsoft.com/en-us/library/windows/desktop/ms644904(v=vs.85).aspx - 特别是备注部分。 还有一个 *** 帖子 - Multicore and thread aware .Net stopwatch?。 结束编辑

我已经从高到低搜索了衡量应用程序性能的最佳方法,我想出的最可靠的是 DateTime.UtcNow。获取开始时间和结束时间,然后取它们之间的差异。您必须对代码进行足够的循环以克服低精度,但我遇到的其他方法都没有提供更可靠的准确性。

【讨论】:

@exacerbatedexpert - 在称事情为 FUD 之前,你为什么不研究一下这件事。看看 - msdn.microsoft.com/en-us/library/windows/desktop/… - 和备注部分。特别指出,由于 BIOS 错误,您可以获得不同的结果,这反过来意味着在多核机器上使用 StopWatch 毫无意义(不设置线程亲和性)。 @exacerbatedexpert -(我确实阅读了您的知识库文章)。 MSDN 文章备注部分没有指定每个 BIOS 都有该错误,但也没有指定只有少数 BIOS-s 有错误。所以现在这取决于你如何解释所写的内容。我认为备注部分中的文字暗示这些错误广泛存在,这就是为什么你不应该依赖秒表在多核 CPU 上给出准确的结果(不设置线程亲和性)。网上有很多关于秒表问题的帖子,这些帖子也都应该支持我的说法...... 我不知道在这种情况下 StopWatch 类是否有问题(处理器规格看起来像 Xeon 但不知道它是否是多核的)。但是,我在自己的代码中肯定遇到了 StopWatch 类的问题,所以我建议不要使用它。问题包括偶尔会出现负时间间隔。然而,更常见的是,连续的值显然是不正确的,后面的值小于前面的值。 ...你不喜欢人们只是删除他们被证明是错误的 cmets【参考方案4】:

在我的机器上,每次调用 GetItem 大约需要 40 毫秒或 400 纳秒。

我在调试器下跟踪调用,在我的 I7 机器上每个 GetItem 大约有 2000 条指令。这超出了我的预期。

【讨论】:

以上是关于Memory Cache .Net 4.0 性能测试:惊人的结果的主要内容,如果未能解决你的问题,请参考以下文章

container_memory_cache高怎么排查

#computer architecture#memory1

200 from memory cache / from disk cache / 304 Not Modified 区别

SmartSql = Dapper + MyBatis + Cache(Memory | Redis) + ZooKeeper + R/W Splitting + ......

计算机的Cache和Memory访问时Write-back,Write-through及write allocate的区别。

性能压测之 Memory与IO 性能数据采集