Я наткнулся на это SO question и, прочитав его, в конце концов заставил меня посмотреть boost::detail::spinlock_pool
.
Цель boost::detail::spinlock_pool
- уменьшить потенциальную конкуренцию для глобальной спин-блокировки, выбирая из массива spinlock
путем хеширования по адресу shared_ptr
. Это похоже на разумное решение, но, похоже, проблема с текущей версией версии (Boost v1.49).
spinlock_pool
управляет статически выделенным массивом из 41 spinlock
экземпляров. Похоже, что sizeof(spinlock)==4
для платформ, на которые я смотрел - это означает, что x64 с 64-байтовыми строками кэша будет 16 spinlock
для каждой строки кэша.
т.е. весь массив охватывает все 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
. 32bit, оптимизированный с использованием MSVS 2010, работает на 4-ядерном Xeon W3520 @2,67 ГГц (отключено HyperThreading).
У меня возникли проблемы с получением согласованных результатов из этих тестов - иногда наблюдались ложные изменения времени до 50%. В среднем, однако, оказывается, что версия SLsPerCacheLine==8
была на ~ 25-30% быстрее, чем версия SLsPerCacheLine==1
с таблицей спин-блокировки размером 41.
Было бы интересно посмотреть, как это масштабируется с большим количеством ядер, NUMA, HyperThreading и т.д. У меня сейчас нет доступа к этому оборудованию.