目录
文章系列
介绍
基准测试:Monitor (C# Lock)——重度争用
基准测试:Monitor (C# Lock)——坏邻居
基准:LockUC——重度竞争
基准:LockUC—坏邻居
基准测试:SpinLockUC/AsyncSpinLockUC——重度争用
基准测试:SpinLockUC/AsyncSpinLockUC——坏邻居
基准测试:TicketSpinLockUC/AsyncTicketSpinLockUC——重度争用
基准测试:TicketSpinLockUC/AsyncTicketSpinLockUC——坏邻居
基准测试:AsyncLockUC——重度争用
基准测试:AsyncLockUC——坏邻居
跨基准测试:Monitor (C# Lock) & SpinLockUC & TicketSpinLockUC——重度争用
跨基准测试:Monitor (C# lock) & SpinLockUC & TicketSpinLockUC——坏邻居
跨基准测试:Monitor (C# lock)和LockUC——重度争用
跨基准测试:Monitor (C# lock)&LockUC——坏邻居
跨基准测试:Monitor (C# lock) &LockUC & AsyncLockUC——重度争用
跨基准测试:Monitor (C# lock)&LockUC&AsyncLockUC——坏邻居
跨基准测试:Monitor (C# lock) & LockUC & AsyncLockUC & SpinLockUC & TicketSpinLockUC——重度争用
跨基准测试:Monitor (C# lock) & LockUC & AsyncLockUC & SpinLockUC & TicketSpinLockUC——坏邻居
概括
- 统一并发 I - 简介
- 统一并发 II - 基准测试方法
- 统一并发 III - 跨基准测试
- 统一并发 IV - 跨平台(.NET Core 2.1 / .NET Standard 2.0)
前两篇文章为在统一并发下对所有已实现的同步原语进行基准测试和跨基准测试做好了准备,以了解基于场景和环境的彼此间同步原语的优点、缺点、成本和行为。
这里研究的第一组基准测试将分别涵盖每个同步原语,分别适用于两种场景,重争用和坏邻居:
- Monitor (C# lock) ——来自上一篇文章
- LockUC
- SpinLockUC
- TicketSpinLockUC
- AsyncLockUC
AsyncSpinLockUC和AsyncTicketSpinLockUC不会被覆盖,因为他们的行为是完全一样的,他们通用线程同行,SpinLockUC和TicketSpinLockUC。AsyncSpinLockUC和AsyncTicketSpinLockUC总是返回作为struct 实现的完成等待器,而不是一个类,因此避免了内存分配,也避免了等待,然后行为显然与non-async对应物相同,由测量数据支持。
第二组将涵盖两个场景、重争用和坏邻居的交叉基准测试:
- Monitor(C# lock),SpinLockUC和TicketSpinLockUC
- Monitor(C# lock), LockUC
- Monitor(C# lock),LockUC和AsyncLockUC
- Monitor(C# lock),LockUC和AsyncLockUC, SpinLockUC,TicketSpinLockUC
统一并发框架是在GitHub和NuGet上可用的开源GreenSuperGreen库中实现的(3个包,LIB,单元测试基准)。
- http://github.com/ipavlu/GreenSuperGreen
- http://www.nuget.org/packages/GreenSuperGreen/
- http://www.nuget.org/packages/GreenSuperGreen.Test/
- http://www.nuget.org/packages/GreenSuperGreen.Benchmarking/
Monitor的吞吐量周期(C# lock)表现得非常好,单调地与顺序吞吐量周期类似,没有意外。但是,消耗的CPU资源量令人担忧。吞吐周期越短,在重度争用场景中浪费CPU资源就越多!这实际上很重要,因为它与常识相反!一般来说,我们预计较短的吞吐量周期会导致较低的CPU负载,这是由线程挂起/恢复技术驱动的期望,但在这些情况下,Monitor C# lock是由其混合性质驱动的。如果争用足够高,或者峰值负载有可能产生高争用,那么Monitor C# lock就会成为一个严重的瓶颈。有两种可能的情况:
- 高争用正常发生,代码库是为高吞吐期而制作的,因此很快就会被识别和处理。
- 高峰期偶尔会出现高争用,很难用复杂的代码库重新创建,那么代码库的性能问题很容易被忽视,问题可以长期隐藏。
解释很简单。如前所述Monitor,C# lock是一种混合锁。在尝试获得访问权的过程中,当其他线程已经拥有访问权时,该线程不会立即挂起,而是在等待更快地获得访问权,希望限制线程挂起/恢复成本,如果没有工作,线程被挂起。这种混合方法是在第一个多核盒子时代引入的,当时它使吞吐量周期更快,但现在有24个或更多核,它带来了沉重的成本,并且每增加一个核,情况就会变得更糟。
我们在Monitor中的工作时间很短,C# lock定的场景,但是有很多请求会丢失大量CPU资源!显然,我们不能再忽视落后于硬件架构的遗留代码库和老化的软件架构。
根据图表,应该避免给定盒子上低于64 ms的吞吐量周期,因为超过60%的CPU资源将耗散到热量中!线程数越少,该数字就会越低。避免高并发始终是一个很好的检查点,是否我们不会在顺序问题上抛出过多的并行性。但是,随着CPU内核的增加,这种浪费60%或更多CPU资源的吞吐量周期水平会更高,这就是当前的未来趋势。
图表 1:Monitor(C# lock)——重度争用基准
基准测试:Monitor (C# Lock)——坏邻居请记住,坏邻居场景是关于多个不相关线程对的累积效应,每个线程对只有两个线程(一对)的最小争用。正如上面的报告中的重争用场景监视器中提到的,C# lock,低争用,可通过监视器访问的吞吐量周期,C# lock可以显着降低,而不会造成大量CPU浪费,但即使在这里我们也可以看到,监视器,C# lock,在CPU浪费中起作用,在低于16毫秒的吞吐量期间是预期的两倍,因此监视器,C# lock实际上是一个坏邻居,很少有低争用的不相关服务/线程可以累积将超过50%的CPU资源消耗为热量仅完成了48%的有用工作。该陈述基于图表中的理想连续趋势线。
图表 2:Monitor(C# lock)——坏邻居基准
基准:LockUC——重度竞争LockUC是一种新的同步原语TaskCompletionSource,其使用方式与Monitor C# lock非常相似,但避免了自旋等待Monitor(C# lock的混合部分)。访问是公平的。LockUC旨在超越Monitor, C#lock,在线程挂起/恢复技术仍然有用的区域避免其混合性质,对于遗留代码,在大范围的吞吐量周期内不需要超高的吞吐量周期,但同时不消耗CPU资源不必要地转化为热量。无法获得lock访问的线程立即挂起!对于我们不期望高争用甚至不关心它的所有情况,都应该鼓励这种情况。但是,我们应该关心的是在服务的其他部分难以创建的边缘情况下被忽视的同步原语的影响。我们应该使用最简单、影响最小且能够保证其行为的工具。LockUC是不带async/await选项的经典线程的不错选择。LockUC明显优于MonitorC# lock,浪费更少的CPU资源,甚至吞吐周期在给定框的1.5毫秒吞吐周期内也更好。低于1.5 ms的吞吐量周期Monitor,C# lock正在取得进展LockUC,但它们都已经消耗了几乎所有可用的CPU资源,在同步时有效地消耗了比实际做一些有用的工作更多的CPU资源!建议单独使用线程挂起/恢复方法进行线程处理在该行之外无法很好地工作,并且必须考虑其他方法。
图表 3:LockUC——重度竞争基准
基准:LockUC—坏邻居请记住,坏邻居场景是关于多个不相关的线程对的累积影响,每个线程对的争用最小。该LockUC失去Monitor,C#lock的吞吐量期上一点点,但对于这一点,CPU的性能显著提高。如果吞吐量周期会成为问题,则应使用其他同步原语。比如SpinLockUC。
图表 4:LockUC——坏邻居基准
基准测试:SpinLockUC/AsyncSpinLockUC——重度争用SpinLockUC是一个围绕.NET SpinLock struct的瘦包装器,旨在为访问线程在受保护部分相遇的可能性很低的情况提供最佳吞吐量周期,避免公平有助于提高吞吐量周期。如果受保护部分中的操作很短,即使有24个线程,也很难导致多个线程旋转更长时间以获得访问权限的情况。更可能的情况是,在新的访问请求到来之前,大多数时候之前的访问请求已经被处理了。这是一个理想的情况,但上面的图表表明当吞吐量周期小于324微秒时,给定框上的24个线程达到临界点,如果有更多线程试图获得访问权,那么在此之后一些处于高争用状态的线程会有效地将时间花在旋转/等待获得访问权上,即使是几秒钟同时因为访问不公平。这种情况在NUMA机器上是一个很大的问题,因为对于某些线程来说,访问某些内存比其他线程更容易,这是基于特定内存与CPU核心的NUMA节点距离的度量,这加剧了缺乏公平性的问题。跨NUMA节点接入成本的影响可以通过工具CoreInfo来测量,相对从标记Russinovich,微软:https://docs.microsoft.com/en-us/sysinternals/downloads/coreinfo。为SpinLock寻求最高吞吐量周期的设计要求伴随着这个价格。请理解这些限制并不是SpinLock实现中留下的一些错误!这些问题是基于原子指令的同步原语的内在属性,无法保证访问公平性!未来似乎正朝着更多CPU内核和更多NUMA节点的方向前进。这是一个承诺,未来的盒子将把吞吐量周期的临界点推到324µs 以下。我们应该为此做好准备,因为这些问题是Red Hat Enterprise Linux已经实施了TicketSpinLock。 它的吞吐量周期稍差,但为此,TicketSpinLock能够保证公平访问,这对于更好的负载平衡很重要,并使系统更接近实时操作系统质量。
不公平访问可以在无法解释的吞吐量周期瞬间下降中发挥重要作用,我们应该牢记如何避免它!我们必须使SpinLock条目中的执行速度非常快,理想情况下,只执行内存更新指令,避免慢速计算并严格避免线程因I/O或内核调用而暂停,如果可能,还应避免内存分配。
在图5中可以清楚地看到SpinLockUC不公平性,因为它导致吞吐量周期统计数据出现峰值,因为不同线程的运行方式截然不同。一些线程在超过324 µs的吞吐量周期内在10秒内获得大量条目,而几乎没有条目。
图6仅显示了吞吐量期,特别是中位数和最大吞吐量期,我们可以看到两组不同线程的行为,一组几乎总是进入,那些更接近理想的吞吐量期,另一个更接近最大吞吐量期对于那些没有获得这么多条目或根本没有条目的人来说,10秒!不得不说,试图在SpinLock中执行长时间运行的代码是一个坏主意,图5和图6显示了它是多么没有意义。
图表 5:SpinLockUC/AsyncSpinLockUC - 重度争用基准
图表 6:SpinLockUC/AsyncSpinLockUC——重度争用基准——不公平——中值与最大吞吐量周期
基准测试:SpinLockUC/AsyncSpinLockUC——坏邻居SpinlockUC基于.NETSpinLock struct是打得非常好于坏邻居案件的情况下,因为竞争是最小的,但仍然同时有一对线程。SpinLock专为争用不频繁且lock定条目内的执行将保持极短的情况而设计。通过以执行路径在lock定入口外比在lock定入口内花费更长的时间来平衡代码,可以进一步降低CPU浪费。
图表 7:LockUC——坏邻居基准
基准测试:TicketSpinLockUC/AsyncTicketSpinLockUC——重度争用TicketSpinLockUC是一个通用的TicketSpinLock实现,它将竞争原子操作,直到它根据到达的票证顺序获得访问,因此访问是公平的。没有像.NET Spinlock那样的自旋等待或让步或休眠,为此,它付出了原子操作争用的沉重成本并占用了大量CPU资源,并且由于公平性,它也失去了下面的吞吐期在给定的盒子上为200 µs,但这并不重要!它适用于lock定条目中极短的执行路径,理想情况下只有几条指令或更新一些变量,以确定公平访问,FIFO顺序,将得到保证。这是一个同步原语,可用于具有确保线程饥饿保护的负载平衡算法。
图表 8:SpinLockUC ——重度竞争基准
基准测试:TicketSpinLockUC/AsyncTicketSpinLockUC——坏邻居TicketSpinLockUC实现显示出在给定框上吞吐量周期低于200 µs的改进,因为坏邻居场景的线程数降低到每个同步原语只有两个线程。这是一个很好的例子,在顺序问题上投入过多的并行性是一个坏主意,它让我们有动力在线程数量和锁定入口内和锁定入口外的执行时间之间取得适当的平衡,后者应该需要更长的时间。
图表 9:LockUC ——坏邻居基准
基准测试:AsyncLockUC——重度争用AsyncLockUC唯一真正是async/await同步原语,实际上是返回不完全awaiter,定时和调度然后基于ThreadPool所述。显然,如果ThreadPool线程耗尽,那么同步将失去性能,但让我们面对现实,这也意味着设计不佳的软件和挂起/恢复同步技术在这种情况下也会失去性能,因为许多活动线程竞争CPU时隙。
AsyncLockUC似乎有有趣的表现,特别在CPU浪费在这里非常低。访问是公平的。内部数据结构内部的锁定是基于无锁的,并且尽可能快和短,以避免任何争用。这只是本报告中的同步原语,不会导致CPU堵塞=> CPU正在燃烧周期的那一刻,但正在完成的工作实际上非常小,因为同步的成本几乎占用了所有CPU资源。
AsyncLockUC表明它可以处理任何工作负载,包括高峰重争用,它不会无谓地耗散CPU资源,并且机器上的其他服务仍有可用资源来执行其职责。这里的CPU使用率低于42%(最大值)!
AsyncLockUC尽可能接近理想同步原语,因为对于超过200 µs的吞吐量周期,它的行为正在快速接近理想同步的原语性能!
图表 10:AsyncLockUC——重度争用基准
基准测试:AsyncLockUC——坏邻居AsyncLockUC似乎在坏邻居方案合理表现不好。
图表 11:AsyncLockUC ——坏邻居基准
跨基准测试:Monitor (C# Lock) & SpinLockUC & TicketSpinLockUC——重度争用最后,我们来到了第一个跨基准测试!Monitor,C# lock,具有混合性质,因此适合用SpinLockUC和TicketSpinLockUC来交叉检查其行为。
Monitor,C# lock是一个线程挂起/恢复同步原语,通过混合启发式避免线程挂起和自旋等待而增强,但它不是免费的!我们可以看到,低于1 ms的吞吐量周期SpinLockUC和Monitor,C# lock的行为与CPU和吞吐量周期非常相似。TicketSpinLockUC在吞吐量期间,公平访问正在失去他们。
我相信图表显示了很好的关系,尤其是在哪里使用什么:
- 显然,SpinLockUC对于超过324 µs的任何吞吐量周期都应该避免,因为线程饥饿是很可能的。
- TicketSpinLockUC应该仅在我们需要确保公平性的情况下使用,例如负载平衡算法,并且仅用于锁定区域中的非常短的操作。我建议低于100 µs的吞吐量周期,否则CPU浪费是不合理的。
- 这留下了一个悬而未决的Monitor问题,在324 µs和32 ms吞吐量周期之间,C# lock的替代品是什么?
图表 12:Monitor(C# lock)、SpinLockUC 和 TicketSpinLockUC——重度争用跨基准测试
跨基准测试:Monitor (C# lock) & SpinLockUC & TicketSpinLockUC——坏邻居坏邻居场景的Monitor C# lock的跨基准测试清楚地显示了SpinLockUC将在哪里找到它的最佳用法。在吞吐量周期短且争用最小的算法中。SpinLockUC将能够节省大量的CPU资源,因为Monitor,C#lock的混合性质在这些情况下显然是浪费了大量的CPU资源。
图表 13:监视器(C# lock)、SpinLockUC 和 TicketSpinLockUC——坏邻居交叉基准
跨基准测试:Monitor (C# lock)和LockUC——重度争用Monitor、C# lock和合理替换的跨基准测试,LockUC在重竞争场景中,正在回答在324µs和32ms吞吐量周期之间使用什么同步原语。LockUC明确成为很好的替代,因为CPU损耗较低,它比用监视器,C#lock越来越差的方式后。
图表 14:Monitor (C# lock) 和 LockUC——重度争用跨基准测试
跨基准测试:Monitor (C# lock)&LockUC——坏邻居具有最小争用的Monitor、C# lock和LockUC的跨基准测试的坏邻居场景显示了Monitor、C# lock的性能问题,因为即使对于LockUC的CPU浪费正在下降的极短执行路径,Monitor、C# lock仍在燃烧中央处理器!
图表 15:Monitor (C# lock) 和 LockUC——Bad Neighbor Cross-Benchmark
跨基准测试:Monitor (C# lock) &LockUC & AsyncLockUC——重度争用最后,我们可以检查Monitor(C# lock)、LockUC和客观最佳同步原语AsyncLockUC在重竞争场景中的跨基准测试。
如果非要停留在一般的线程环境,最好用LockUC代替Monitor(C#lock),因为这样浪费的CPU资源更少,实际上在某些配置下甚至可以同时做更多的工作。
如果我们可以重写Async/Await环境的代码,我们可以选择GreenSuperGreen/Unified Concurrency下的最佳选项AsyncLockUC,即尽可能接近理想同步原语,并避免其他同步原语的CPU僵局!
有一些情况是由Monitor C# lock执行的,在这种情况下,低吞吐量期是必要的,不考虑CPU浪费。我希望我已经用基准进行了充分的描述,在这种情况下,使用Monitor C# lock的CPU浪费非常高。这样的吞吐量周期可以通过SpinLockUC/ AsyncSpinLockUC、TicketSpinLockUC/AsyncTicketSpinLockUC来实现,所有的基准测试都表明代码的这些部分需要不同的思考和设计,否则它们就会成为瓶颈。
图表 16:Monitor (C# lock) & LockUC & AsyncLockUC——重度争用跨基准测试
跨基准测试:Monitor (C# lock)&LockUC&AsyncLockUC——坏邻居坏邻居场景应该不足为奇。这些建议与重争用场景的建议非常相似。
如果非要停留在一般的线程环境中,最好将Monitor(C# lock)替换为LockUC,因为这样浪费的CPU资源更少,在某些配置中实际上甚至可以同时做更多的工作。
如果我们可以重写Async/Await环境的代码,我们可以选择GreenSuperGreen/Unified Concurrency下的最佳选项AsyncLockUC,即尽可能接近理想同步原语,并避免其他同步原语的CPU gridlock!
图表 17:监视器(C# lock)& LockUC 和 AsyncLockUC——坏邻居交叉基准
跨基准测试:Monitor (C# lock) & LockUC & AsyncLockUC & SpinLockUC & TicketSpinLockUC——重度争用最后,我们可以检查Monitor的Cross-Benchmark(C# lock),LockUC客观上最好的同步原语AsyncLockUC是重竞争场景。
如果非要停留在一般的线程环境,最好用LockUC代替Monitor(C#lock),因为这样浪费的CPU资源更少,实际上在某些配置下甚至可以同时做更多的工作。
如果我们可以重写Async/Await环境的代码,我们可以选择 GreenSuperGreen/Unified Concurrency下的最佳选项AsyncLockUC,即尽可能接近理想同步原语,并避免其他同步原语的 CPU gridlock!
有一些情况是由Monitor C# lock执行的,在这种情况下,低吞吐量期是必要的,不考虑CPU浪费。我希望我已经用基准进行了充分的描述,在这种情况下,使用Monitor C# lock的 CPU 浪费非常高。这样的吞吐量周期可以通过SpinLockUC/ AsyncSpinLockUC、TicketSpinLockUC/AsyncTicketSpinLockUC来实现,所有的基准测试都表明代码的这些部分需要不同的思考和设计,否则它们就会成为瓶颈。
图表 18:Monitor (C# lock) & LockUC & AsyncLockUC & SpinLockUC & TicketSpinLockUC ——重度争用跨基准测试
跨基准测试:Monitor (C# lock) & LockUC & AsyncLockUC & SpinLockUC & TicketSpinLockUC——坏邻居坏邻居场景应该不足为奇。这些建议与重争用场景的建议非常相似。
如果非要停留在一般的线程环境中,最好将Monitor(C#lock)替换为LockUC,因为这样浪费的CPU资源更少,在某些配置中实际上甚至可以同时做更多的工作。
如果我们可以重写Async/Await环境的代码,我们可以选择GreenSuperGreen/Unified Concurrency下的最佳选项AsyncLockUC,即尽可能接近理想同步原语,并避免其他同步原语的CPU僵局!
图表 18:Monitor (C# lock) & LockUC & AsyncLockUC & SpinLockUC & TicketSpinLockUC —— Bad Neighbor Cross-Benchmark
概括本文介绍了在UnifiedConcurrency框架GreenSuperGreen库下实现同步原语的基准测试和跨基准测试,用于两种不同的场景,重争用和坏邻居。
我们根据场景和情况单独和相互比较地讨论了同步原语的优点、缺点、成本、弱点、优点和行为。
https://www.codeproject.com/Articles/1242156/Unified-Concurrency-III-cross-benchmarking