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

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

3 ответа

Решение

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

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

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

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

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

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

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


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

Одна важная вещь, которая изменилась, заключается в том, что аппаратная предварительная выборка намного лучше, чем на P4, и может распознавать пошаговые шаблоны доступа вплоть до довольно большого шага и нескольких потоков одновременно (например, один вперед / назад на страницу 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) намного сложнее и должны либо выполнять реальный поток, либо не использовать гиперпоточность (оставьте другое логическое ядро ​​бездействующим, чтобы у сольного потока были все ресурсы).

Программная предварительная выборка всегда была "хрупкой": правильные магические числа настройки для ускорения зависят от деталей аппаратного обеспечения и, возможно, загрузки системы. Слишком рано, и оно выселено до загрузки спроса. Слишком поздно, и это не помогает. В этой статье блога показаны коды + графики для интересного эксперимента по использованию предварительной выборки 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::atomicfetch_add скомпилируется в lock add (или же lock xadd если возвращаемое значение используется), но алгоритм, использующий CAS, чтобы сделать что-то, что не может быть сделано с lock Инструкция обычно не является катастрофой. Используйте C++11 std::atomic или C11 stdatomic вместо GCC Legacy __sync встроенные модули или новее __atomic встроенные модули, если вы не хотите смешивать атомарный и неатомарный доступ в одном месте...

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

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 чтобы ядро ​​дефрагментировало физическую память всякий раз, когда нужно, вместо того, чтобы возвращаться к 4k страницам. (См. Документацию по ядру). В качестве альтернативы используйте 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 MOV действительно быть "свободным"? Почему я не могу воспроизвести это вообще?,

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

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

Другие вопросы по тегам