Какие операции заставляют параллельный код работать медленно? - PullRequest
3 голосов
/ 08 декабря 2010

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

Ответы [ 5 ]

5 голосов
/ 08 декабря 2010

Не весь код по своей природе может быть выполнен для параллельной работы.

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

((((x+y)+z)+a)*b)

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

Рассмотрим суммирование, мне нужно сложить 100 000 чисел.

sum = 0;
for (i = 0; i < 100000; i++) {
   sum += numbers[i];
}

Добавление является коммутативным и транзитивным, a + b + c + d можно разбить на (a + b + c) + d, a + (b + c) + d, (a + b) + (c + d) и т. Д. Последний случай - это равномерное распределение работы: половина в одной скобке, половина в другой.

Итак, предположим, что мы выполнили большую задачу, подобную этой:

sumA = 0;
for (i = 0; i < 100000 / 2; i++) {
   sumA += numbers[i];
}

sumB = 0;
for (i = 100000 / 2; i < 100000; i++) {
   sumB += numbers[i];
}

sum = sumA + sumB;

Разделив пополам, два цикла могут работать одновременно и все же получить один и тот же ответ, мы просто должны рекомбинировать в конце.

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

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

Итак, в ответ на ваш вопрос. Это не так много, что вы можете сделать, чтобы сделать вашу программу параллельной, как если бы ваша программа могла быть естественной.

2 голосов
/ 08 декабря 2010

Имейте в виду, что, как правило, сериализованная часть приложения зависит от размера проблемы. Также, как правило, сериализованная часть либо фиксируется, либо медленно растет с размером проблемы, поэтому в целом при увеличении размера проблемы процентное влияние последовательной части уменьшается. См. Также закон Густафсона .

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

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

2 голосов
/ 08 декабря 2010

Как много отвечает понятиям в информатике; Это зависит.

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

В основном: http://www.erlang.org/doc/efficiency_guide/processes.html

Чтобы повысить производительность с помощью эмулятора SMP, ваше приложение большую часть времени должно иметь более одного работающего процесса Erlang. В противном случае эмулятор Erlang все еще может запускать только один процесс Erlang за раз, но вы все равно должны заплатить за блокировку. Хотя мы стараемся максимально снизить накладные расходы на блокировку, они никогда не станут точно равными нулю.

Другие свойства вашего приложения также должны быть приняты во внимание. Одним из основных преимуществ Erlang / OTP является изоляция между процессами. Например, веб-сервер или телекоммуникационный узел могут быть параллельными для каждого входящего запроса / вызова посредством процессов порождения. Но другим важным преимуществом является изоляция процесса (входящего запроса), контроль и отказоустойчивое поведение, которое «приходит бесплатно». Erlang / OTP был создан, чтобы хорошо работать в этих ситуациях.

С другой стороны, приложение с только последовательными операциями (т.е. списки: foldl / 3) может не принести никакой пользы от процессов порождения. Вместо этого он может потерять производительность из-за издержек процесса нереста.

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

После некоторых разговоров я попытаюсь ответить на ваш вопрос.

  • Распараллеливание достигается путем создания более одного процесса, который может выполняться одновременно с другим процессом.
  • При порождении процессов приходится платить, даже если это дешево.
  • Последовательные операции могут не приносить никакой пользы, вызывая процессы для каждой операции, наоборот, это может ухудшать производительность.
  • Другие важные преимущества могут быть получены при порождении процессов в Erlang / OTP. Убедитесь, что они работают вместе с вашим приложением.
  • Остерегайтесь "проблемы масштабирования" при доступе к ресурсам того же типа из параллельных процессов.
  • Тестирование как с параллельным выполнением, так и без него, порождая или не порождая процессы для ваших операций.
  • Проверьте еще немного.
2 голосов
/ 08 декабря 2010

Использование чисто функционального языка (где нет побочных эффектов) - хорошее начало (примеры: Haskell, Lisp и т. Д.)

Если вы находитесь в более OO-среде, по возможности пишите неизменяемые классы (в Java: используйте конечные поля, и эти поля должны иметь неизменяемые типы), и инкапсулируйте изменяемые поля, чтобы вам не приходилось беспокоиться о том, что они могут быть изменены кодом, запущенным в другом потоке.

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

Удачи!

0 голосов
/ 10 декабря 2010

как я могу гарантировать, что я пишу код, максимально параллельный с самого начала?

В общем, проблема связана с зависимостью данных.Операция зависит от другой операции, если ей нужны данные, чтобы начать выполнять себя.Здесь есть несколько важных трюков.Предположим, вы знаете, что операция может иметь только 2 возможных результата.Затем вы можете начать оба вычисления, а затем «выбрать» правильное.Фактически, дизайн ЦП часто использует эту идею в вычислительных единицах (см., Например, статью в Википедии о Carry Select Adders ).Еще один трюк, когда вы знаете, что результат будет определенным значением, скажем, с вероятностью 99%.Затем вы можете умело выполнять следующую операцию и надеяться, что ваш прогноз окажется верным.Если это не удается, вы всегда можете откатить вычисления и повторить их.Это также делается в современных процессорах, см., Например, прогнозирование ветвлений со спекулятивным выполнением и буферизацией переупорядочения.

На этом этапе вам должно быть ясно, что современные архитектуры ЦП уже используют огромное количество параллелизациифокусы .Проблема в том, что они вытеснили все локальные распараллеливания, которые мы можем надеяться получить, и теперь нам нужно внедрить идеи «на один уровень» в наши программы.

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

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

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

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

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

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

Хотя это не волшебная серебряная пуля.Если вы сериализуете все свои сообщения через один процесс как «дроссель», тогда ваш код не будет параллельным, в игру вступят Амдаль или Густафссон, и вы останетесь с последовательной программой.

...