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

Обновлено: 07.07.2024

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

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ПРОГРАММИСТ О ПАМЯТИ

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

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ КОМПЬЮТЕРЩИК ОБ АРИФМЕТИКЕ С ПЛАВАЮЩЕЙ ТОЧКОЙ

Арифметика с плавающей точкой - сложная тема, и ее нелегко освоить. Даже многие программисты Java не знают, что может пойти не так при сравнении значения float / double с оператором==. Эта статья является еще одной жемчужиной этой серии и обязательна для чтения для всех разработчиков программного обеспечения и программистов. По мере того, как ваш опыт растет, вы, как ожидается, знаете тонкие детали общих вещей, и арифметика с плавающей точкой является одним из них. Как старший Разработчик Java, вы должны знать, как выполнить денежный расчет, когда использовать float, double или Big Decimal классы, как округлить числа с плавающей запятой и т.д. Даже если вы знаете основы арифметики с плавающей точкой, вы узнаете что-то новое о вычислении с плавающей точкой, прочитав статью.

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ РАЗРАБОТЧИК О UNICODE

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ПРОГРАММИСТ О ВРЕМЕНИ

Помимо кодирования символов, время и дата - это еще одна область, где многие программисты борются. Даже старшие разработчики потерялись между GMT, UTC, переходом на летнее время и между високосными секундами. Есть так много вещей, которые могут пойти не так, и есть равное количество неправильных представлений. Такие вещи, как, содержит ли дата часовой пояс или нет, могут смутить вас, а преобразование времени UNIX в другой часовой пояс может вас напугать, заставит забыть о синхронизации часов и задержек. Я надеюсь, что многие из вашего заблуждения о времени уйдет, и вы будете строить фундаментальный отсчёт времени, читая эту классическую статью.

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ВЕБ-РАЗРАБОТЧИК О КОДИРОВКЕ URL

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ПРОГРАММИСТ О ВЕБ-РАЗРАБОТКЕ?

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ПРОГРАММИСТ О SEO

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

Язык программирования C имеет понятие "неопределенное поведение". Неопределенное поведение - это широкая тема с множеством нюансов. Многие, казалось бы, разумные вещи в C на самом деле имеют неопределенное поведение, и это общий источник ошибок в программах. Кроме того, любое неопределенное поведение в C дает лицензию на реализацию (компилятор и среда выполнения) для создания кода, который форматирует жесткий диск, делает совершенно неожиданные вещи или еще хуже. Прочитайте эту отличную статью, чтобы глубоко погрузиться в море неопределенного поведения.

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ПРОГРАММИСТ О СЕТИ

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

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ РАЗРАБОТЧИК JAVA О STRING

Каждый программист Java должен знать об этом. Строка очень важна в повседневном программировании на Java, поэтому хорошие знания необходимы для любого Java-разработчика. В этой статье затрагиваются многие важные области String, включая строковый пул, строковый литерал, сравнение String using = = vs equals (), преобразование байтов в String, почему String является неизменяемым, правильно объединяя строки и многое другое. Продвинутый программист может уже знать все эти вещи, но даже тогда это хорошо, чтобы пересмотреть их.

ЧТО ДОЛЖЕН ЗНАТЬ КАЖДЫЙ ПРОГРАММИСТ О БЕЗОПАСНОСТИ?

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

КАЖДЫЙ ПРОГРАММИСТ ДОЛЖЕН ЗНАТЬ НОМЕРА ЛАТЕНТНОСТИ

Друзья! Приглашаем вас к обсуждению. Если у вас есть своё мнение, напишите нам в комментарии.

Управление памятью – одна из главных задач ОС. Она критична как для программирования, так и для системного администрирования. Я постараюсь объяснить, как ОС работает с памятью. Концепции будут общего характера, а примеры я возьму из Linux и Windows на 32-bit x86. Сначала я опишу, как программы располагаются в памяти.

Каждый процесс в многозадачной ОС работает в своей «песочнице» в памяти. Это виртуальное адресное пространство, которое в 32-битном режиме представляет собою 4Гб блок адресов. Эти виртуальные адреса ставятся в соответствие (mapping) физической памяти таблицами страниц, которые поддерживает ядро ОС. У каждого процесса есть свой набор таблиц. Но если мы начинаем использовать виртуальную адресацию, приходится использовать её для всех программ, работающих на компьютере – включая и само ядро. Поэтому часть пространства виртуальных адресов необходимо резервировать под ядро.

