代码改变世界

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

2009-11-11 14:28  Jeffrey Zhao  阅读(...)  评论(... 编辑 收藏

由于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