Разница в производительности между общей памятью IPC и памятью потоков

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

Но у меня есть сомнения:

1) shmat() отображает локальную виртуальную память процесса в общий сегмент. Этот перевод должен выполняться для каждого адреса разделяемой памяти и может представлять значительную стоимость. В многопоточном приложении нет необходимости в дополнительном переводе: все адреса виртуальной машины преобразуются в физические адреса, как в обычном процессе, который не имеет доступа к общей памяти.

2) Сегмент разделяемой памяти должен каким-то образом поддерживаться ядром. Например, когда все процессы, подключенные к shm, сняты, сегмент shm все еще работает и может быть в конечном итоге повторно доступен вновь запущенными процессами. Могут быть некоторые накладные расходы, связанные с операциями ядра в сегменте shm.

Является ли многопользовательская система общей памяти так же быстро, как многопоточное приложение?

Ответ 1

1) shmat() отображает локальную виртуальную память процесса в общий сегмент. Этот перевод должен выполняться для каждого адреса разделяемой памяти и может представлять значительную стоимость по сравнению с количеством запросов shm. В многопоточном приложении нет необходимости в дополнительном переводе: все адреса виртуальных машин преобразуются в физические адреса, как в обычном процессе, который не имеет доступа к общей памяти.

Нет никаких накладных расходов по сравнению с обычным доступом к памяти, кроме первоначальной стоимости для настройки общих страниц - заполнение таблицы страниц в процессе, который вызывает shmat() - в большинстве вариантов Linux, что 1 страница (4 или 8 байт) на 4 КБ общей памяти.

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

2) Сегмент разделяемой памяти должен каким-то образом поддерживаться ядром. Я не знаю, что это означает "как-то" с точки зрения производительности, но, например, когда все процессы, связанные с shm, сбрасываются, сегмент shm все еще работает и может быть в конечном итоге повторно доступен вновь запущенными процессами. Должна быть, по крайней мере, некоторая степень накладных расходов, связанных с вещами, которые ядро необходимо проверять в течение всего срока службы сегмента shm.

Независимо от того, является ли общий доступ или нет, на каждой странице памяти есть прикрепленная к нему "структурная страница" с некоторыми данными о странице. Один из пунктов - подсчет ссылок. Когда страница выдается процессу [независимо от того, идет ли это через "shmat" или какой-либо другой механизм], счетчик ссылок увеличивается. Когда он освобождается каким-либо способом, счетчик ссылок уменьшается. Если декрементированный счет равен нулю, страница фактически освобождается - иначе "с ней больше ничего не происходит".

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

То же самое происходит, когда создается "вилка". Когда процесс разветвляется, вся таблица страниц родительского процесса по существу копируется в дочерний процесс, и все страницы становятся доступными только для чтения. Всякий раз, когда происходит запись, происходит ошибка с ядром, что приводит к копированию этой страницы - поэтому теперь есть две копии этой страницы, и процесс, выполняющий запись, может изменить страницу, не затрагивая другой процесс. После того, как умирает дочерний (или родительский) процесс, конечно, все страницы, все еще принадлежащие BOTH-процессам [например, кодовое пространство, которое никогда не записывается, и, вероятно, куча общих данных, которые никогда не были затронуты и т.д.), Очевидно, не могут быть освобождаются до тех пор, пока ОБА процессы "мертвы". Итак, снова ссылки на подсчитанные страницы полезны здесь, так как мы только отсчитываем счетчик ссылок на каждой странице и когда счетчик ссылок равен нулю, то есть когда все процессы, использующие эту страницу, освободили его, страница фактически вернулся как "полезная страница".

То же самое происходит с общими библиотеками. Если один процесс использует общую библиотеку, он будет освобожден, когда этот процесс завершится. Но если два, три или 100 процессов используют одну и ту же разделяемую библиотеку, код, очевидно, должен оставаться в памяти, пока страница больше не понадобится.

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

Ответ 2

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

Ответ 3

Помимо затрат на установку ( shmat) и отсоединение (shmdt) общей памяти, доступ должен быть одинаково быстрым. Другими словами, он должен быть быстрым, поскольку аппаратное обеспечение поддерживает его. Не должно быть накладных расходов в виде дополнительного слоя для каждого доступа.

Синхронизация также должна быть одинаковой. Например, в Linux futex может использоваться как для процессов, так и для потоков. Атомная переменная также должна работать нормально.

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

Наконец, эта дискуссия может быть интересной: стоят ли shmat и shmdt? , (Предостережение: оно довольно устарело. Я не знаю, изменилась ли ситуация с тех пор.)

Этот связанный с этим вопрос также может быть полезен: в чем разница между общей памятью для IPC и общей памятью потоков? (Короткий ответ: не так много.)

Ответ 4

Стоимость общей памяти пропорциональна количеству изменений "мета": распределение, освобождение, выход из процесса,...

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

ЦП выполняет отображение таблицы страниц. Физически процессор не знает, что сопоставление является общим.

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

Ответ 5

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

Интересна то, как архитектура процессора позволяет нескольким ядрам (таким образом, потоки и процессы) обращаться к одной и той же памяти. Это делается через кеши L1, затем L2, L3 и, наконец, DRAM. Там должно быть много координации между контроллерами всего этого.

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

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

Копирование может быть 1) memcpy, 2) запись в трубку, 3) внутренняя передача DMA (в наши дни чипы Intel могут сделать это).

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

Штраф - больше оперативной памяти, но окупаемость заключается в том, что в игре играют такие вещи, как программирование модели Actor. Это способ удалить всю сложность защиты разделяемой памяти с помощью семафоров из вашей программы.