image

image

Это не значит, что ядро использует так много физической памяти – просто у него в распоряжении находится часть адресного пространства, которое можно поставить в соответствие необходимому количеству физической памяти. Пространство памяти для ядра отмечено в таблицах страниц как эксклюзивно используемое привилегированным кодом, поэтому если какая-то программа пытается получить в него доступ, случается page fault. В Linux пространство памяти для ядра присутствует постоянно, и ставит в соответствие одну и ту же часть физической памяти у всех процессов. Код ядра и данные всегда имеют адреса, и готовы обрабатывать прерывания и системные вызовы в любой момент. Для пользовательских программ, напротив, соответствие виртуальных адресов реальной памяти меняется, когда происходит переключение процессов:

Голубым отмечены виртуальные адреса, соответствующие физической памяти. Белым – пространство, которому не назначены адреса. В нашем примере Firefox использует гораздо больше места в виртуальной памяти из-за своей легендарной прожорливости. Полоски в адресном пространстве соответствуют сегментам памяти таким, как куча, стек и проч. Эти сегменты – всего лишь интервалы адресов памяти, и не имеют ничего общего с сегментами от Intel. Вот стандартная схема сегментов у процесса под Linux:

image

Когда программирование было белым и пушистым, начальные виртуальные адреса сегментов были одинаковыми для всех процессов. Это позволяло легко удалённо эксплуатировать уязвимости в безопасности. Зловредной программе часто необходимо обращаться к памяти по абсолютным адресам – адресу стека, адресу библиотечной функции, и т.п. Удаленные атаки приходилось делать вслепую, рассчитывая на то, что все адресные пространства остаются на постоянных адресах. В связи с этим получила популярность система выбора случайных адресов. Linux делает случайными стек, сегмент отображения в память и кучу, добавляя смещения к их начальным адресам. К сожалению, в 32-битном адресном пространстве особо не развернёшься, и для назначения случайных адресов остаётся мало места, что делает эту систему не слишком эффективной.

Самый верхний сегмент в адресном пространстве процесса – это стек, в большинстве языков хранящий локальные переменные и аргументы функций. Вызов метода или функции добавляет новый кадр стека (stack frame) к существующему стеку. После возврата из функции кадр уничтожается. Эта простая схема приводит к тому, что для отслеживания содержимого стека не требуется никакой сложной структуры – достаточно всего лишь указателя на начало стека. Добавление и удаление данных становится простым и однозначным процессом. Постоянное повторное использование районов памяти для стека приводит к кэшированию этих частей в CPU, что добавляет скорости. Каждый поток выполнения (thread) в процессе получает свой собственный стек.

Можно прийти к такой ситуации, в которой память, отведённая под стек, заканчивается. Это приводит к ошибке page fault, которая в Linux обрабатывается функцией expand_stack(), которая, в свою очередь, вызывает acct_stack_growth(), чтобы проверить, можно ли ещё нарастить стек. Если его размер не превышает RLIMIT_STACK (обычно это 8 Мб), то стек увеличивается и программа продолжает исполнение, как ни в чём не бывало. Но если максимальный размер стека достигнут, мы получаем переполнение стека (stack overflow) и программе приходит ошибка Segmentation Fault (ошибка сегментации). При этом стек умеет только увеличиваться – подобно государственному бюджету, он не уменьшается обратно.

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

После стека идёт сегмент отображения в память. Тут ядро размещает содержимое файлов напрямую в памяти. Любое приложение может запросить сделать это через системный вызов mmap() в Linux или CreateFileMapping() / MapViewOfFile() в Windows. Это удобный и быстрый способ организации операций ввода и вывода в файлы, поэтому он используется для подгрузки динамических библиотек. Также возможно создать анонимное место в памяти, не связанное с файлами, которое будет использоваться для данных программы. Если вы сделаете в Linux запрос на большой объём памяти через malloc(), библиотека C создаст такую анонимное отображение вместо использования памяти из кучи. Под «большим» подразумевается объём больший, чем MMAP_THRESHOLD (128 kB по умолчанию, он настраивается через mallopt().)

