Быстрее фундаментальных структур данных на многоядерных машинах? - PullRequest
12 голосов
/ 25 февраля 2009

Я долго размышлял над этим вопросом:

Можете ли вы построить более быструю фундаментальную структуру данных (например, связанный список, хэш-таблицу, набор, пропускающий список, фильтр Блума, красно-черное дерево и т. Д.) На многоядерном компьютере, воспользовавшись тем, что у вас есть более один процессор?

Я провел некоторые предварительные эксперименты с pthreads и обнаружил, что pthread_create () занимает порядка 30us, но простая вставка hash_map занимает гораздо меньше времени, чем на одном ядре. И поэтому мне стало трудно представить создание более быстрого hash_map <>, поскольку примитивы синхронизации и создание потоков выполняются очень медленно. Я также могу представить параллельный обход и балансировку деревьев, но опять-таки примитивы синхронизации, казалось бы, увеличивают время выполнения, а не сокращают его.

Мне все еще кажется интуитивно понятным, что «у меня больше ЦП, и, следовательно, я должен быть в состоянии сделать это быстрее», но я не могу полностью обернуть голову вокруг доказательства или контр-доказательства этого утверждения , Я довольно много экспериментировал с C ++, но теперь подозреваю, что другие языки могут предложить лучшие решения (erlang?) Для этой задачи. Мысли?

РЕДАКТИРОВАТЬ детали: я думаю, что есть несколько парадигм программирования / структуры данных, которые часто используются и которые могут быть ускорены. Например, я часто пишу код, который в основном выглядит следующим образом (где реальные данные были заменены на «rand ()»)

static const int N = 1000000; 
static const int M = 10000000; // 10x more lookups 
hash_map<int, int> m; 
// batch insert a bunch of interesting data 
for (int i = 0; i < N; i++) m[rand()] = rand(); 

// Do some random access lookups. 
for (int i = 0; i < M; i++) m[rand()]++;

Этот вид парадигмы часто используется для таких вещей, как настройки значений и данных конфигурации, пакетная обработка и т. Д. 10-кратное (или более) соотношение поиска / вставки - это то, что делает традиционный hash_map <> идеальным для операций такого типа. ,

Это может быть легко разделено пополам, с фазой вставки и фазой поиска, и в параллельном мире может быть некоторая операция «очистки очереди» между двумя половинами. Более сложным является чередующийся вариант вставки + поиска:

hash_map<int, int> m; 

for (int i = 0; i < N; i++) { 
   if (rand() % LOOKUP_RATIO == 0) 
     hash_map[rand()]++;  // "lookup" 
   else 
     hash_map[rand()] = rand();  // "insert" 
}

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

Представьте на секунду следующий фрагмент:

hash_map<int,int> a;
hash_map<int,int> b; 
for (int i = 0; i < N; i++) { 
  // the following 2 lines could be executed in parallel 
  a[rand()] = rand(); 
  b[rand()] = rand(); 
}

И, таким образом, поиск может быть выполнен "параллельно":

int lookup(int value) { 
  // The following 2 lines could be executed in parallel: 
  v1 = a[value]; 
  v2 = b[value]; 
  if (v1)  // pseudo code for "value existed in a" 
    return v1; 
  else 
    return v2; 
}

Ответы [ 8 ]

6 голосов
/ 25 февраля 2009

Проблема в том, что совместно используемые данные сами по себе являются областью параллельных вычислений. В идеале вы хотите, чтобы каждое ядро ​​работало с отдельными данными, иначе с синхронизацией будут связаны накладные расходы. (Как общаться без общего состояния? Передача сообщений.)

Также немного странно говорить об ускорении структур данных. Я считаю более естественным говорить об ускорении операций над структурой данных, поскольку различные операции над разными структурами данных имеют разные характеристики. Есть ли определенный тип доступа, который вы хотите ускорить?

РЕДАКТИРОВАТЬ, в ответ на дополнительные детали: я предполагаю, что цель состоит в том, чтобы иметь одну хеш-карту, к которой можно получить доступ параллельно, и ее основой могли бы быть несколько хеш-таблиц, но которые были бы прозрачно представлены пользователю эта структура данных в виде одной хэш-таблицы. Естественно, мы бы беспокоились о том, чтобы тратить слишком много времени на вращение замков. Также на этом уровне мы должны знать о проблемах согласованности кэша. То есть, если ядра или процессоры имеют отдельные кэши, указывающие на одни и те же данные, и один изменяет данные, то кэшированные данные в другом становятся недействительными. Если это случается многократно, это может повлечь за собой огромные затраты, и параллелизм может быть хуже, чем наличие одного ядра. Поэтому я очень осторожен с общими данными.

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

