10.3.3 性能比较
前面我们对实现原子整型操作的种种机制已经讨论了很多,现在让我们来看一些事实。不过在这之前,我首先强调一下,本节的表格只反映单/多处理器Intel架构上的Win32操作系统的性能,其他架构和操作系统可能具有不同的特征。
我曾考察过7种不同的策略,每一种都使用了一个公共的全局变量,该变量被线程递增或递减。第一种策略"未加守卫的(Unguarded)"不加锁,只是简单地通过++/--操作符来递增/递减。接下来的两种策略(Synesis的Atomic_*库函数和WinSTL的内联函数)则使用"架构-派发"技术。第4种策略调用了Win32的Interlocked_*系统函数。最后3种策略则分别使用了Win32的CRITICAL_SECTION、WinSTL的spin_mutex和Win32的MUTEX内核对象作为同步对象来守护临界区,其中临界区由对变量的++或--操作构成。测试的结果显示在表10.2中,所显示的是31个竞争线程1000万次操作的总时间。因为这些策略在不同的机器上进行了测试,因此我们获得的是一个相对性能比较表。
表10.2
|
同 步 策 略< xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" /> |
单 处 理 器 |
对称多处理器 |
|
绝对时间(ms) |
与未加守卫
的情形的百分比 |
绝对时间(ms) |
与未加守卫
的情形的百分比 |
|
未加守卫的++/-- |
362 |
100% |
525(不正确) |
100% |
|
Synesis Atomic_* API |
509 |
141% |
2464 |
469% |
|
WinSTL atomic_* 内联函数 |
510 |
141% |
2464 |
469% |
|
Win32 Interlocked* API |
2324 |
642% |
2491 |
474% |
|
Win32 CRITICAL_SECTION |
5568 |
1538% |
188235 |
35854% |
|
WinSTL spin_mutex |
5837 |
1612% |
3871 |
737% |
|
Win32 MUTEX |
57977 |
16016% |
192870 |
36737% |
我故意给测试程序所生成的线程数目定了一个奇数,这样当所有线程都执行完毕,被操纵的变量就会具有一个跟迭代次数相同的较大的非零值。由此我们就可以迅速确定操作是否确实被原子地执行了。该表显示的所有情况,包括在单处理器上的"未加守卫的(Unguarded)"操纵,行为都是正确的。但是,在"未加守卫的/对称多处理器(Unguarded/SMP)"的情形,每次运行后变量的值都有相当大的差异,这就表示线程之间互相中断了其他线程的RMW指令周期,也恰恰验证了我们对于多处理器机器上的某些单指令操作的非原子性的认识。
从性能方面来说,有几点可以很明显地看出来。首先,不管采用哪种机制,它们在多处理器上的开销相对于单处理器上来说总是较高的,这就意味着多处理器缓存(cache)不喜欢被中断。
其次,通过使用按架构派发技术,Synesis的Atomic_API和WinSTL的atomic_*在单处理器系统上的表现都非常好,其开销只有Win32 Interlocked_*系统库函数的22%,或"未加守卫的(Unguarded)"++/--操作的141%。而且,在多处理器上,除LOCK之外的处理器测试所引入的额外开销非常小,仅为1%。我想说的是,如果你写的程序既需要在单处理器架构上工作,又需要在多处理器架构上工作,并且你只想交付单一版本的程序,使用这种按架构派发的技术很可能会为你带来可观的收益。
这张表的结果印证了一个众所周知的事实:作为一种内核对象,将互斥体用于实现原子操作会导致很高的开销,在Win32系统上这么使用显得头脑不够冷静。
跟互斥体稍有区别的是CRITICAL_SECTION。我不知道你怎么认为,但是就我在Win32平台上学习多线程编程(www.cppentry.com)时所获知的大部分劝诫,都是提倡将CRITICAL_SECTION作为替代互斥体的一种更好的方案。的确,在单处理器系统上,它比互斥体快上10倍,然而在多处理器机器上,它跟互斥体的性能就不相上下了。再一次,你需要在多处理器系统上测试你的代码,从而验证你对所采用的机制的效率的假定。我得指出,CRITICAL_SECTION并非实现原子操作的有效机制,和互斥体的情况不同,我曾在客户代码中看到许多对CRITICAL_SECTION的使用。
你可能想知道为什么会有人使用自旋互斥体来实现原子操作。好吧,我来告诉你。Linux的<asm/atomic.h>中提供的原子操作只能保证24位以内的操作。此外,在Linux的一些变种上,具有正确语义(递增并返回原始值)的函数可能并不存在。通过使用概念上比较简单的写/置函数(write/set functions),支持读写更宽数据的原子操作仍然是可以实现的,同时并不会引入过高的性能开销。
我希望上表中的结果能够让你停下来思考一下你自己的实现。记住,这是在特定于Win32平台的测试结果,在其他平台上性能可能有显著的不同。但是其主要作用是学习性能评测,并质疑/验证你的假设。