Выполнение очереди кражи работы в C/С++?

Я ищу правильную реализацию очереди кражи работы в C/CPP. Я просмотрел Google, но не нашел ничего полезного.

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

Ответ 1

Нет бесплатного обеда.

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

Мне действительно интересно, зачем вам это нужно. Я считаю, что правильная реализация означает нечто вроде TBB и Cilk. Опять же, кражу работы трудно реализовать.

Ответ 3

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

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

До сих пор это довольно общий материал с двумя основными исключениями: 1) контексты переключения (например, настройки регистров контекста процессора, такие как "стек" ) не могут быть указаны в чистом C или С++. Вы можете решить эту проблему, согласившись записать часть своего пакета в конкретный машинный код целевой платформы. 2) Атомный доступ к очередям для мультипроцессора не может быть выполнен исключительно на C или С++ (игнорируя алгоритм Деккера), поэтому вам нужно будет закодировать их с помощью примитивов синхронизации ассемблера, таких как X86 LOCK XCH или Compare and Swap. Теперь код, связанный с обновлением очереди, когда у вас есть безопасный доступ, не очень сложный, и вы можете легко написать это в нескольких строках C.

Однако, я думаю, вы обнаружите, что попытка кодирования такого пакета на C и С++ со смешанным ассемблером по-прежнему довольно неэффективна, и в конечном итоге вы все равно закодируете все на ассемблере. Все, что осталось, - это точки входа, совместимые с C/С++: -}

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

Настоящая муха в мазе для C и С++ заключается в том, когда вы создаете задачу, представляющую работу, сколько пространства стека вы назначаете? Серийные программы на C/С++ избегают этого вопроса, просто объединяя огромные количества (например, 10 МБ) одного линейного стека, и никто не заботится о том, сколько из этого пространства стека направлено впустую. Но если вы можете создать тысячи задач и заставить их всех жить в определенный момент, вы не можете разумно выделить 10 Мб каждому. Итак, теперь вам нужно либо определить статически, сколько пространства стека потребуется задаче (Turing-hard), либо вам потребуется выделить куски стека (например, для вызова функции), которые широко доступные компиляторы C/С++ не делают (например, тот, который вы, скорее всего, используете). Последний выход - это дросселировать создание задачи, чтобы ограничить ее до нескольких сотен в любой момент и мультиплексировать несколько сотен действительно огромных стеков среди задач, которые являются живыми. Вы не можете сделать последнее, если задачи могут блокировать/приостанавливать состояние, потому что вы столкнетесь с вашим порогом. Таким образом, вы можете сделать это только в том случае, если задачи выполняют только вычисления. Это кажется довольно серьезным ограничением.

Для PARLANSE мы создали компилятор, который выделяет записи активации в куче для каждого вызова функции.

Ответ 4

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

Проект Cilk

Премия HPC Challenge

Наша запись Cilk для HPC Challenge Награда класса 2 получила премию 2006 года за "Лучшая комбинация элегантности и Представление''. Премия была присуждена SC'06 в Тампе 14 ноября 2006 года.

Ответ 5

Если вы ищете автономную реализацию класса очереди выполнения в С++, построенную на pthread или boost:: thread, удачи, насколько мне известно, нет никого.

Однако, как утверждают другие, Cilk, TBB и Microsoft PPL имеют встроенные функции под капотом.

Вопрос заключается в том, хотите ли вы использовать очередь для работы или реализовать ее? Если вы просто хотите использовать один, то выбранные выше варианты являются хорошими отправными точками, достаточно просто планировать "задачу" в любом из них.

Поскольку BlueRaja говорит, что task_group и structured_task_group в PPL сделают это, также обратите внимание, что эти классы доступны и в последней версии Intel TBB. Параллельные петли (parallel_for, parallel_for_each) также реализуются с помощью workstealing.

Если вы должны смотреть на источник, а не на использование реализации, TBB - это OpenSource, и Microsoft отправляет источники для своего CRT, поэтому вы можете запускать spelunking.

Вы также можете посмотреть блог Joe Duffy для реализации С# (но это С# и модель памяти разные).

-Rick

Ответ 6

structured_task_group класс PPL использует очередь на кражу работы для ее реализации. Если вам нужна WSQ для потоковой передачи, я бы рекомендовал это.
Если вы действительно ищете источник, я не знаю, указан ли код в ppl.h или если есть предварительно скомпилированный объект; Я должен буду проверить, когда я вернусь домой сегодня вечером.

Ответ 7

OpenMP может очень хорошо поддерживать кражу работы, хотя ее называемый рекурсивный parallelism

Сообщение форума OpenMP

