Ложное совместное использование в boost:: detail:: spinlock_pool? - программирование
Подтвердить что ты не робот

Ложное совместное использование в boost:: detail:: spinlock_pool?

Я наткнулся на это 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 и т.д. У меня сейчас нет доступа к этому оборудованию.

4b9b3361

Ответ 1

Краткое

Вы правы, поскольку в шпиндельных узлах используется одна и та же строка кэша, если они могут быть в разных строках кэша. Ака ложного обмена. И вполне может быть некоторое усиление производительности, выделяемое блокировками в разных строках кэша (но см. Ниже).

Однако это НЕ то же самое, что блокировка. Конфликт блокировок возникает, когда блокировка удерживается одним парнем, и один или несколько других ребята хотят получить к нему доступ.

т.е. У spinlock_pool есть аргумент в отношении строки кэша, вызванный совместными резидентными в одной строке кэша. Но он имеет (уменьшенную) конкуренцию блокировки программного обеспечения.

Уменьшенный блокировка программного обеспечения раздора почти undoubtedy хорошо.

Конфликт в кеш-памяти, вероятно, немного болит, так как ваши тесты показывают в некоторой степени, но это эффект второго порядка по сравнению с конкуренцией блокировки программного обеспечения.

ДЕТАЛЬ

Фон

Во-первых, некоторый фон:

Классические spinloops являются тестовыми и тестовыми:

    loop: 
         tmp := load( memory_location_of_sw_lock )
         if( is_locked(tmp) ) goto loop
         was_locked := atomic_hw_locked_rmw( memory_location_of_sw_lock, 
                                             value_that_will_get_sw_lock )
         if( was_locked(tmp) ) goto loop
     got_the_sw_lock:
          ...
          ... critical section
          ...
          // release the lock, e.g.
          memory_location_of_sw_lock := 0

Есть также тестовые и заданные спин-блоки, которые выглядят как

    loop: 
         was_locked := atomic_hw_locked_rmw( memory_location_of_sw_lock, 
                                             value_that_will_get_sw_lock )
         if( was_locked(tmp) ) goto loop

Они могут иметь очень плохую производительность на большинстве современных систем памяти с кэшами, с обратной записью или обратной записью. (Хотя некоторые из предложенных мной аппаратных оптимизаций делают тестово-заданные спинлопы так же быстро, как и тестовые и тестовые настройки spinloops - немного быстрее, потому что они меньше.)

Обратите внимание, что здесь есть две разные концепции блокировки: "программная" блокировка, которую получает спин-блокировка, и аппаратная блокировка, которая используется инструкцией atom_hw_locked_rmw, такой как Intel LOCK INC mem или CMPXCHG. Мы не очень заботимся о последнем, за исключением того, что он обычно безоговорочно записывает в строку кэша, содержащую блокировку программного обеспечения, недействительные другие копии строки кэша. (Сделать условное обозначение записи является еще одной возможной аппаратной оптимизацией.)

O (N ^ 2) всплеск промахов в кеше (софт)

Конфликт блокировки с тестовыми и тестовыми наборами особенно хорош. Официанты все вращаются на замке, и когда он выпущен, происходит всплеск доступа к шине. Один парень побеждает, другие понимают, что они проиграли, и в конце концов они успокоились, чтобы снова развернуться. Этот всплеск активности особенно плох, потому что для N ожидающих парней (потоков/процессов/процессоров) всплеск активности шины может быть O (N ^ 2) по размеру, потому что в худшем случае все выходят из тестовой части тестовой части, и-test-and-set spinloop, и каждый пытается выполнить атомарную запись RMW (чтение-изменение-запись), такую ​​как x86 LOCK INC mem или CMPXCHG, в то же время. Это означает, что каждый в конце концов напишет строку, хотя всем, кроме первого, не нужно писать блокировку, потому что они не получат блокировку.

например.

Lock is held by P0
P1-PN are spinning in test-and-test-and-set spinloops waiting for it.

P0 releases the lock, e.g. by writing it to 0

