并发环境下的缓存容器性能优化
2014-04-22 14:19
411 查看
我们在项目中经常会遇到这样的场景:一些信息读取开销较大,但只需要生成一次便可反复使用,因此我们会将其永久地缓存起来。例如在ASP.NET MVC中,系统会根据Controller的名称来缓存对应的元数据。这些缓存容器都有一些共同的特点,便是存储的对象数量有限(少则几十,多不过数千),但都需要在并发环境下被大量地读取,因此必须是线程安全的。那么,我们该如何设计这样的容器呢?
一个非常常见的做法是使用字典(System.Collections.Generic.Dictionary<TKey, TValue>)进行存储。字典使用了“哈希表”这个数据结构,其读取和写入的时间复杂度几乎都是O(1),性能可谓非常之高。只不过,字典不是线程安全的,例如它在添加元素的时候可能会对内部的bucket数组进行动态调整,这显然不是一个原子操作。因此,它无法直接用户并行环境的读写,我们对它的操作必须加锁。
说起加锁,最简单的方式便是在读和写方法中使用同一个互斥体(mutex)进行lock:
这么做可以保证在任意时刻只有单个线程在访问字典,无论读写,这自然做到了线程安全。但是,这么做的话对于并发环境并不友好。因为一个字典是完全可以同时被多个线程“读”的,也就是说,如果我们简单使用lock便会大大降低“读”操作的可并发性,从而对性能产生负面影响。
那么我们能否对“写”操作进行lock,而让“读”操作完全自由呢?例如:
这也是不允许的。因为这样做无法避免在某个线程读取字典的同时,另一个线程正在修改字典。一旦出现读写操作同时进行的情况,字典就很可能被破坏了。因此,其实我们的要求有两点:
只能由单个线程写,但可以由多个线程同时读。
在进行读操作时,不可同时进行写操作。反之亦然。
这便是“读写锁”使用场景。只要使用“写”锁来保护修改操作,而使用“读”锁来保护读取操作,便可以让字典正确并高效地工作在并发环境下。“读写锁”在.NET框架中有着两个实现:ReaderWriterLock和ReaderWriterLockSlim。前者出现在.NET
1.x中,而后者随.NET 2.0发布。两者的区别在于前者性能低,后者性能高,因此在目前,基本上需要读写锁的场景都会建议使用ReaderWriterLockSlim(除了稍后会提到的情况)。
但是,ReaderWriterLockSlim并非不会带来开销,我们接下来的试验便要来验证这一点。首先,我们准备1000个数字,它们便是我们使用的测试数据源:
然后,我们使用CodeTimer测试它在三种情况下的性能:
最后一种方式使用了性能较差的ReaderWriterLock,我的目的除了表现出它与ReaderWriterLockSlim的性能差距之外,还有一个原因在于.NET
3.5的ReaderWriterLockSlim类会在某些时候读取Environment.ProcesserCount,这在Medium Trust的环境下会引发SecurityException。因此您会发现,在目前的ASP.NET MVC框架中,所有需要读写锁的地方都使用了ReaderWriterLock而不是Slim组件。
试验结果如下:
可见,尽管ReaderWriterLockSlim的性能比ReaderWriterLock要高出许多,但相对于无锁的读取还是有明显差距。这让我感到很不合算,因为这是由缓存容器的使用性质决定的。在一般情况下,这种容器都是“有限次地修改”,而几近“无限次地读取”,尤其到了程序运行后期,所以该使用的对象都已经缓存下来,字典内容更不会产生任何改变——但是,每一次读取还要继续承受ReaderWriterLockSlim的开销,这笔帐相信人人会算。那么,我们又如何能够做到“无锁”地读取呢?既然我们“锁”的目的是因为需要“修改”,那么我们使用“不可变(Immutable)”的容器是否就能有所帮助呢?为此,我向F#借来了Immutable
Map。
F#中的Map类定义在Microsoft.FSharp.Core.dll中,这需要安装F#
SDK for Visual Studio 2008,当然如果您直接安装了VS 2010自然也就有了相应的程序集——不过一些辅助类库(如Microsoft.FSharp.PowerPack),以及源文件(F#是个开源的语言)还是需要去之前的链接里下载一个压缩包。在项目中引用了程序集后,便可以使用Microsoft.FSharp.Collections命名空间下的FSharpMap类型了。我们也为它准备了同样的试验代码:
把它和ReaderWriterLockSlim相比,结果如下:
嗯?为什么使用Immutable Map之后,即便没有锁,性能还是不如ReaderWriterLockSlim的版本呢?阅读代码(fsharp\FSharp.Core\map.fs)之后发现,其原因在于数据结构的选取上。FSharpMap的数据结构是“AVL树”,即自平衡(self-balancing)的二叉搜索树,它的Get操作时间复杂度是O(logN),N为节点个数——这自然比不过字典的O(1)时间复杂度了。那么,我们能否得到一个Get操作时间复杂度为O(1)的Immutable
Map呢?
我在推特上提出这个问题之后不多久,许式伟大牛谈了他的看法:
我的理解中,FP中线性数据结构如数组,Hash等都会失效,所以没有O(1)的Immutable Map。
我同意这个看法。字典的高性能,在于求得key的Hash之后,可以通过数组下标“瞬间”定位到元素所在的bucket,但是在FP中无法做到这一点。不过幸好,我们用的C#并非是FP,我们有自己的“保底”方案:添加元素时将现有的元素完整复制到新的字典中,最后返回的是新的字典。例如:
这不就是一个“不可变的哈希表”吗?当然,对于真正可应用的缓存容器来说,还需要考虑更多一些东西。我在这里准备了两个缓存容器的基类,可用于此类容器的实现。其中ReadWriteCache类基于ReaderWriterLockSlim,而ReadFreeCache类则基于不可变的字典。那么,这两种做法的性能究竟如何,又分别适合什么样的场景,我们还有没有其他的做法呢?
关于这些问题,就留到下次再讨论吧。
2009-11-16 00:29 by 老赵, 16700 visits
上一篇文章里,我谈到对于某些场景中的缓存容器,其写操作非常少,到了程序后期甚至为零,而对它的读操作却几乎是密集连续且无穷无尽的。对于这样的容器,如果使用ReaderWriterLockSlim去进行保护每个“读”操作,这开销是在有些多余。因此我提出了“不可变”的哈希表,目的是在保持读操作的时间复杂度为O(1)的情况下,尽可能避免多余的开销。现在我们便将它和其他几种时间进行一个性能的对比。
需要强调一点的是,我们这里讨论的仅仅是符合我提出的特定场景的缓存容器,而不是一个“线程安全的字典”。或者说,其实我这里更强调的是“并发环境下”的“读”性能,而不涉及IDictionary<TKey, TValue>的其他操作(如Count),更不会关心如CopyTo、Remove这类功能的性能。
上一篇文章结束时,我给出了两个缓存容器的基类,可用于此类容器的实现。其中ReadWriteCache类基于ReaderWriterLockSlim,而ReadFreeCache类则基于不可变的字典。不过这两种做法不太适合进行性能测试,因此我这里的实验使用了这样的接口:
我为这个接口提供了几种最基本实现,无论是“读”还是“写”,都是最直接的,并不对任何特殊的情况(如key缺失,key重复)进行处理。例如ImmutableMapCache:
ImmutableMapCache是基于F#中的Map而实现的读写操作。由于是Immutable的集合,因此对它的读操作不需要任何并发方面的保护——而写操作理论上也是线程安全的,但是我这里还是使用了lock。这是因为如果没有lock的话,在实际并发的场景中容易出现“摇摆”的情况出现。试想,同时有2个线程正在添加元素,它们同时读取了集合的当前状态,但是在写回的时候只后一个线程的操作生效,先写回的线程的修改丢失了。当并发程度高的情况下,“摇摆”会更加严重。因此,无论是ImmutableMapCache,还是基于Immutable
Dictionary的实现,在Set操作中都使用lock进行保护。
测试代码如下:
请注意,这里的测试都是在单线程环境下的。严格来说,这并不表示每种容器在多线程环境下的表现。事实上,即便是多线程环境下,不同实现随并发程度的高低也会有所变化。因此,除了进行实验和观察结果之外,也必须结合实际情况进行思考,而不能简单的“采纳”这次实验的结果。在这里我们总共测试5种不同的实现:
它们分别是:
RwLockSlimDictionary:基于Dictionary,使用ReaderWriterLockSlim进行保护的缓存容器。
RwLockDictionary:基于Dictionary,使用ReaderWriterLock进行保护的缓存容器。
Immutable Map:基于F#中Map实现的缓存容器。
Immutable Dictionary:基于不可变的哈希表实现的缓存容器。
Concurrent Dictionary:基于.NET 4.0中提供的Concurrent Dictionary实现的缓存容器。
运行环境是.NET 4.0 Beta 2,实验进行三次。我们首先关注“写”操作的结果,如下:
取平均值作为最后结果,并绘制成图表:
在这结果里我并没有包含基于Immutable Dictionary的实现,因为它的结果实在太惨,如果一并放入的话,其他实现方式就几乎看不出来了。我们在来看一下我们的测试代码,它是统计向一个空容器内添加n个元素所花的时间(n等于100、200、...、1000)。对于基于字典的实现来说,添加1个元素的时间复杂度是O(1),因此添加n个元素的时间复杂度是O(n)。而基于Immutable Map的实现,其添加1个元素的时间复杂度是O(log(n)),于是添加n个元素的时间复杂度是O(n
* log(n))。这两种时间复杂度都可以从图表中表现出来——当然,这个表现形式是“趋势”也就是“形状”,同样的时间复杂度的常数还是不一样的。
那么Immutable Dictionary又是什么样的呢?我们为其单独进行一番实验,减小实验粒度,希望可以得到更清晰的结果:
将其结果绘制成图表:
向Immutable Dictionary中添加1个元素的时间复杂度是O(n),于是添加n个元素的时间复杂度则是O(n2),从图表上看,这趋势也是相当明显的。而且,与基于Dictionary的实现方式不同,由于Immutable Dictionary每次都要重新复制元素,它对于GC的压力也是非常可观的,如下:
从图中可以看到一个有趣的结果,那就是GC的频率在n为8000到9000的某一个时刻的收集频率突然开始加快了,莫非这就是传说中GC的自我调节能力吗?不过这并非是本次实验的关键,我们只需要发现说,Immutable Dictionary与Dictionary相比,前者对GC的消耗较大,而后者则几乎没有GC方面的压力。
在写方面,Immutable Dictionary的表现可谓残不忍睹,但如果是“读”的话,一切就都不一样了:
将结果绘制成图表:
与“写”操作的测试方式不同,“读”操作测试的是“从包含n个元素的容器中读取元素”所需要的时间。除了Immutable Map是O(logN)的时间复杂度外,其余4种容器都是基于Get操作时间复杂度为O(1)的哈希表。换句话说,基于Immutable Map的容器会随着元素数量而耗时增加,而其他4种容器,它们“读”操作的耗时和其中有多少元素并没有关系。
从结果上我们可以得出一些有趣的结论。首先,ReaderWriterLockSlim似乎会进行“自我调节”,一开始它的Write Lock开销较大,但是随着实验的进行,它的开销变小了很多。其次,基于Immutable Dictionary的实现自然因为其“Read Free”而表现最好,但是.NET中Concurrent Dictionary的表现也相当出色,可谓遥遥领先于基于ReaderWriterLockSlim的实现。而在“写”操作测试时,它的表现也可圈可点,仅次于RwLockSlimDictionary。我并不清楚Concurrent
Dictionary的实现方式,有人说是Lock Free,也有人说是小粒度的锁。这点可以通过阅读代码来得知,在这里就不多作展开了。
http://blog.zhaojie.me/2009/11/concurrent-cache-performance-improvement-2-benchmark.html
一个非常常见的做法是使用字典(System.Collections.Generic.Dictionary<TKey, TValue>)进行存储。字典使用了“哈希表”这个数据结构,其读取和写入的时间复杂度几乎都是O(1),性能可谓非常之高。只不过,字典不是线程安全的,例如它在添加元素的时候可能会对内部的bucket数组进行动态调整,这显然不是一个原子操作。因此,它无法直接用户并行环境的读写,我们对它的操作必须加锁。
说起加锁,最简单的方式便是在读和写方法中使用同一个互斥体(mutex)进行lock:
object mutex = new object(); Dictionary<int, int> dict = new Dictionary<int, int>(); // read with lock lock (mutex) { var value = dict[1]; } // write with lock lock(mutex) { dict[1] = 1; }
这么做可以保证在任意时刻只有单个线程在访问字典,无论读写,这自然做到了线程安全。但是,这么做的话对于并发环境并不友好。因为一个字典是完全可以同时被多个线程“读”的,也就是说,如果我们简单使用lock便会大大降低“读”操作的可并发性,从而对性能产生负面影响。
那么我们能否对“写”操作进行lock,而让“读”操作完全自由呢?例如:
object mutex = new object(); Dictionary<int, int> dict = new Dictionary<int, int>(); // read directly var value = dict[1]; // write with lock lock(mutex) { dict[1] = 1; }
这也是不允许的。因为这样做无法避免在某个线程读取字典的同时,另一个线程正在修改字典。一旦出现读写操作同时进行的情况,字典就很可能被破坏了。因此,其实我们的要求有两点:
只能由单个线程写,但可以由多个线程同时读。
在进行读操作时,不可同时进行写操作。反之亦然。
这便是“读写锁”使用场景。只要使用“写”锁来保护修改操作,而使用“读”锁来保护读取操作,便可以让字典正确并高效地工作在并发环境下。“读写锁”在.NET框架中有着两个实现:ReaderWriterLock和ReaderWriterLockSlim。前者出现在.NET
1.x中,而后者随.NET 2.0发布。两者的区别在于前者性能低,后者性能高,因此在目前,基本上需要读写锁的场景都会建议使用ReaderWriterLockSlim(除了稍后会提到的情况)。
但是,ReaderWriterLockSlim并非不会带来开销,我们接下来的试验便要来验证这一点。首先,我们准备1000个数字,它们便是我们使用的测试数据源:
var source = Enumerable.Range(1, 1000).ToArray(); var dict = source.ToDictionary(i => i);
然后,我们使用CodeTimer测试它在三种情况下的性能:
int iteration = 10000; CodeTimer.Initialize(); CodeTimer.Time("Read Free", iteration, () => { foreach (var i in source) { var ignore = dict[i]; } }); var rwLockSlim = new ReaderWriterLockSlim(); CodeTimer.Time("ReaderWriterLockSlim", iteration, () => { foreach (var i in source) { rwLockSlim.EnterReadLock(); var ignore = dict[i]; rwLockSlim.ExitReadLock(); } }); var rwLock = new ReaderWriterLock(); CodeTimer.Time("ReaderWriterLock", iteration, () => { foreach (var i in source) { rwLock.AcquireReaderLock(0); var ignore = dict[i]; rwLock.ReleaseReaderLock(); } });
最后一种方式使用了性能较差的ReaderWriterLock,我的目的除了表现出它与ReaderWriterLockSlim的性能差距之外,还有一个原因在于.NET
3.5的ReaderWriterLockSlim类会在某些时候读取Environment.ProcesserCount,这在Medium Trust的环境下会引发SecurityException。因此您会发现,在目前的ASP.NET MVC框架中,所有需要读写锁的地方都使用了ReaderWriterLock而不是Slim组件。
试验结果如下:
Read Free Time Elapsed: 228ms CPU Cycles: 538,190,052 Gen 0: 0 Gen 1: 0 Gen 2: 0 ReaderWriterLockSlim Time Elapsed: 1,161ms CPU Cycles: 2,783,025,072 Gen 0: 0 Gen 1: 0 Gen 2: 0 ReaderWriterLock Time Elapsed: 2,792ms CPU Cycles: 6,682,368,396 Gen 0: 0 Gen 1: 0 Gen 2: 0
可见,尽管ReaderWriterLockSlim的性能比ReaderWriterLock要高出许多,但相对于无锁的读取还是有明显差距。这让我感到很不合算,因为这是由缓存容器的使用性质决定的。在一般情况下,这种容器都是“有限次地修改”,而几近“无限次地读取”,尤其到了程序运行后期,所以该使用的对象都已经缓存下来,字典内容更不会产生任何改变——但是,每一次读取还要继续承受ReaderWriterLockSlim的开销,这笔帐相信人人会算。那么,我们又如何能够做到“无锁”地读取呢?既然我们“锁”的目的是因为需要“修改”,那么我们使用“不可变(Immutable)”的容器是否就能有所帮助呢?为此,我向F#借来了Immutable
Map。
F#中的Map类定义在Microsoft.FSharp.Core.dll中,这需要安装F#
SDK for Visual Studio 2008,当然如果您直接安装了VS 2010自然也就有了相应的程序集——不过一些辅助类库(如Microsoft.FSharp.PowerPack),以及源文件(F#是个开源的语言)还是需要去之前的链接里下载一个压缩包。在项目中引用了程序集后,便可以使用Microsoft.FSharp.Collections命名空间下的FSharpMap类型了。我们也为它准备了同样的试验代码:
var map = source.Aggregate(FSharpMap<int, int>.Empty, (m, i) => m.Add(i, i)); CodeTimer.Time("Immutable Map", iteration, () => { foreach (var i in source) { var ignore = map[i]; } });
把它和ReaderWriterLockSlim相比,结果如下:
ReaderWriterLockSlim Time Elapsed: 1,232ms CPU Cycles: 2,963,830,380 Gen 0: 0 Gen 1: 0 Gen 2: 0 Immutable Map Time Elapsed: 2,055ms CPU Cycles: 4,956,894,996 Gen 0: 0 Gen 1: 0 Gen 2: 0
嗯?为什么使用Immutable Map之后,即便没有锁,性能还是不如ReaderWriterLockSlim的版本呢?阅读代码(fsharp\FSharp.Core\map.fs)之后发现,其原因在于数据结构的选取上。FSharpMap的数据结构是“AVL树”,即自平衡(self-balancing)的二叉搜索树,它的Get操作时间复杂度是O(logN),N为节点个数——这自然比不过字典的O(1)时间复杂度了。那么,我们能否得到一个Get操作时间复杂度为O(1)的Immutable
Map呢?
我在推特上提出这个问题之后不多久,许式伟大牛谈了他的看法:
我的理解中,FP中线性数据结构如数组,Hash等都会失效,所以没有O(1)的Immutable Map。
我同意这个看法。字典的高性能,在于求得key的Hash之后,可以通过数组下标“瞬间”定位到元素所在的bucket,但是在FP中无法做到这一点。不过幸好,我们用的C#并非是FP,我们有自己的“保底”方案:添加元素时将现有的元素完整复制到新的字典中,最后返回的是新的字典。例如:
public static class DictionaryExtensions { public static Dictionary<TKey, TValue> AddImmutably<TKey, TValue>( this Dictionary<TKey, TValue> source, TKey key, TValue value) { var result = source.ToDictionary(p => p.Key, p => p.Value); result.Add(key, value); return result; } }
这不就是一个“不可变的哈希表”吗?当然,对于真正可应用的缓存容器来说,还需要考虑更多一些东西。我在这里准备了两个缓存容器的基类,可用于此类容器的实现。其中ReadWriteCache类基于ReaderWriterLockSlim,而ReadFreeCache类则基于不可变的字典。那么,这两种做法的性能究竟如何,又分别适合什么样的场景,我们还有没有其他的做法呢?
关于这些问题,就留到下次再讨论吧。
并发环境下的缓存容器性能优化(下):性能测试
2009-11-16 00:29 by 老赵, 16700 visits上一篇文章里,我谈到对于某些场景中的缓存容器,其写操作非常少,到了程序后期甚至为零,而对它的读操作却几乎是密集连续且无穷无尽的。对于这样的容器,如果使用ReaderWriterLockSlim去进行保护每个“读”操作,这开销是在有些多余。因此我提出了“不可变”的哈希表,目的是在保持读操作的时间复杂度为O(1)的情况下,尽可能避免多余的开销。现在我们便将它和其他几种时间进行一个性能的对比。
需要强调一点的是,我们这里讨论的仅仅是符合我提出的特定场景的缓存容器,而不是一个“线程安全的字典”。或者说,其实我这里更强调的是“并发环境下”的“读”性能,而不涉及IDictionary<TKey, TValue>的其他操作(如Count),更不会关心如CopyTo、Remove这类功能的性能。
上一篇文章结束时,我给出了两个缓存容器的基类,可用于此类容器的实现。其中ReadWriteCache类基于ReaderWriterLockSlim,而ReadFreeCache类则基于不可变的字典。不过这两种做法不太适合进行性能测试,因此我这里的实验使用了这样的接口:
public interface IConcurrentCache<TKey, TValue> { TValue Get(TKey key); void Set(TKey key, TValue value); }
我为这个接口提供了几种最基本实现,无论是“读”还是“写”,都是最直接的,并不对任何特殊的情况(如key缺失,key重复)进行处理。例如ImmutableMapCache:
public class ImmutableMapCache<TKey, TValue> : IConcurrentCache<TKey, TValue> { private object m_writeLock = new object(); private FSharpMap<TKey, TValue> m_map = FSharpMap<TKey, TValue>.Empty; public TValue Get(TKey key) { return this.m_map[key]; } public void Set(TKey key, TValue value) { lock (this.m_writeLock) { this.m_map = this.m_map.Add(key, value); } } }
ImmutableMapCache是基于F#中的Map而实现的读写操作。由于是Immutable的集合,因此对它的读操作不需要任何并发方面的保护——而写操作理论上也是线程安全的,但是我这里还是使用了lock。这是因为如果没有lock的话,在实际并发的场景中容易出现“摇摆”的情况出现。试想,同时有2个线程正在添加元素,它们同时读取了集合的当前状态,但是在写回的时候只后一个线程的操作生效,先写回的线程的修改丢失了。当并发程度高的情况下,“摇摆”会更加严重。因此,无论是ImmutableMapCache,还是基于Immutable
Dictionary的实现,在Set操作中都使用lock进行保护。
测试代码如下:
static void CacheBenchmark<TCache>() where TCache : IConcurrentCache<int, int>, new() { var typeName = typeof(TCache).Name; var index = typeName.IndexOf('`'); var cacheName = typeName.Substring(0, index); // warm up TCache cache = new TCache(); cache.Set(1, 1); cache.Get(1); for (int n = 100; n <= 1000; n += 100) { cache = new TCache(); CodeTimer.Time(cacheName + " (Set " + n + " elements)", 100, () => { for (var i = 0; i < n; i++) { cache.Set(i, i); } }); CodeTimer.Time(cacheName + " (Get from " + n + " elements)", 1, () => { var key = 0; for (int i = 0; i < 1000 * 1000 * 5; i++) { cache.Get(key); key = (key + 1) % n; } }); } }
请注意,这里的测试都是在单线程环境下的。严格来说,这并不表示每种容器在多线程环境下的表现。事实上,即便是多线程环境下,不同实现随并发程度的高低也会有所变化。因此,除了进行实验和观察结果之外,也必须结合实际情况进行思考,而不能简单的“采纳”这次实验的结果。在这里我们总共测试5种不同的实现:
CacheBenchmark<RwLockSlimDictionaryCache<int, int>>(); CacheBenchmark<RwLockDictionaryCache<int, int>>(); CacheBenchmark<ImmutableMapCache<int, int>>(); CacheBenchmark<ImmutableDictionaryCache<int, int>>(); CacheBenchmark<ConcurrentDictionaryCache<int, int>>();
它们分别是:
RwLockSlimDictionary:基于Dictionary,使用ReaderWriterLockSlim进行保护的缓存容器。
RwLockDictionary:基于Dictionary,使用ReaderWriterLock进行保护的缓存容器。
Immutable Map:基于F#中Map实现的缓存容器。
Immutable Dictionary:基于不可变的哈希表实现的缓存容器。
Concurrent Dictionary:基于.NET 4.0中提供的Concurrent Dictionary实现的缓存容器。
运行环境是.NET 4.0 Beta 2,实验进行三次。我们首先关注“写”操作的结果,如下:
取平均值作为最后结果,并绘制成图表:
在这结果里我并没有包含基于Immutable Dictionary的实现,因为它的结果实在太惨,如果一并放入的话,其他实现方式就几乎看不出来了。我们在来看一下我们的测试代码,它是统计向一个空容器内添加n个元素所花的时间(n等于100、200、...、1000)。对于基于字典的实现来说,添加1个元素的时间复杂度是O(1),因此添加n个元素的时间复杂度是O(n)。而基于Immutable Map的实现,其添加1个元素的时间复杂度是O(log(n)),于是添加n个元素的时间复杂度是O(n
* log(n))。这两种时间复杂度都可以从图表中表现出来——当然,这个表现形式是“趋势”也就是“形状”,同样的时间复杂度的常数还是不一样的。
那么Immutable Dictionary又是什么样的呢?我们为其单独进行一番实验,减小实验粒度,希望可以得到更清晰的结果:
for (int n = 100; n <= 10000; n += 100) { CodeTimer.Time(String.Format("add {0} elements", n), 1, () => { var cache = new ImmutableDictionaryCache<int, int>(); for (int i = 0; i < n; i++) { cache.Set(i, i); } }); }
将其结果绘制成图表:
向Immutable Dictionary中添加1个元素的时间复杂度是O(n),于是添加n个元素的时间复杂度则是O(n2),从图表上看,这趋势也是相当明显的。而且,与基于Dictionary的实现方式不同,由于Immutable Dictionary每次都要重新复制元素,它对于GC的压力也是非常可观的,如下:
从图中可以看到一个有趣的结果,那就是GC的频率在n为8000到9000的某一个时刻的收集频率突然开始加快了,莫非这就是传说中GC的自我调节能力吗?不过这并非是本次实验的关键,我们只需要发现说,Immutable Dictionary与Dictionary相比,前者对GC的消耗较大,而后者则几乎没有GC方面的压力。
在写方面,Immutable Dictionary的表现可谓残不忍睹,但如果是“读”的话,一切就都不一样了:
将结果绘制成图表:
与“写”操作的测试方式不同,“读”操作测试的是“从包含n个元素的容器中读取元素”所需要的时间。除了Immutable Map是O(logN)的时间复杂度外,其余4种容器都是基于Get操作时间复杂度为O(1)的哈希表。换句话说,基于Immutable Map的容器会随着元素数量而耗时增加,而其他4种容器,它们“读”操作的耗时和其中有多少元素并没有关系。
从结果上我们可以得出一些有趣的结论。首先,ReaderWriterLockSlim似乎会进行“自我调节”,一开始它的Write Lock开销较大,但是随着实验的进行,它的开销变小了很多。其次,基于Immutable Dictionary的实现自然因为其“Read Free”而表现最好,但是.NET中Concurrent Dictionary的表现也相当出色,可谓遥遥领先于基于ReaderWriterLockSlim的实现。而在“写”操作测试时,它的表现也可圈可点,仅次于RwLockSlimDictionary。我并不清楚Concurrent
Dictionary的实现方式,有人说是Lock Free,也有人说是小粒度的锁。这点可以通过阅读代码来得知,在这里就不多作展开了。
http://blog.zhaojie.me/2009/11/concurrent-cache-performance-improvement-2-benchmark.html
相关文章推荐
- 并发环境下的缓存容器性能优化(下):性能测试
- 并发环境下的缓存容器性能优化(下):性能测试
- 并发环境下的缓存容器性能优化(上):不可变的哈希表
- 并发环境下的缓存容器性能优化(上):不可变的哈希表
- Tomcat性能优化,如何优化tomcat配置(从内存、并发、缓存4个方面)优化
- 简述性能优化tomcat配置(从内存、并发、缓存方面)优化及压力测试
- 高并发环境下的连接器性能优化
- 大型高并发网站之查询性能优化(综合篇)
- PHP 性能 优化 缓存
- tomcat的并发与性能优化思路
- 弱网络环境下,网络性能优化
- log4j日志输出性能优化-缓存、异步
- 优化tomcat配置(从内存、并发、缓存4个方面)
- 使用优化器性能视图获取SQL语句执行环境
- iOS开发UI篇—UITableviewcell的性能优化和缓存机制
- J2EE运行环境性能大优化艺术之一
- [Android]ListView性能优化之视图缓存
- Hibernate 性能优化之二级缓存
- J2EE运行环境性能大优化艺术之一
- iOS开发UI基础—20UITableviewcell的性能优化和缓存机制