Спецификация OpenMP определяет конструкторы задач (которые могут быть вложенными, поэтому они очень подходят для рекурсивного parallelism), но не определяют детали того, как они реализуются. В реализациях OpenMP, в том числе gcc, обычно используется некоторая форма кражи работы для задач, хотя точный алгоритм (и результирующая производительность) может меняться!

См. #pragma omp task и #pragma omp taskwait

Обновление

Глава 9 книги С++ Concurrency в действии описывает, как реализовать "кражу работы для потоков пулов". Я не читал/не реализовал его сам, но это не выглядит слишком сложным.

Ответ 8

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

Ответ 9

Повреждение ваших рабочих задач на более мелкие блоки устраняет необходимость воровства в первую очередь?

Ответ 10

Ближайшая реализация этого алгоритма поиска работы, которую я нашел, называется Wool от Karl-Filip Faxén. src/report/comparison

Ответ 11

Я портировал этот проект C на С++.

Оригинальный Steal может испытывать грязное чтение при расширении массива. Я попытался исправить ошибку, но в итоге сдался, потому что мне не нужен динамически растущий стек. Вместо того, чтобы пытаться выделить пространство, метод Push просто возвращает false. Затем вызывающий абонент может выполнить ожидание, т.е. while(!stack->Push(value)){}.

#pragma once
#include <atomic>

  // A lock-free stack.
  // Push = single producer
  // Pop = single consumer (same thread as push)
  // Steal = multiple consumer

  // All methods, including Push, may fail. Re-issue the request
  // if that occurs (spinwait).

  template<class T, size_t capacity = 131072>
  class WorkStealingStack {

  public:
    inline WorkStealingStack() {
      _top = 1;
      _bottom = 1;
    }

    WorkStealingStack(const WorkStealingStack&) = delete;

    inline ~WorkStealingStack()
    {

    }

    // Single producer
    inline bool Push(const T& item) {
      auto oldtop = _top.load(std::memory_order_relaxed);
      auto oldbottom = _bottom.load(std::memory_order_relaxed);
      auto numtasks = oldbottom - oldtop;

      if (
        oldbottom > oldtop && // size_t is unsigned, validate the result is positive
        numtasks >= capacity - 1) {
        // The caller can decide what to do, they will probably spinwait.
        return false;
      }

      _values[oldbottom % capacity].store(item, std::memory_order_relaxed);
      _bottom.fetch_add(1, std::memory_order_release);
      return true;
    }

    // Single consumer
    inline bool Pop(T& result) {

      size_t oldtop, oldbottom, newtop, newbottom, ot;

      oldbottom = _bottom.fetch_sub(1, std::memory_order_release);
      ot = oldtop = _top.load(std::memory_order_acquire);
      newtop = oldtop + 1;
      newbottom = oldbottom - 1;

      // Bottom has wrapped around.
      if (oldbottom < oldtop) {
        _bottom.store(oldtop, std::memory_order_relaxed);
        return false;
      }

      // The queue is empty.
      if (oldbottom == oldtop) {
        _bottom.fetch_add(1, std::memory_order_release);
        return false;
      }

      // Make sure that we are not contending for the item.
      if (newbottom == oldtop) {
        auto ret = _values[newbottom % capacity].load(std::memory_order_relaxed);
        if (!_top.compare_exchange_strong(oldtop, newtop, std::memory_order_acquire)) {
          _bottom.fetch_add(1, std::memory_order_release);
          return false;
        }
        else {
          result = ret;
          _bottom.store(newtop, std::memory_order_release);
          return true;
        }
      }

      // It uncontended.
      result = _values[newbottom % capacity].load(std::memory_order_acquire);
      return true;
    }

    // Multiple consumer.
    inline bool Steal(T& result) {
      size_t oldtop, newtop, oldbottom;

      oldtop = _top.load(std::memory_order_acquire);
      oldbottom = _bottom.load(std::memory_order_relaxed);
      newtop = oldtop + 1;

      if (oldbottom <= oldtop)
        return false;

      // Make sure that we are not contending for the item.
      if (!_top.compare_exchange_strong(oldtop, newtop, std::memory_order_acquire)) {
        return false;
      }

      result = _values[oldtop % capacity].load(std::memory_order_relaxed);
      return true;
    }

  private:

    // Circular array
    std::atomic<T> _values[capacity];
    std::atomic<size_t> _top; // queue
    std::atomic<size_t> _bottom; // stack
  };

Full Gist (включая модульные тесты). Я только запускал тесты на сильной архитектуре (x86/64), насколько это возможно слабые архитектуры идут, ваш пробег может измениться, если вы попытаетесь использовать это, например Неон/PPC.

Ответ 12

dunno, если это вам поможет, но посмотрите эту статью о сети разработчиков AMD, ее простое, но должен дать вам что-то полезное