P1 "test-" instruction reads the lock
...
PN "test-" instruction reads the lock

All of P1-PN see the lock as released, 
   so they fall out of the "test-" part of the spinloop which uses ordinary instructions
   to the test-and-set part of the spinloop, which uses a hardware atomic RMW like LOCK INC mem

P1 locked RMW happens, and acquires the spinlock for P1
   It invalidates all other cache lines
   P1 goes away and does something with the data protected by the lock

P2 locked RMW fails to acquire the spinlock
   It invalidates all other caches because it has a write
   P1 falls back into its test- spinloop

P3 locked RMW fails
   It invalidates all other caches because it has a write
   P1 falls back into its test- spinloop

...

PN locked RMW fails

и теперь, по крайней мере, все оставшиеся процессоры P2..PN должны выполнять обычные разблокированные промахи кэшей для своего тестового прокрутки. Это подразумевает по меньшей мере N + (N-1) пропуски кеша. Это может быть значительно хуже, потому что каждый из них может написать официанту, который не сможет получить блокировку, чтобы заставить всех остальных официантов сделать разблокированное чтение. I.e., в зависимости от времени, вы можете получить

   1 release the lock
   N reads to test- the lock
   1 locked atomic RMW to acquire the lock
   N reads...
   for each of the remaining N-1 processors
      1 locked RMW that fails to acquire the lock
      N reads 

который является O (N ^ 2). Или, возможно, для процессора M, 1 заблокирован RMW, а затем 2..M читает - это все еще O (N ^ 2).

Что это значит для этого вопроса?

ОК, если есть истинная ошибка блокировки, вы получаете этот O (N ^ 2) пакет промахов в кеше при выпуске исправленной блокировки.

Тем не менее, spinlock_pool распространяет официантов на несколько блокировок. Если в пуле спинлоков есть S-блокировки, вы получаете гораздо меньше официантов: вероятно, меньше N/S (потому что соперничество имеет тенденцию быть суперлинейным по количеству людей, имеющих одну и ту же блокировку).

т.е. с Boost spinlock_pool, вы можете наивно ожидать получить 1/41-й уровень конкуренции --- и, вероятно, меньше этого.

Помните, что spinlock_pool является компромиссом между наличием блокировки на shared_pointer, увеличением размера общих указателей и наличием всех общих_последователей одной и той же блокировки. Таким образом, любые споры о spinlock в share_pointer могут быть (a) истинными соперниками или (b) ложными конфликтами, вызванными shared_pointers, которые являются независимым хэшированием к той же записи в spinlock_pool.

Теперь, да, если вместо того, чтобы иметь N официантов, у вас есть "только" N/41 официанты, тогда пакет все равно O ((N/41) ^ 2) или O (N ^ 2). Но, если N обычно меньше 41... вы получаете идею.

В основном, хеширование для распространения shared_Pointers на несколько записей spinlock_pool быстро уменьшает количество конфликтов.

Но... спин-блоки живут в одной и той же линии? Правильно... но официанты в других строках кэша не будут продвигаться к тестовой части их спинлоопа.

т.е. если заявленная блокировка с M официантами разделяет линию кэша с M другими процессами, вы получите трафик M * N. Но если хеширование уменьшило M до O (1), вы получите только N трафика.

И если в большинстве случаев других официантов вообще нет, тогда вы получаете только O (1) raffic в релизе блокировки.

BOTTOM LINE:

Сокращение конкуренции с программным блокированием намного выше, чем при уменьшении кеша, что приводит к конфликту в линейке кэша.

Теперь все еще может быть полезно, если вы не упакуете столько записей spinlock_pool в одну и ту же строку кеша. Но это отнюдь не очевидно; это эмпирический вопрос, под которым я подразумеваю, что вам нужно запустить эксперимент, и он будет варьироваться в зависимости от рабочей нагрузки.

Иногда ложное совместное использование этих блокировок в одной строке кэша будет плохой. Классическим примером являются блокировки, которые защищают очереди выполнения процессоров.

