【发布时间】:2012-06-17 18:07:28
【问题描述】:
我遇到了这个 SO question 并阅读它最终导致我查看 boost::detail::spinlock_pool。
boost::detail::spinlock_pool 的目的是通过散列shared_ptr 的地址从spinlocks 数组中进行选择,从而减少对全局自旋锁的潜在争用。这似乎是一个合理的解决方案,但当前 (Boost v1.49) 版本的实现似乎存在问题。
spinlock_pool 管理一个由 41 个spinlock 实例组成的静态分配数组。对于我所查看的平台,sizeof(spinlock)==4 似乎 - 这意味着在 x64 上使用 64 字节缓存线,每个缓存线将有 16 个spinlocks。
即整个数组跨越所有 2 1/2 缓存行。
即一个随机自旋锁与另一个错误共享的概率为 40%。
...这几乎完全违背了池的目的。
我的分析是正确的还是我遗漏了一些重要的东西?
更新:我终于写了一个小基准程序:
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/timer.hpp>
#include <iostream>
#include <vector>
#include <stdlib.h>
using namespace std;
enum { BufferSize = 1<<24, SLsPerCacheLine = 1 };
int ibuffer[BufferSize];
using boost::detail::spinlock;
size_t nslp = 41;
spinlock* pslp = 0;
spinlock& getSpinlock(size_t h)
{
return pslp[ (h%nslp) * SLsPerCacheLine ];
}
void threadFunc(int offset)
{
const size_t mask = BufferSize-1;
for (size_t ii=0, index=(offset&mask); ii<BufferSize; ++ii, index=((index+1)&mask))
{
spinlock& sl = getSpinlock(index);
sl.lock();
ibuffer[index] += 1;
sl.unlock();
}
};
int _tmain(int argc, _TCHAR* argv[])
{
if ( argc>1 )
{
size_t n = wcstoul(argv[1], NULL, 10);
if ( n>0 )
{
nslp = n;
}
}
cout << "Using pool size: "<< nslp << endl;
cout << "sizeof(spinlock): "<< sizeof(spinlock) << endl;
cout << "SLsPerCacheLine: "<< int(SLsPerCacheLine) << endl;
const size_t num = nslp * SLsPerCacheLine;
pslp = new spinlock[num ];
for (size_t ii=0; ii<num ; ii++)
{ memset(pslp+ii,0,sizeof(*pslp)); }
const size_t nThreads = 4;
boost::thread* ppThreads[nThreads];
const int offset[nThreads] = { 17, 101, 229, 1023 };
boost::timer timer;
for (size_t ii=0; ii<nThreads; ii++)
{ ppThreads[ii] = new boost::thread(threadFunc, offset[ii]); }
for (size_t ii=0; ii<nThreads; ii++)
{ ppThreads[ii]->join(); }
cout << "Elapsed time: " << timer.elapsed() << endl;
for (size_t ii=0; ii<nThreads; ii++)
{ delete ppThreads[ii]; }
delete[] pslp;
return 0;
}
我编译了两个版本的代码,一个是SLsPerCacheLine==1,一个是SLsPerCacheLine==8。使用 MSVS 2010 优化的 32 位,在 4 核 Xeon W3520 @ 2.67Ghz 上运行(超线程已禁用)。
我无法从这些测试中获得一致的结果 - 偶尔会观察到高达 50% 的虚假时序变化。然而,平均而言,SLsPerCacheLine==8 版本似乎比自旋锁表大小为 41 的SLsPerCacheLine==1 版本快约 25-30%。
看看它如何随着更多内核、NUMA、超线程等扩展会很有趣。我目前无法使用那种硬件。
【问题讨论】:
-
如果你输入的数据是正确的,我相信你的分析也是正确的。为了验证,您可以将
shared_ptr与在自旋锁变量之间使用足够填充的修改版本进行基准测试。 -
SLsPerCacheLine的名称具有误导性。 :p 无论如何,请记住,您可能会在程序的无关紧要的部分中看到 30% 的增益,并且缓存污染会降低程序中计算密集型部分的性能。 -
也许我在你的想法中遗漏了一些东西,但我认为顺序缓存线大小的内存块通常不会映射到大多数现代硬件上的相同缓存线。所以,是的,41 个对象的数组可能跨越 2.5 个缓存行,但它们不应该冲突。这并不是说没有(或没有或永远不会有)他们会的架构......
-
@Hurkyl:关于 SLsPerCacheLine,你会给它取什么名字?我得出结论的时间是程序输出的 boost::timer 结果。该计划的“无关紧要”部分不属于该时间安排的一部分。我还使用 VTune 对此进行了分析,并确认大部分时间都花在了
spinlock::lock()上——这正是您所期望的。 -
@twalberg:我不明白。我说两个随机选择的自旋锁在同一个高速缓存行上的几率应该是 40%(或 2.5 分之 1)。你认为那句话有什么问题?
标签: c++ boost c++11 shared-ptr boost-thread