内存缓存 .Net 4.0 性能测试:惊人的结果

9
这个性能测试结果有误还是系统缓存表现异常强?以下是我的测试结果:
[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();
        }
    }
}

4
在我看来还不错。是什么让你对结果产生疑虑? - Oded
4
实际问题是什么?到底什么是令人惊讶的? - Bob Horn
1
我发现了非凡的表现,这让我有些怀疑。 - lsalamon
4个回答

10

我刚在网上搜索了有关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);
        }
    }
}

6

看起来不错。虽然低于一秒的时间不是非常可靠的;你可能遇到了垃圾回收,你的电脑可能会有短暂的其他操作,第一次JIT编译等。

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

我上周做的一些测试每秒达到了八百万次迭代(虽然没有做太多事情),但仍然是单线程的。所以,是的,现在的电脑速度很快 ;-)


3

在我的电脑上,它大约为40毫秒,或每次GetItem调用约为400纳秒。

我在调试器下跟踪了这些调用,在我的i7机器上每个GetItem调用约有2000条指令。这比我预期的要多。


3
问题出现在StopWatch类上,它无法在多核机器上使用!(假设您有一个多核CPU)这似乎与BIOS如何处理计数器有关,当线程从一个内核移动到另一个内核时(即使是单线程应用程序也会跳转内核!)。
编辑:
请查看-http://msdn.microsoft.com/en-us/library/windows/desktop/ms644904(v=vs.85).aspx -特别是注释部分。还有一个stackoverflow帖子-Multicore and thread aware .Net stopwatch?. 结束编辑 我已经搜索了很多方法来测量应用程序性能,并且我找到的最可靠方法是DateTime.UtcNow.获取开始和结束时间,然后取它们之间的差异。你必须循环你的代码足够多次以避免低精度,但我找到的没有其他方法给出更可靠的准确性。

2
@exacerbatedexpert - 在称某些事情为FUD之前,为什么不先研究一下呢?看看这个链接- http://msdn.microsoft.com/en-us/library/windows/desktop/ms644904(v=vs.85).aspx - 以及备注部分。明确指出由于BIOS错误可能会得到不同的结果,这意味着在多核机器上使用StopWatch是没有意义的(除非设置线程亲和性)。 - Marko
@exacerbatedexpert -(我确实阅读了您的知识库文章)。MSDN文章注释部分没有指定每个BIOS都有这个错误,但也没有指定只有少数BIOS存在错误。所以现在取决于您如何解释所写的内容。我认为备注部分的文本暗示这些错误普遍存在,这就是为什么您不应该依赖秒表在多核CPU上(未设置线程亲和力)提供准确结果。网络上有多篇帖子讨论秒表存在问题,这些都支持我的说法... - Marko
2
我不知道StopWatch类在这种情况下是否有问题(处理器规格看起来像是Xeon,但不知道它是否是多核的)。然而,我在自己的代码中确实遇到了StopWatch类的问题,所以我建议不要使用它。问题包括偶尔出现负时间间隔。更常见的情况是,连续的值明显不正确,后面的值比前面的值小。 - Simon Elms
8
当别人删除被证明错误的评论时,你喜欢吗? - Alex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接