Если в куче оказывается недостаточно места для выполнения запроса, эту проблему может обработать сама программа без вмешательства ядра. В ином случае куча увеличивается системным вызовом brk(). Управление кучей – дело сложное, оно требует хитроумных алгоритмов, которые стремятся работать быстро и эффективно, чтобы угодить хаотичному методу размещению данных, которым пользуется программа. Время на обработку запроса к куче может варьироваться в широких пределах. В системах реального времени есть специальные инструменты для работы с ней. Кучи тоже бывают фрагментированными:

image

И вот мы добрались до самой нижней части схемы – BSS, данные и текст программы. BSS и данные хранят статичные (глобальные) переменные в С. Разница в том, что BSS хранит содержимое непроинициализированных статичных переменных, чьи значения не были заданы программистом. Кроме этого, область BSS анонимна, она не соответствует никакому файлу. Если вы пишете static int cntActiveUsers , то содержимое cntActiveUsers живёт в BSS.

Сегмент данных, наоборот, содержит те переменные, которые были проинициализированы в коде. Эта часть памяти соответствует бинарному образу программы, содержащему начальные статические значения, заданные в коде. Если вы пишете static int cntWorkerBees = 10 , то содержимое cntWorkerBees живёт в сегменте данных, и начинает свою жизнь как 10. Но, хотя сегмент данных соответствует файлу программы, это приватное отображение в память (private memory mapping) – а это значит, что обновления памяти не отражаются в соответствующем файле. Иначе изменения значения переменных отражались бы в файле, хранящемся на диске.

Пример данных на диаграмме будет немного сложнее, поскольку он использует указатель. В этом случае содержимое указателя, 4-байтный адрес памяти, живёт в сегменте данных. А строка, на которую он показывает, живёт в сегменте текста, который предназначен только для чтения. Там хранится весь код и разные другие детали, включая строковые литералы. Также он хранит ваш бинарник в памяти. Попытки записи в этот сегмент оканчиваются ошибкой Segmentation Fault. Это предотвращает ошибки, связанные с указателями (хотя не так эффективно, как если бы вы вообще не использовали язык С). На диаграмме показаны эти сегменты и примеры переменных:

image

Изучить области памяти Linux-процесса можно, прочитав файл /proc/pid_of_process/maps. Учтите, что один сегмент может содержать много областей. К примеру, у каждого файла, сдублированного в память, есть своя область в сегменте mmap, а у динамических библиотек – дополнительные области, напоминающие BSS и данные. Кстати, иногда, когда люди говорят «сегмент данных», они имеют в виду данные + bss + кучу.

Бинарные образы можно изучать при помощи команд nm и objdump – вы увидите символы, их адреса, сегменты, и т.п. Схема виртуальных адресов, описанная в этой статье – это т.н. «гибкая» схема, которая по умолчанию используется уже несколько лет. Она подразумевает, что переменной RLIMIT_STACK присвоено какое-то значение. В противном случае Linux использует «классическую» схему:

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

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

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

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

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

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

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

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

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

Информация о низкоуровневом оборудовании о том, как работают DRAM и кэши, все еще применима . DDR4 использует те же команды , как описано для DDR1/DDR2 (пакетное чтение/запись). Улучшения DDR3/4 не являются фундаментальными изменениями. AFAIK, все независимые от Arch вещи по-прежнему применяются в целом, например, до 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 из старого кода , и только подумайте о том, чтобы вернуть его обратно, если профилирование показывает, что кеш отсутствует не насыщает пропускную способность памяти). Предварительная выборка обеих сторон шага next ​​двоичного поиска все еще может помочь. например Как только вы решите, на какой элемент смотреть дальше, предварительно выберите элементы 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, но все равно штраф за пропускную способность).

Как предсказывал Ульрих, в наши дни каждая multi-socket ​​система является 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 legacy __sync встроенные модули или более новые __atomic встроенные модули если вы не хотите смешивать атомарный и неатомарный доступ в одном месте .

8.1 DWCAS ( cmpxchg16b ) : вы можете уговорить gcc выдать его, но если вы хотите эффективную загрузку только одной половины объекта, вам нужен некрасивый union hacks: Как я могу реализовать счетчик 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 , чтобы заставить ядро ​​дефрагментировать физическую память при необходимости, вместо того, чтобы возвращаться к 4k страницам. (Смотрите документы по ядру ). В качестве альтернативы используйте madvise(MADV_HUGEPAGE) после выполнения больших выделений (желательно с выравниванием 2 МБ).

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

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

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

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

ОТВЕТЫ

Ответ 1

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

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

Ответ 2

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

Ответ 3

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

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

Читайте также: