Подтвердить что ты не робот

Более быстрые фундаментальные структуры данных на многоядерных машинах?

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

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

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

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

Детали EDIT: Я думаю, что есть несколько парадигм программирования/данных, которые часто используются, которые могут быть ускорены. Например, я часто нахожу код, который в основном выглядит так (где реальные данные были заменены на "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()]++;

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

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

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; 
}
4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

Я бы подумал, что вам нужно посмотреть на структуры данных и спросить: "Что в этом можно сделать асинхронно?"

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

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

Ответ 5

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

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

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

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

Ответ 7

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

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

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

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

Ответ 8

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