10.3 原子整型的性能
在步入多线程扩展(10.4节)和线程相关的存储(10.5节)这两个主题之前,我们先来考察一下各种原子整型操作策略的性能的各个方面。
10.3.1 基于互斥体的原子整型
如果你的操作系统未提供原子整型操作,你可能需要求助于互斥体来对"原子整型API的访问"进行加锁,如程序清单10.3所示:
程序清单10.3
- namespace
- {
- Mutex s_mx;
- }
- int atomic_postincrement(int volatile *p)
- {
- lock_scope<Mutex> lock(s_mx);
- return *p++;
- }
- int atomic_predecrement(int volatile *p)
- {
- lock_scope<Mutex> lock(s_mx);
- return --*p;
- }
这里的问题在于性能。你不但要付出因获取和释放互斥体对象而陷入内核态所导致的也许相当可观的开销,你还要面对若干线程试图同时执行各自原子操作而导致的竞争条件。你的进程中的每个单独的原子操作都引入一个单独的互斥体对象,这自然会造成性能上的瓶颈。
我曾亲眼目睹有人试图通过为每个原子函数提供一个单独的互斥体来减小开销。然而遗憾的是,这么做只是成功地缩短了线程的等待时间。另外,我估计你也猜到了,这在单处理器的Intel机器上通过了彻底的测试。但是,一旦这个应用程序到了多处理器的机子上,这种做法可就洋相百出了。1由于每一个互斥体保护的是函数而非数据,所以当有些线程递增某个变量的同时另一个线程在递减该变量是可能的。这种做法所能确保的只是防止两个线程同一刻对同一整型变量做同样的事。正如多线程领域的其他东西一样,你只有在多处理器的机器上测试代码后才能确信它们是正确的。
先不管这个可怜的失败,确实有办法在多个互斥体之间共享相同的竞争条件从而减轻的这个性能瓶颈。我们所需要的是基于被操纵的变量的某些属性来选择互斥体,这个属性就是变量的地址(我们不能基于变量的值来选择互斥体,因为它随时可能改变)。
我们的做法看起来像这样:
- namespace
- {
- Mutex s_mxs[NUM_MUTEXES];
- };
-
- int __stdcall Atomic_PreIncrement_By(int volatile *v)
- {
- size_t index = index_from_ptr(v, NUM_MUTEXES);
- lock_scope<Mutex> lock(s_mxs[index]);
- return ++*(v);
- }
函数index_from_ptr()提供从变量地址到一个位于[0, NUM_MUTEXES-1]区间上的整型的确定性映射。简单地对地址进行取模运算(%)在这里并不适合作为映射手段,因为大多数系统都将数据对齐到4、8、16或32字节边界上。下面这种做法可能比较适合: - inline size_t index_from_ptr(void volatile *v, size_t range)
- {
- return (((unsigned)v) >> 7) % range;
- }
通过在我的Win32机器上的测试,我发现使用7比其他值具有更好的效果,但这种做法不大可能被原封不动地套用到其他平台上,因而你必须为你的平台进行特定的优化。