Оптимизация WKWYL в make_shared <>() вводит штраф за некоторые многопоточные приложения?

Несколько дней назад мне довелось посмотреть эту очень интересную презентацию от Stephan T. Lavavej, в которой упоминается оптимизация "Мы знаем, где вы живете" (извините за использование акроним в заголовке вопроса, SO предупредил меня, что вопрос может быть закрыт в противном случае), и этот красивый от Herb Sutter по машинной архитектуре.

Вкратце, оптимизация "Мы знаем, где вы живете" состоит в том, чтобы разместить контрольные счетчики в том же блоке памяти, что и объект, который make_shared создает, что приводит к одному распределению памяти, а не к двум, и делает shared_ptr более компактный.

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

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

Предположим, что один для потоков требуется обновить счетчик ссылок несколько раз (например, при копировании shared_ptr), а остальные просто нужны для доступа к заостренному объекту: разве это не замедлит выполнение потоков all, заставив их конкурировать за одну и ту же строку кэша?

Если пересчет жил где-то в памяти, я бы сказал, что утверждение будет менее вероятным..

Является ли это хорошим аргументом против использования make_shared() в подобных случаях (если, конечно, он реализует WKWYL-оптимизацию)? Или есть ошибочность в моих рассуждениях?

3 ответа

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

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

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

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

Иногда преимущество make_shared не связано с линиями и локалями кэша, просто он делает одно динамическое распределение вместо двух. Значение этого зависит от того, сколько объектов вы выделяете и освобождаете: это может быть незначительным; это может быть разница между вашим приложением, установленным в ОЗУ, и заменой, как сумасшедший; в некоторых случаях может потребоваться, чтобы ваше приложение выполнило все необходимые ему распределения.

FYI, есть другая ситуация, возможно, не использовать make_shared, и что, когда объект не мал, и у вас есть слабые указатели, которые значительно ожидают shared_ptr. Причина в том, что блок управления не освобождается до тех пор, пока слабые указатели не исчезнут, и, следовательно, если вы использовали make_shared, тогда вся память, занятая объектом, не освобождается до тех пор, пока слабые указатели не исчезнут. Объект будет разрушен, как только общие указатели будут, конечно, так важны только размер класса, а не связанные ресурсы.


Обратите внимание, что выделение количества ссылок не напрямую связано с оптимизацией WKWYL - это основной предполагаемый эффект std::make_shared. У вас есть полный контроль: используйте make_shared<t>()</t>, чтобы сохранить выделение и поместите счетчик ссылок с объектом, или используйте shared_ptr<t>( new T() )</t>, чтобы сохранить его отдельно.

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

Однако, как я вижу это, есть два фактора, почему это не учитывается в решении для этой оптимизации:

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


Предположим, что один для потоков требуется обновить счетчик ссылок несколько раз (например, при копировании shared_ptr вокруг), а другие просто нужны для доступа к заостренному объекту: это не замедлит выполнение потоков all, заставив их конкурировать за одну и ту же строку кэша?

Да, но это реалистичный сценарий?

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

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

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

licensed under cc by-sa 3.0 with attribution.