Что Каждый Программист Должен Знать О Памяти?

3 ответов


насколько я помню, содержимое Drepper описывает фундаментальные понятия о памяти: как работает кэш процессора, что такое физическая и виртуальная память и как ядро Linux работает с этим зоопарком. Возможно, в некоторых примерах есть устаревшие ссылки на API, но это не имеет значения; это не повлияет на актуальность фундаментальных концепций.

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


из моего быстрого взгляда-через это выглядит довольно точно. Единственное, что нужно заметить, это часть о разнице между" интегрированными "и" внешними " контроллерами памяти. С момента выпуска линейки i7 процессоры Intel все интегрированы, и AMD использует интегрированные контроллеры памяти с момента первого выпуска чипов AMD64.

поскольку эта статья была написана, не много изменилось, скорости стали выше, контроллеры памяти намного больше интеллектуальный (i7 будет задерживать запись в ОЗУ, пока не почувствует, что совершает изменения), но не все изменилось. По крайней мере, не так, как разработчик программного обеспечения будет заботиться.


руководство в формате PDF находится на https://www.akkadia.org/drepper/cpumemory.pdf.

это все же очень обидно. (мной, и я думаю, другими экспертами по настройке производительности). Было бы здорово, если бы Ульрих (или кто-нибудь еще) написал обновление 2017, но это было бы много работы (например, повторный запуск тестов). См. также другие ссылки на оптимизацию производительности x86 и оптимизацию SSE/asm (и C/C++) в x86 тег wiki. (Статья Ульриха не специфична для x86, но большинство (все) его тестов находятся на оборудовании x86.)

детали оборудования низкого уровня о том, как работают DRAM и кэши, все еще применяются. Память DDR4 использует те же команды как описано для DDR1 / DDR2 (чтение / запись пакета). Улучшения DDR3/4 не являются фундаментальными изменениями. AFAIK, все Арко-независимые вещи по-прежнему применяются в целом, например, к AArch64 / ARM32.

посмотреть также the латентность связанных платформ раздел этого ответа для важных деталей о влиянии задержки memory/L3 на однопоточную полосу пропускания:bandwidth <= max_concurrency / latency, и это на самом деле основное узкое место для однопоточной полосы пропускания на современном многоядерном процессоре, таком как Xeon. (Но четырехъядерный рабочий стол Skylake может приблизиться к максимальной пропускной способности DRAM с помощью одного потока). Эта ссылка имеет очень хорошую информацию о магазинах NT и обычных магазинах на архитектуры x86.

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

(обычно) не используйте программное обеспечение prefetch

одна важная вещь, которая изменилась, это аппаратная предварительная выборка много лучше, чем на P4 и может распознавать шаблоны доступа strided до довольно большие шаг и несколько потоков одновременно (например, один вперед / назад на страницу 4k). руководство по оптимизации Intel описывает некоторые детали HW prefetchers в различных уровнях кэша для их микроархитектуры семейства Sandybridge. Ivybridge и более поздние версии имеют аппаратную предварительную выборку следующей страницы, вместо того, чтобы ждать промаха кэша на новой странице, чтобы вызвать быстрый запуск. (Я предполагаю, что AMD имеет некоторые подобные вещи в своем Руководстве по оптимизации.) Остерегайтесь, что руководство Intel также полно старых советы, некоторые из которых хороши только для P4. Разделы Sandybridge-specific, конечно, точны для SnB, но, например,ООН-слоение микро-сплавленного uops измененного в HSW и руководстве не упоминает его.

обычный совет в эти дни, чтобы удалить все SW prefetch из старого кода, и только подумайте о том, чтобы вернуть его, если профилирование показывает пропуски кэша (и вы не насыщаете пропускную способность памяти). Prefetching обе стороны далее шаг двоичного поиска все еще может помочь. например, как только вы решите, на какой элемент смотреть дальше, предварительно установите 1/4 и 3/4 элементов, чтобы они могли загружаться параллельно с загрузкой / проверкой середины.

предложение использовать отдельный поток предварительной выборки (6.3.4) полностью устарело, я думаю, и был хорош только на Pentium 4. P4 имел гиперпоточность (2 логических ядра, разделяющих одно физическое ядро), но недостаточно ресурсов выполнения вне порядка или trace-cache для получения пропускной способности, выполняющей два полных вычислительных потока на одном ядре. Но современные процессоры (Sandybridge-family и Ryzen) являются много beefier и должен либо запускать реальный поток, либо не использовать hyperthreading (оставьте другое логическое ядро бездействующим, чтобы соло-поток имел полные ресурсы.)

предвыборки Software всегда был "хрупким": правильные магические номера настройки для ускорения зависят от деталей оборудования и, возможно, системы нагрузка. Слишком рано, и он выселяется до нагрузки спроса. Слишком поздно и это не поможет. в этой статье показывает код + графики для интересного эксперимента по использованию SW prefetch на Haswell для предварительной выборки непоследовательной части проблемы. См. также Как правильно использовать инструкции prefetch?. NT prefetch интересен, но еще более хрупок (потому что раннее выселение из L1 означает, что вам нужно пройти весь путь до L3 или DRAM, а не только L2). Если вам нужно до последней капли производительности,и вы можете настроить для конкретной машины, SW prefetch стоит посмотреть для последовательного доступа, но если мая все еще будет замедление, если у вас достаточно работы ALU, чтобы сделать, приближаясь к узкому месту в памяти.


