Java 中的旋转测试和设置与旋转读取
spin-on-test-and-set vs spin-on-read in Java
背景
我正在使用开源 off-heap cache implementation, ohc。
最近,我在 github 上发现了一个 pull request,建议
replace spin-on-compare-and-set with spin-on-read.
Here是代码的变化,只增加了一行while(lockFieldUpdater.get(this) != 0L)
,类似于
while (true)
{
if (lockFieldUpdater.compareAndSet(this, 0L, t))
return true;
// while(lockFieldUpdater.get(this) != 0L)
Thread.yield();
}
基准性能
我编译它并使用 benchmark tool 测试它:
线上表现
然后我用在生产上,原来读取的平均时间成本是35000纳秒,新版本只需要10000纳秒
问题
这两种实现有什么区别?为什么在这种情况下读取测试要快得多?
为了理解性能提高的原因,了解一点缓存一致性协议是有好处的。原始版本仅依赖于繁重的读-修改-写操作,如果失败则放弃。这是一个笨拙的做法,因为 CAS 操作会产生相当多的缓存一致性流量,以获得缓存行的所有权,使其他核心上的副本无效等。随着线程数量的增加,这种天真的方法会导致大量争用。
修改后的版本是对原始方法的改进,因为它可以更好地同步线程的操作。通过确保每个线程都将在其自己的缓存副本上旋转,只有在本地副本失效(在另一个核心中修改)后,该线程才会再次被允许尝试 CAS。
这与 TATAS locks 是对简单 TAS 锁的改进的原因非常相似。
至于为什么您的本地基准测试显示加速约为 6% 而您的生产服务器看到加速约为 3.5 倍,可能有几个原因可以解释。
- 您的生产服务器从局部变量上旋转受益匪浅,因为跨 NUMA 节点的内存访问会严重影响性能。
- TAS锁和TATAS锁的性能都会随着争用锁的线程数的增加而下降。但是 TATAS 锁降级比 TAS 锁慢。这篇博客文章 Test-and-set spinlocks 有一个很好的图表来说明这一点。也许您的本地基准测试太小而看不到任何显着改进?
背景
我正在使用开源 off-heap cache implementation, ohc。
最近,我在 github 上发现了一个 pull request,建议
replace spin-on-compare-and-set with spin-on-read.
Here是代码的变化,只增加了一行while(lockFieldUpdater.get(this) != 0L)
,类似于
while (true)
{
if (lockFieldUpdater.compareAndSet(this, 0L, t))
return true;
// while(lockFieldUpdater.get(this) != 0L)
Thread.yield();
}
基准性能
我编译它并使用 benchmark tool 测试它:
线上表现
然后我用在生产上,原来读取的平均时间成本是35000纳秒,新版本只需要10000纳秒
问题
这两种实现有什么区别?为什么在这种情况下读取测试要快得多?
为了理解性能提高的原因,了解一点缓存一致性协议是有好处的。原始版本仅依赖于繁重的读-修改-写操作,如果失败则放弃。这是一个笨拙的做法,因为 CAS 操作会产生相当多的缓存一致性流量,以获得缓存行的所有权,使其他核心上的副本无效等。随着线程数量的增加,这种天真的方法会导致大量争用。
修改后的版本是对原始方法的改进,因为它可以更好地同步线程的操作。通过确保每个线程都将在其自己的缓存副本上旋转,只有在本地副本失效(在另一个核心中修改)后,该线程才会再次被允许尝试 CAS。
这与 TATAS locks 是对简单 TAS 锁的改进的原因非常相似。
至于为什么您的本地基准测试显示加速约为 6% 而您的生产服务器看到加速约为 3.5 倍,可能有几个原因可以解释。
- 您的生产服务器从局部变量上旋转受益匪浅,因为跨 NUMA 节点的内存访问会严重影响性能。
- TAS锁和TATAS锁的性能都会随着争用锁的线程数的增加而下降。但是 TATAS 锁降级比 TAS 锁慢。这篇博客文章 Test-and-set spinlocks 有一个很好的图表来说明这一点。也许您的本地基准测试太小而看不到任何显着改进?