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

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

Ответ 1

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

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

Ответ 2

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

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

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

См. также раздел Платформы с задержкой ответа в этом ответе для получения важных сведений о влиянии задержки памяти /L3 на однопоточную полосу пропускания: bandwidth <= max_concurrency / latency, и это фактически является основным узким местом для пропускная способность на современных многоядерных процессорах, таких как 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++ 11 std::atomic или C11 stdatomic вместо унаследованных gcc __sync встроенных или более новых __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

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

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