3 голосов
/ 25 февраля 2009

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

Если у вас есть массивы данных для использования, я считаю, что почти всегда лучше выделять меньший массив для работы для каждого потока, а затем объединить небольшие массивы обратно в мастер-массив после завершения - фактически, если вы в кластерной среде использование «одного и того же» массива даже невозможно!

Если вы реализуете алгоритм, который использует ассоциативные массивы (например, словарь .NET), вы почти всегда собираетесь дублировать некоторую работу где-то между потоками. Старайтесь избегать этого, когда это возможно.

Если вы программируете для среды CUDA (GPU), вы очень быстро поймете, что весь мир (нет, должен!) Может быть преобразован в массив перед работой:)

3 голосов
/ 25 февраля 2009

во-первых, я не думаю, что уместно сравнивать pthread_create() время с операцией hashmap. лучше сравнивать с (не) временем блокировки как в спорных, так и в неконтролируемых случаях.

Тем не менее, вы правы, время синхронизации является узким местом и ухудшается, поскольку они должны идти на межпроцессорную шину / мост / канал, что угодно, в то время как большинство других структур данных пытаются оставаться в кеше (или даже в теневые регистры).

Есть два основных направления для решения этой проблемы:

  1. улучшенные общие структуры: проверка структур без блокировки и / или транзакционной памяти. оба пытаются максимизировать доступность, заменяя цикл «lock-modify-release» на «try-check-commit / rollback». в большинстве случаев проверка должна пройти успешно, поэтому откат не должен влиять на среднюю производительность. обычно проверка / фиксация выполняется атомарно, поэтому это дорого с точки зрения пропускной способности процессора, но намного меньше, чем традиционные блокировки.

  2. меньше совместного использования: это то, что подчеркивают языки erlang / haskell. облегчая и удешевляя передачу небольших сообщений, межпотоковое взаимодействие больше похоже на вызовы функций с параметрами и меньше, чем общая память. это гораздо более масштабируемо, поскольку только два процесса должны синхронизироваться и могут (теоретически) использовать не-RAM каналы с меньшими задержками.

редактирование: Я удивлен, что никто не имеет никакого мнения о структурах без блокировки. проверьте this (pdf) и this (видео) о реализации без блокировок в Java, которая масштабируется (почти) линейно до 300 CPUS

1 голос
/ 23 марта 2011

Пожалуйста, посмотрите эту статью CACM - Структуры данных для многоядерного возраста (к сожалению, это премиум-контент): http://cacm.acm.org/magazines/2011/3/105308-data-structures-in-the-multicore-age/fulltext

Ранняя версия статьи находится здесь: http://www.cs.tau.ac.il/~shanir/concurrent-data-structures.pdf

1 голос
/ 25 февраля 2009

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

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

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

Это также относится к векторам, деревьям B + и двоичным деревьям, хотя я считаю, что хеш-таблицы могут быть сконструированы так, чтобы для обновлений требовалось немного меньше блокировок.

1 голос
/ 25 февраля 2009

Я думаю, вам нужно взглянуть на структуры данных и спросить: «Что в этом можно сделать асинхронно?»

И для многих структур данных я не вижу ничего особенного.

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

0 голосов
/ 25 февраля 2009

Поместите все в рабочие очереди. Это ключ - и вы приблизитесь к масштабированию на нескольких машинах. Синхронизация стоит дорого и только потом станет дороже (представьте, что у вас есть барьер памяти со 128 процессорами).

0 голосов
/ 25 февраля 2009

У Хавьера есть хорошая точка зрения: если вы выполняете операции параллельно, у вас уже есть потоки, вам просто нужно дать им что-то сделать.

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

Следует учитывать наличие одного (или небольшого пула) потоков на структуру данных и рассматривать доступ как «службу». То есть вместо потока, ищущего что-то в хэш-карте, он выдает синхронный запрос потоку, обслуживающему эту структуру данных. Это локализует операции блокировки (только потоки, обслуживающие запросы, должны знать о технике блокировки), но может сделать очередь запросов узким местом.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...