кэша размер строке еще 64 байта. (Пропускная способность чтения/записи L1D очень высокие и современные процессоры могут выполнять 2 векторные нагрузки на часы + 1 векторный магазин, если все это попадает в L1D. Смотри как кэш может быть так быстро?.) С AVX512, размер линии = ширина вектора, поэтому вы можете загрузить / сохранить всю строку кэша в одной инструкции. (И, таким образом, каждая несоосная загрузка/хранилище пересекает границу строки кэша, а не каждую другую для 256B AVX1/AVX2, что часто не замедляет цикл над массивом, который не был в L1D.)

инструкции по несогласованной загрузке имеют нулевое наказание, если адрес выровнен во время выполнения, но компиляторы (особенно gcc) делают лучший код, когда autovectorizing если они знают о каких-то гарантиях выравнивания. На самом деле unaligned ops, как правило, быстрые, но разбиения страниц все еще больно (гораздо меньше на Skylake, хотя; только ~11 дополнительных циклов задержки против 100, но все же штраф пропускной способности).


как и предсказывал Ульрих, каждый multi-socket система NUMA в эти дни: интегрированные контроллеры памяти стандартны, т. е. нет внешнего Northbridge. Но SMP больше не означает multi-socket, потому что многоядерные процессоры широко распространенный. (Процессоры Intel от Nehalem до Skylake использовали большой включительно кэш L3 в качестве поддержки для согласованности между ядрами.) Процессоры AMD отличаются, но я не так ясно в деталях.

Skylake-X (AVX512) больше не имеет инклюзивного L3, но я думаю, что все еще есть каталог тегов, который позволяет ему проверять, что кэшируется в любом месте на чипе (и если да, то где) без фактического вещания snoops на все ядра. SKX использует сетку, а не кольцо автобус!--24-->, С обычно еще худшей задержкой, чем предыдущие многоядерные Xeons, к сожалению.

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


6.4.2 Atomic ops: тест, показывающий цикл CAS-retry как 4X хуже, чем аппаратный арбитраж lock add вероятно, все еще отражает максимум раздор случае. Но в реальных многопоточных программах синхронизация сведена к минимуму (потому что это дорого), поэтому конкуренция низкая, и цикл CAS-retry обычно удается без повторной попытки.

C++11 std::atomic fetch_add будет составлять с lock add (или lock xadd если используется возвращаемое значение), но алгоритм, использующий CAS, чтобы сделать что-то, что нельзя сделать с lockинструкция ed обычно не является катастрофой. Использовать C++11 std::atomic или C11 stdatomic вместо GCC legacy __sync встроенные модули или новее __atomic встроенные модули если вы не хотите смешивать атомарный и неатомный доступ к тому же месту...

8.1 DCAS (cmpxchg16b): вы можете уговорить gcc излучать его, но если вы хотите эффективные нагрузки только одной половины объекта, вам нужно уродливо union хаки: как я могу реализовать счетчик ABA с c++11 КАС?

8.2.4 транзакций: после нескольких ложных запусков (выпущенных затем отключенных обновлением микрокода из-за редко запускаемой ошибки) Intel имеет рабочую транзакционную память в процессорах последней модели Broadwell и всех процессорах Skylake. Дизайн по-прежнему то, что Дэвид Кантер описал для Haswell. Существует способ lock-ellision использовать его для ускорения кода, который использует (и может вернуться) обычный замок (особенно с одним замком для всех элементы контейнера, поэтому несколько потоков в одном и том же критическом разделе часто не сталкиваются), или написать код, который знает о транзакциях напрямую.


7.5 Hugepages: анонимные прозрачные hugepages хорошо работают в Linux без необходимости вручную использовать hugetlbfs. Сделайте распределения >= 2MiB с выравниванием 2MiB (например,posix_memalign или aligned_alloc это не применяет глупое требование ISO C++17 к сбою, когда size % alignment != 0).

2mib-выровненное анонимное распределение будет использовать hugepages по умолчанию. Некоторые рабочие нагрузки (например, которые продолжают использовать большие распределения некоторое время после их создания) могут извлечь выгоду из
echo always >/sys/kernel/mm/transparent_hugepage/defrag чтобы получить ядра для дефрагментации физической памяти, когда это необходимо, вместо того, чтобы падать обратно на 4К страниц. (См.ядро docs). В качестве альтернативы, используйте madvise(MADV_HUGEPAGE) после делать большие распределения (предпочтительно все еще с 2MiB юстировка.)


Приложение B: Oprofile: Linux perf в основном вытеснил oprofile. Для подробных событий, характерных для определенных микроархитектур,использовать ocperf.py фантик. например,

ocperf.py stat -etask-clock,context-switches,cpu-migrations,page-faults,cycles,\
branches,branch-misses,instructions,uops_issued.any,\
uops_executed.thread,idq_uops_not_delivered.core -r2 ./a.out

некоторые примеры его использования см. В разделе может ли движение x86 действительно быть "свободным"? Почему я не могу воспроизвести это вообще?.