缓存方式与对象创建的性能比较
2009-11-11 14:28 by 老赵, 19670 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)
坐沙发,再慢慢看