Иногда ложное совместное использование этих блокировок в одной и той же строке кэша является хорошим - оно может обеспечить те же преимущества, что и предварительная выборка в кеше. Например. представьте, что вы выделили массив shared_pointers и что люди обычно получают доступ к aosptr [i], а затем aosptr [i + 1] и т.д.

Все зависит от вашей рабочей нагрузки. Я видел, как он падал в обоих направлениях. Обычно, по моему опыту, один замок на строку кэша лучше, хотя часто это не так много.

БОЛЬШЕ FUN

В случае, если вам небезразлично, оптимизация аппаратных средств для тестовых и тестовых наборов spinloops была предметом моей тезисов в области МС, "таксономия функциональных возможностей примитивов синхронизации, включая блокировку блокировки шины" - протокол кэширования аппаратного обновления, который устранил всплеск доступа к шине. Не публикуется ни в одной официальной публикации, кроме университетской микрофиши.

Моя работа была вдохновлена ​​работой "Производительность альтернатив спин-блокировки для многопроцессоров с общей памятью" Т. Андерсона - IEEE Transactions on Parallel and Distributed Systems, 1990.

Я считаю справедливым сказать, что большинство публикаций посвящено программному обеспечению, алгоритмам, включая знаменитую блокировку MCS. Я думаю, справедливо также сказать, что такие методы, в то время как популярны в теории, мало использовались программистами-подмастерьями.

Кстати, здесь есть еще несколько аппаратных оптимизаций. Например. CMPXCHG вообще не нужно записывать блокировку. К сожалению, на нынешнем оборудовании Intel или, по крайней мере, на оборудовании Intel, которое я разработал в 1991 году, которое, как я считаю, используется, единственный способ освободить аппаратный замок (который используется для реализации атомно заблокированного RMW) заключается в использовании специального микрокод пишите "store-unlock". Heck, микрокод может даже использовать LoadLinked/StoreConditional (LL/SC), возвращаясь к блокировке в тех ситуациях, где наивный LL/SC не делает движения вперед по некоторым потокам.

Возможно, Intel недавно установила это. Я оставил Intel в 2009 году. Я пытался ее исправить, улучшить, оптимизировать, о, с 1991 года. И Intel недавно улучшала производительность шлюзов. Но я думаю, что они в основном были обработаны по незащищенной работе блокировки и не оптимизировали конкурирующую производительность блокировки.

Аналогично, Ален Каги, в своей диссертации и в некоторых публикациях, а также в патенте http://www.google.com/patents/US6460124, показывает, что, добавляя разумные задержки, аппаратное обеспечение кеша может создавать блокировки так же эффективны, как блокировки в очереди.

Некоторые из этих оптимизаций аппаратного обеспечения делают тестовые и установочные spinloops лучше, чем тестовые и тестовые и заданные spinloops.

Самая последняя разработка - это Intel TSX (Transactional Synchronization Extensions), состоящая из HLE (Hardware Lock Elision (Ravi Rajwar), работавшего над этим в UWisc, в то время как я и Ален Каги были там, хотя моя работа была синхронизирована ранее в UIUC) ) и RTM (ограниченная транзакционная память). Обе эти проблемы помогают безгранично. Хм, они помогают с конкурирующими замками, которые ложно борются, например. крупный зерновой замок, защищающий то, что может быть независимым. То есть ложное нарушение блокировки. В некотором смысле, HLE может сделать spinlock_pool ненужным.

ИЗВИНЕНИЕ

Извините: я дал длинный ответ, который сводится к тому, что "сокращение конкуренции в программном обеспечении может быть гораздо более важным, чем утверждение линейки кэша оборудования для spinloops".

В то время как я ожидал бы некоторой производительности, получаемой от выделения 1 линии spinloop на строку кэша, она может быть небольшой, а на некоторых не совсем-необычных рабочих нагрузках может быть даже потеря.

И, конечно же, вам нужно будет его измерить.

Но, в любом случае, большой выигрыш возникает из-за сокращения конкуренции в программном обеспечении.