Что каждый программист должен знать о памяти?


165

Мне интересно, сколько из Ульриха Дреппера, что каждый программист должен знать о памяти с 2007 года, все еще в силе. Также я не смог найти более новую версию, чем 1.0 или опечатки.


1
кто-нибудь знает, могу ли я скачать эту статью в формате mobi где-нибудь, чтобы я мог легко прочитать ее на kindle? "pdf" очень трудно читать из-за проблем с масштабированием / форматированием
javapowered

1
Это не mobi, но LWN опубликовал статью как набор статей, которые легче читать на телефоне / планшете. Первое на lwn.net/Articles/250967
Натан

Ответы:


111

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

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


3
Да, я действительно не понимаю, почему программист должен знать, как работают SRAM и DRAM на аналоговом уровне - это мало поможет при написании программ. И людям, которые действительно нуждаются в этих знаниях, лучше потратить время на чтение руководств о деталях фактического времени и т. Д. Но для людей, заинтересованных в материалах низкого уровня HW? Может быть, не полезно, но, по крайней мере, интересно.
Во

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

144

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

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

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

Смотрите также в Связанном Platforms раздел латентности этого ответа на важные детали о влиянии памяти / L3 задержки на однопоточной пропускной способности: bandwidth <= max_concurrency / latencyи это на самом деле основное препятствие для однопоточной пропускной способности на современных многоядерные CPU , как Xeon , Но четырехъядерный настольный ПК Skylake может приблизиться к увеличению пропускной способности DRAM с помощью одного потока. Эта ссылка содержит очень хорошую информацию о магазинах NT и обычных магазинах на x86. Почему Skylake намного лучше, чем Broadwell-E для однопоточной пропускной способности памяти? это резюме.

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


(обычно) Не ​​используйте программную предварительную выборку

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

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

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

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


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

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


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

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

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


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

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

8.1 DWCAS ( cmpxchg16b) : вы можете уговорить gcc на его излучение, но если вы хотите эффективную загрузку только одной половины объекта, вам нужны ужасные unionхаки: как я могу реализовать счетчик ABA с c ++ 11 CAS? , (Не путайте DWCAS с DCAS из 2 отдельных областей памяти . Атомная эмуляция DCAS без блокировки невозможна с DWCAS, но транзакционная память (например, x86 TSX) делает это возможным.)

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


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

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


Приложение 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 MOV действительно быть «свободным»? Почему я не могу воспроизвести это вообще? ,


3
Очень поучительный ответ и указатели! Это явно заслуживает большего количества голосов!
CLAF

@Peter Cordes Есть ли другие руководства / документы, которые вы бы порекомендовали прочитать? Я не высокопроизводительный программист, но я хотел бы узнать больше об этом и, надеюсь, выбрать методы, которые я могу включить в свое ежедневное программирование.
user3927312

4
@ user3927312: agner.org/optimize - это одно из лучших и наиболее последовательных руководств по низкоуровневым материалам, особенно для x86, но некоторые общие идеи применимы и к другим ISA. Помимо руководства asm, у Агнера есть оптимизирующий C ++ PDF. Другие ссылки на производительность / архитектуру ЦП см. На stackoverflow.com/tags/x86/info . Я также написал кое-что об оптимизации C ++, помогая компилятору улучшить asm для критических циклов, когда стоит взглянуть на вывод asm компилятора: код C ++ для тестирования гипотезы Коллатца быстрее, чем рукописный asm?
Питер Кордес

74

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

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


5
Я бы хотел принять вас обоих. Но я проголосовал за ваш пост.
Framester

5
Возможно, самое важное изменение, которое имеет отношение к разработчикам ПО, заключается в том, что предварительная выборка потоков - плохая идея. Процессоры достаточно мощные, чтобы запускать 2 полных потока с гиперпоточностью, и имеют намного лучшую предварительную выборку HW. Предварительная выборка SW в целом намного менее важна, особенно для последовательного доступа. Смотри мой ответ.
Питер Кордес
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.