Многопоточность
В этом ответе я рассмотрю один важный случай использования различий между работой процессора и ввода-вывода: при написании многопоточного кода.
Пример, связанный с вводом / выводом в ОЗУ: Векторная сумма
Рассмотрим программу, которая суммирует все значения одного вектора:
#define SIZE 1000000000
unsigned int is[SIZE];
unsigned int sum = 0;
size_t i = 0;
for (i = 0; i < SIZE; i++)
/* Each one of those requires a RAM access! */
sum += is[i]
Распараллеливание того, что разделение массива по поровну для каждого из ваших ядер имеет ограниченную полезность для обычных современных настольных компьютеров.
Например, на моем Ubuntu 19.04 ноутбук Lenovo ThinkPad P51 с процессором: ЦП Intel Core i7-7820HQ (4 ядра / 8 потоков), ОЗУ: 2x Samsung M471A2K43BB1-CRC (2x 16 ГБ). Я получаю такие результаты:
График данных .
Обратите внимание, что между запусками существует большое расхождение. Но я не могу значительно увеличить размер массива, так как я уже на 8 ГБ, и у меня нет настроения для статистики по нескольким прогонам сегодня. Однако это выглядело как типичный прогон после многих ручных прогонов.
Код эталона:
Я не знаю достаточно компьютерной архитектуры, чтобы полностью объяснить форму кривой, но ясно одно: вычисления не становятся в 8 раз быстрее, чем наивно ожидалось из-за того, что я использовал все свои 8 потоков! По какой-то причине 2/3 потока было оптимальным, а добавление большего просто замедляет процесс.
Сравните это с работой с привязкой к процессору, которая на самом деле становится в 8 раз быстрее: Что означают 'real', 'user' и 'sys' в выводе времени (1)?
Причина, по которой все процессоры используют одну шину памяти, соединяющуюся с ОЗУ:
CPU 1 --\ Bus +-----+
CPU 2 ---\__________| RAM |
... ---/ +-----+
CPU N --/
так что шина памяти быстро становится узким местом, а не процессором.
Это происходит потому, что добавление двух чисел занимает один цикл ЦП, чтение памяти занимает около 100 циклов ЦП в аппаратном обеспечении 2016 года.
Таким образом, процессорная работа, выполняемая для байта входных данных, слишком мала, и мы называем это процессом, связанным с вводом-выводом.
Единственный способ ускорить дальнейшие вычисления - это ускорить индивидуальный доступ к памяти с помощью нового аппаратного обеспечения памяти, например, Многоканальная память .
Обновление, например, до более быстрой тактовой частоты процессора не очень полезно.
Другие примеры
матричное умножение ограничено ЦП для ОЗУ и графических процессоров. Вход содержит:
2 * N**2
номеров, но:
N ** 3
умножения сделаны, и этого достаточно, чтобы распараллеливание стоило того для большого практического N.
Вот почему существуют параллельные библиотеки умножения матриц ЦП, подобные следующим:
Использование кэша сильно влияет на скорость реализации. См. Например, пример сравнения дидактического графического процессора .
У графических процессоров есть узкое место при передаче данных в ЦП.
Они спроектированы таким образом, что выходные данные рендеринга (прямоугольник пикселей) могут быть выведены непосредственно в видеопамять, чтобы избежать циклического обращения ЦП.
Сеть является прототипом примера IO.
Даже когда мы отправляем один байт данных, для достижения цели все равно требуется много времени.
Распараллеливание небольших сетевых запросов, таких как HTTP-запросы, может значительно повысить производительность.
Если сеть уже работает на полную мощность (например, загружается торрент), распараллеливание может все еще увеличить увеличение задержки (например, вы можете загрузить веб-страницу «одновременно»).
фиктивная связанная с ЦП операция C ++, которая принимает одно число и много его хрустит:
Как узнать, связаны ли вы с процессором или IO
IO не в ОЗУ привязан как диск, сеть: ps aux
, затем theck, если CPU% / 100 < n threads
. Если да, вы связаны с IO, например, блоки read
просто ждут данных, и планировщик пропускает этот процесс. Затем используйте дополнительные инструменты, такие как sudo iotop
, чтобы решить, какой именно IO является проблемой.
Или, если выполнение быстрое и вы параметризовали количество потоков, вы можете легко увидеть из time
, что производительность увеличивается с увеличением количества потоков для работы с привязкой к ЦП: Что означает «реальный» 'user' и 'sys' означают в выводе времени (1)?
Ограничение RAM-IO: трудно сказать, поскольку время ожидания RAM включено в измерения CPU%
. Возможно, лучшее, что вы можете сделать, это оценить ошибки в кеше.
Смотри также:
CPython Global Intepreter Lock (GIL)
В качестве краткого примера я хочу указать на глобальную блокировку интерпретатора Python (GIL): Что такое глобальная блокировка интерпретатора (GIL) в CPython?
Эта деталь реализации CPython не позволяет нескольким потокам Python эффективно использовать работу, связанную с процессором. Документы CPython говорят:
Детали реализации CPython: В CPython из-за Глобальной блокировки интерпретатора только один поток может выполнить код Python одновременно (даже если некоторые ориентированные на производительность библиотеки могут преодолеть это ограничение). Если вы хотите, чтобы ваше приложение лучше использовало вычислительные ресурсы многоядерных машин, рекомендуется использовать multiprocessing
или concurrent.futures.ProcessPoolExecutor
. Тем не менее, многопоточность по-прежнему является подходящей моделью, если вы хотите запускать несколько задач, связанных с вводом / выводом одновременно.
Следовательно, здесь у нас есть пример, в котором содержимое с привязкой к ЦП не подходит, а ограничение ввода / вывода равно.