Hello World
Spiga

缓存方式与对象创建的性能比较

2009-11-11 14:28 by 老赵, 19598 visits

由于Lambda表达式构造URL的速度不佳,我最近对于性能上的细节问题进行了一些探索和尝试。对于很多问题,以前由于不会形成性能瓶颈,因此并没有进行太多关注。还有一些问题可以“推断”出大致的结论,也趁这个机会进行更详细的试验,希望可以得到更为确切的结论和理性的认识。这次我打算做的实验,是关于对象的缓存与创建的性能比较。在某些情况下,我们会将创建好的对象缓存起来,以便今后进行复用。但是不同的缓存方式会有不同的性能,因此……我们现在便来试试看。

值得注意的是,我们这里的“缓存”,只是为了复用而保存而已,并没有一些过期机制等复杂的要求——甚至来删除操作也没有,我们这里只关心“读”操作。

泛型字典

在很多场景下,我们会为每个类型保存一个对应的对象。如果可以得到泛型参数的话,我们可以使用泛型字典来进行保存:

public static class Cache<T>
{
    public static object Instance { get; set; }
}

而测试代码便是:

private static void InitGenericStorage()
{
    Cache<object>.Instance = null;
    TestGenericStorage(1); // warm up;
}

private static void TestGenericStorage(int iteration)
{ 
    for (int i = 0; i < iteration; i++)
    {
        var instance = Cache<object>.Instance;
    }            
}

普通字典

但是,很多时候我们无法得到泛型参数信息(如这里),因此无法使用泛型字典。此时我们只能将对象保存在一个Dictionary中,测试代码如下:

private static Dictionary<Type, object> s_normalDict;

private static void InitNormalDictionary()
{
    s_normalDict = new Dictionary<Type, object>();
    s_normalDict[typeof(object)] = new object();

    TestNormalDictionary(1); // warm up
}

private static void TestNormalDictionary(int iteration)
{
    var key = typeof(object);

    for (int i = 0; i < iteration; i++)
    {
        var instance = s_normalDict[key];
    }
}

性能测试

“缓存”的目的是为了复用那些“创建和回收代价较高”的对象,但是它一定比每次都创建对象要高效吗?为此,我们也准备一个对照组:

private static void TestCreateObject(int iteration)
{
    for (int i = 0; i < iteration; i++)
    {
        var instance = new object();
    }
}

于是进行测试,自然还是使用CodeTimer

InitGenericStorage();
InitNormalDictionary();
TestCreateObject(1);

CodeTimer.Initialize();            

int iteration = 100 * 100 * 100 * 100;

CodeTimer.Time("Generic Storage", 1, () => TestGenericStorage(iteration));
CodeTimer.Time("Normal Dictionary", 1, () => TestNormalDictionary(iteration));
CodeTimer.Time("Simply Creation", 1, () => TestCreateObject(iteration));

结果如下:

Generic Storage
        Time Elapsed:   64ms
        CPU Cycles:     151,015,248
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

Normal Dictionary
        Time Elapsed:   9,304ms
        CPU Cycles:     22,475,810,124
        Gen 0:          0
        Gen 1:          0
        Gen 2:          0

Simply Creation
        Time Elapsed:   567ms
        CPU Cycles:     1,369,039,272
        Gen 0:          1144
        Gen 1:          0
        Gen 2:          0

您从中得出什么结论了呢?

结论

我得到的结论有两点:

泛型字典的性能远高于使用普通字典进行存储:从结果中可以看出,它们之间的差距接近150倍,而这也是使用字典的最高性能了——因为里面只有1个元素,如果元素数量一多,字典的性能还会有所降低。当然,字典的查询操作时间复杂度是O(1),性能已经非常高了,只可惜泛型字典可以说由CLR亲自操刀进行优化,性能自然不可同日而语。当然,泛型字典也有缺点,例如占用的空间(应该)较多,且只能全局唯一,不如普通字典的缓存方式来的灵活。另外,除非能够在代码中得到泛型参数,否则同样无法使用泛型字典。

直接构造对象的性能不一定会比保存在字典里差:在上面的实验中,我们发现即便是直接构造object对象,也比使用字典来得高效。由于CLR中对象的构造非常迅速,因此我们不应该缓存任意对象,而只应该缓存那些创建比较耗时,资源占用较多的对象,否则这样的“优化”只会适得其反。当然,我们使用了object这个最为简单的类型进行实验,性能自然最高,如果是创建一些复杂对象便不一定了。直接构造对象的另一个缺点可能是对GC会造成一定压力。但是从实验结果上看,只出现了0代的垃圾回收。因此对于“用完立即释放”的对象,一般并不会形成性能瓶颈。

还有一点值得一提。在这个示例中,事实上泛型字典和直接创建对象都是线程安全的做法,而实际使用过程中,为了避免“写”操作带来的影响,使用字典进行缓存的时候还必须使用ReaderWriterLockSlim进行保护——这也会对性能产生很大的负面影响。关于这点,我最近会有更进一步的探索。

(完整测试代码:http://gist.github.com/231716

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

35 条回复

  1. killkill
    *.*.*.*
    链接

    killkill 2009-11-11 14:33:00

    坐沙发,再慢慢看

  2. 假正经哥哥
    *.*.*.*
    链接

    假正经哥哥 2009-11-11 14:44:00

    板凳

  3. Kai.Ma
    *.*.*.*
    链接

    Kai.Ma 2009-11-11 15:35:00

    内存 VS CPU (⊙o⊙)哦

  4. 老赵
    admin
    链接

    老赵 2009-11-11 15:38:00

    @Kai.Ma
    其实我认为这里和时间换空间关系不大了……

  5. Zhenway
    *.*.*.*
    链接

    Zhenway 2009-11-11 15:48:00

    经常把泛型字典(一直叫它泛型类型静态字段)和DynamicMethod绑在一起使用,既不用担心线程安全,又不用担心效率

  6. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 16:05:00

    其实这个东西装配脑袋叫它类型字典(Type Dictionary)。

  7. 老赵
    admin
    链接

    老赵 2009-11-11 16:08:00

    @Ivony...
    啊,我记错了……这是官方说法吗?如果是的话,我改一下。

  8. JimLiu
    *.*.*.*
    链接

    JimLiu 2009-11-11 16:20:00

    当然,字典的查询操作时间复杂度是O(1),已经非常高了

    第一眼看吓了我一条,后来才发现是“(性能)已经非常高了”

  9. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 16:21:00

    Jeffrey Zhao:
    @Ivony...
    啊,我记错了……这是官方说法吗?如果是的话,我改一下。




    官方好像没说法(脑袋似乎不是微软发言人),其实我也喜欢泛型字典的称呼的。。。

    可以问一下脑袋看看,是不是有官方说法,我看看他在不。。。。



    不是官方说法,脑袋文中有:

    “类型字典”这个词是我根据其特性杜撰的。。。。。


    其实我们可以商量下啊,这个手法其实用途也蛮广的,统一说法比较好。

  10. 老赵
    admin
    链接

    老赵 2009-11-11 16:22:00

    @Ivony...
    嘿嘿,那就不改了……

  11. 老赵
    admin
    链接

    老赵 2009-11-11 16:22:00

    JimLiu:
    当然,字典的查询操作时间复杂度是O(1),已经非常高了

    第一眼看吓了我一条,后来才发现是“(性能)已经非常高了”


    呵呵,是我写快了。

  12. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-11 16:52:00

    泛型字典显然会被人理解成Dictionary<,>……

  13. 老赵
    admin
    链接

    老赵 2009-11-11 16:57:00

    @装配脑袋
    或者叫做泛型参数字典?

  14. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-11 17:00:00

    我叫“类型字典”是因为它确实是“类型”为key的字典啊。。

  15. 老赵
    admin
    链接

    老赵 2009-11-11 17:08:00

    @装配脑袋
    但我觉得没有突出“泛型”的含义,“类型字典”有些Dictionary<Type,>的感觉……
    所以要不叫做“泛型参数”字典?就是用“泛型参数”作为Key,也必须有泛型参数才能用的字典……

  16. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 17:42:00

    Jeffrey Zhao:
    @装配脑袋
    但我觉得没有突出“泛型”的含义,“类型字典”有些Dictionary<Type,>的感觉……
    所以要不叫做“泛型参数”字典?就是用“泛型参数”作为Key,也必须有泛型参数才能用的字典……




    呃,,,,那个东西微软倒是有官方说法,叫泛型类型参数,简称类型参数。简称我也觉得别扭,所以一般不用简称。

    其实我也一直用类型字典这个说法,倒并不是因为自己支持这个说法,而是这玩意儿是脑袋第一个在自己的博客上披露且命名,表示对原作者的尊重而已。

    但正如老赵所言,类型字典这个词怎么样也不能让人联系到泛型上去。但泛型字典又正如脑袋所说,容易让人误会成是Dictionary<...>。真是左右为难啊,大家不如都来各抒己见,这个东西怎么称呼吧,别又搞出N个名字造成沟通不便。

  17. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-11 18:00:00

    Ivony...:

    Jeffrey Zhao:
    @装配脑袋
    但我觉得没有突出“泛型”的含义,“类型字典”有些Dictionary<Type,>的感觉……
    所以要不叫做“泛型参数”字典?就是用“泛型参数”作为Key,也必须有泛型参数才能用的字典……




    呃,,,,那个东西微软倒是有官方说法,叫泛型类型参数,简称类型参数。简称我也觉得别扭,所以一般不用简称。

    其实我也一直用类型字典这个说法,倒并不是因为自己支持这个说法,而是这玩意儿是脑袋第一个在自己的博客上披露且命名,表示对原作者的尊重而已。

    但正如老赵所言,类型字典这个词怎么样也不能让人联系到泛型上去。但泛型字典又正如脑袋所说,容易让人误会成是Dictionary<...>。真是左右为难啊,大家不如都来各抒己见,这个东西怎么称呼吧,别又搞出N个名字造成沟通不便。


    官方说法还不是Techincal Writer说了算 谁说得早 说得有道理就听谁的

  18. qiaojie
    *.*.*.*
    链接

    qiaojie 2009-11-11 18:47:00

    这玩意有什么用?
    直接把Instance作为具体类的静态属性不就结了么?
    搞个泛型类去放Instance,有多此一举的嫌疑。

  19. 老赵
    admin
    链接

    老赵 2009-11-11 18:54:00

    @qiaojie
    这玩意儿可是相当有用。
    静态属性没法根据统一的签名进行获取,而Cache<T>是可以的。
    试想这么一个API:

    TBinder GetBinder<TBinder>()
    {
        return BinderCache<TBinder>.Instance;
    }
    那么可以GetBinder<StringBinder>()和GetBinder<ArticleBinder>()。
    否则你的GetBinder方法拿到个TBinder后,又该怎么访问各自独立的静态属性?

  20. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 18:58:00

    qiaojie:
    这玩意有什么用?
    直接把Instance作为具体类的静态属性不就结了么?
    搞个泛型类去放Instance,有多此一举的嫌疑。




    这玩意儿等于为所有已知未知的类型全部增加了一个静态属性,不管它愿不愿意。。。

  21. 老赵
    admin
    链接

    老赵 2009-11-11 19:00:00

    @Ivony...
    而且为静态属性统一签名了,可以用一致的方式访问……

  22. qiaojie
    *.*.*.*
    链接

    qiaojie 2009-11-11 19:03:00

    Ivony...:

    qiaojie:
    这玩意有什么用?
    直接把Instance作为具体类的静态属性不就结了么?
    搞个泛型类去放Instance,有多此一举的嫌疑。




    这玩意儿等于为所有已知未知的类型全部增加了一个静态属性,不管它愿不愿意。。。



    除了可以看成是一个非侵入式的singleton实现方案,还有没有更有意义的使用场景?

  23. Avlee
    *.*.*.*
    链接

    Avlee 2009-11-11 20:08:00

    这个测试中造成性能的差别真的在字典的使用上吗?

  24. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-11 20:16:00

    qiaojie:

    Ivony...:

    qiaojie:
    这玩意有什么用?
    直接把Instance作为具体类的静态属性不就结了么?
    搞个泛型类去放Instance,有多此一举的嫌疑。




    这玩意儿等于为所有已知未知的类型全部增加了一个静态属性,不管它愿不愿意。。。



    除了可以看成是一个非侵入式的singleton实现方案,还有没有更有意义的使用场景?




    这个与singleton没有关系,属性的类型不是T,而是某个确定的类型,当然也可以是T。。。。

    至于使用场景,这是一个泛型的特性,怎么灵活运用主要还是看程序员自己。并没有什么标准的使用场景。。。其实可以用到的地方是非常多的。

  25. 老赵
    admin
    链接

    老赵 2009-11-11 20:16:00

    qiaojie:
    除了可以看成是一个非侵入式的singleton实现方案,还有没有更有意义的使用场景?


    一个Cache<T>类里也可以不止一个字段。
    这个“泛型参数字典”主要就是可以把一个或多个对象和某个泛型参数对应起来,而且线程安全(在静态构造函数里初始化),性能高。

  26. 老赵
    admin
    链接

    老赵 2009-11-11 20:17:00

    Avlee:这个测试中造成性能的差别真的在字典的使用上吗?


    是的,你可以试验一下,代码在文章最后,呵呵。

  27. 剑走偏锋[E.S.T]
    *.*.*.*
    链接

    剑走偏锋[E.S.T] 2009-11-12 10:25:00

    楼上的例子有点问题

    public static class Cache<T>
    {
        public static object Instance { get; set; }
    }
    


    您举的这个缓存泛型类,虽然有个泛型参数T,但是没用上啊?
    是不是应该改成这样?

    public static class Cache<T>
    {
        public static T Instance { get; set; }
    }
    

  28. 老赵
    admin
    链接

    老赵 2009-11-12 10:29:00

    @剑走偏锋[E.S.T]
    没问题,故意不用的,做试验而已,能说明问题就行。

  29. lsjwzh
    *.*.*.*
    链接

    lsjwzh 2009-11-12 15:52:00

    Jeffrey Zhao:

    qiaojie:
    除了可以看成是一个非侵入式的singleton实现方案,还有没有更有意义的使用场景?


    一个Cache<T>类里也可以不止一个字段。
    这个“泛型参数字典”主要就是可以把一个或多个对象和某个泛型参数对应起来,而且线程安全(在静态构造函数里初始化),性能高。




    “泛型参数字典”为什么是线程安全的呢?为什么在 静态构造函数里初始化 就是线程安全的?“泛型参数字典”的写入(赋值)是不是属于原子操作呢?
    新手,不懂,望能指点一二。谢谢!

  30. 老赵
    admin
    链接

    老赵 2009-11-12 16:03:00

    @lsjwzh
    静态构造函数是CLR帮忙的,其他赋值之类的和普通情况没什么区别。

  31. lsjwzh
    *.*.*.*
    链接

    lsjwzh 2009-11-12 16:15:00

    @Jeffrey Zhao
    那是不是对“泛型参数字典”赋值时(Cache<object>.Instance= new object())不能同时读取?还是会读到以前的值?

  32. 老赵
    admin
    链接

    老赵 2009-11-12 18:43:00

    @lsjwzh
    是,会产生竞争,但是“读到以前的值”这种说法可能不太妥当。

  33. hftgood
    124.72.17.*
    链接

    hftgood 2010-08-27 16:57:25

    Your title here...

    你写的太深奥了我是菜鸟看不懂...

  34. Visitor.Name
    74.125.158.*
    链接

    Visitor.Name 2012-04-25 17:52:42

    泛型字典已然成为了Dictionary的别称,用这个名字不太好 从功能上定义,“字典”应当是根据一个索引(或者键)查找相应值(或者值集合)的对象 而Cache则是根据一个索引生成一个值,同时用生成的值构造一个缓存,如果下次索引相同就可以缓存否则继续生成。 从功能上来说,Cache是一个动态生成的固定表,所以直接使用[泛型缓存/泛型缓存表/泛型对象缓存/泛型实例缓存]之类的名字即可,没必要纠结于字典二字

  35. 阳光
    124.74.45.*
    链接

    阳光 2012-06-01 15:55:18

    泛型字段全局唯一? 这个不明白

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我