Цао Да говорит о перестройке памяти

Операционная система

Причина написания этой статьи очень проста: сообщество Golang внутри компании организовало первую сессию обмена, и приглашенным спикером был наш любимый Цао Да. Это нужно слушать, но лекции Цао Да очень хардкорные, поэтому я заранее попросил у него справочные материалы и потратил 1 час на предварительный просмотр, чтобы ничего не понять, когда это будет официально распространено. Конечно, это еще и уважение к себе и к говорящему. Все ссылки находятся в последней части статьи, вы можете изучить их самостоятельно.

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

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

Так что писать технические статьи непросто, я тоже размышляю о себе.

Что такое перераспределение памяти

Существует два типа: аппаратное и программное обеспечение, включая перестройку процессора и перестройку компилятора.

перестановка процессора

Цитирование ссылок【内存一致模型】Пример в:

2 thread

Выполните приведенный выше код в двух потоках одновременно, значения инициализации A и B равны 0, каков окончательный результат?

Начнем с нескольких очевидных результатов:

исполнительный лист выходной результат
1-2-3-4 01
3-4-1-2 01
1-3-2-4 11
1-3-4-2 11

Конечно, есть и некоторые симметричные случаи, когда один и тот же вывод указан в таблице выше. Например, выполнение последовательности 3-1-4-2 выведет 11.

От перестановки и комбинации 01 всего получается 4 вида: 00, 01, 10, 11. В таблице по-прежнему два типа: 10 и 00. Давайте сосредоточимся на анализе того, появятся ли эти два результата.

прежде всего10, предполагая, что (2) выводит 1, а (4) выводит 0. Затем сначала отсортируйте 2 и 3: (3) -> (2), так как вы должны сначала присвоить B 1, (2) может напечатать 1; аналогично, (4) -> (1). Кроме того, поскольку 1 печатается первым, (2) должно быть перед (4) и объединено: (3) -> (2) -> (4) -> (1). (2) на самом деле выполняется раньше (1), что невозможно!

Тогда давайте проанализируем00, чтобы напечатать 00, оператор печати должен быть выполнен до присвоения соответствующей переменной:

00

Стрелки на рисунке указывают последовательность. Это неудобно и образует кольцо. Если мы начнем сначала с (1), последовательность будет (1) -> (2) -> (3) -> (4) -> (1). (1) Выполнить 2 раза, как это возможно? так00Эта ситуация также невозможна.

Однако в реальном мире возможны две упомянутые выше ситуации. В конспектах лекций Цао Да есть методы проверки, и заинтересованные студенты должны испытать их на себе. Всего было проведено 100 миллионов тестов, и результаты тестов следующие:

test result

Это очень нелогично, но в многопоточном мире всевозможные странные проблемы — это только то, о чем вы не можете думать, и ни один компьютер не может этого сделать.

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

Среди них, чтобы повысить эффективность чтения и записи памяти, инструкции чтения и записи будут переставлены, что называется内存重排, на английскомMemory Reordering.

В этой части речь идет о перераспределении ЦП, на самом деле, есть еще перераспределение компилятора.

Перестановка компилятора

Взгляните на фрагмент кода:

X = 0
for i in range(100):
    X = 1
    print X

Результатом выполнения этого кода является печать 100 единиц. Умный компилятор проанализирует присваивание X внутри циклаX = 1Он избыточен, и каждый раз необходимо присваивать ему 1, что совершенно не нужно. Поэтому код будет оптимизирован:

X = 1
for i in range(100):
    print X

Оптимизированный результат бега точно такой же, как и раньше, отлично!

Однако, если есть другой поток, делающий это одновременно:

X = 0

Поскольку два потока выполняются параллельно, результат выполнения кода перед оптимизацией может выглядеть следующим образом:11101111.... Был 0, но он был переназначен на 1 в следующем цикле, и после этого всегда был 1.

А как же оптимизированный код:11100000.... из-заX = 1Этот оператор присваивания оптимизирован, и в какой-то момент X становится0После этого нет возможности вернуться к исходному1.

В многоядерном сценарии невозможно легко определить, что две программы «эквивалентны».

Видно, что перестановка компилятора также основана на эффективности работы, но при работе с несколькими потоками будут возникать различные проблемы.

Почему перераспределение памяти

Цитирую Цао Да:

Программная или аппаратная система может в определенной степени нарушить порядок выполнения кода в соответствии с результатами анализа кода для достижения своей скрытой цели.

Программное обеспечение относится к компилятору, а аппаратное обеспечение относится к процессору. Скрытая цель состоит в том, чтобы:

Уменьшить количество программных инструкций Максимальная загрузка ЦП

Цао Да снова похудела!

Основной принцип перестройки памяти

Две невозможные ситуации, упомянутые в примере с перестановкой процессора, не столь очевидны и даже не так сложны для понимания. Какова причина?

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

Когда код, который мы пишем, выполняется как один поток, операторы будут вести себя так, как мы предполагали.顺序выполнить. Когда один поток становится многопоточным, ситуация меняется.

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

Следствием этого является неэффективность. Два потока не могут быть распараллелены, потому что изменения, сделанные одним потоком, повлияют на другой поток, а последний может работать только после того, как влияние модификации первого становится «видимым», которое становится последовательным.

Переосмыслите предыдущий пример:

2 thread

Рассмотрим вопрос, почему (2) ждет выполнения (1) перед его выполнением? Между ними нет никакой связи, и они не влияют друг на друга, их можно делать параллельно!

Поскольку (1) является оператором записи, он занимает больше времени, чем (2), начиная сa single view of memoryС этой точки зрения (2) следует дождаться, пока «эффект» (1) не станет видимым для всех других потоков перед выполнением. Однако на современном ЦП это занимает сотни циклов ЦП.

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

cpu cache

Чтобы (2) выполнялось, не дожидаясь, пока «эффект» выполнения (1) станет видимым, мы можем сохранить эффект (1) вstore buffer:

store buffer

Когда «эффект» (1) записываетсяstore bufferПосле этого (2) может начать выполняться, вам не нужно ждать, покаA = 1добраться до кеша L3. потому чтоstore bufferЭто делается в ядре, так что это очень быстро. В какой-то момент после этого,A = 1Он будет записываться в кеш L3 уровень за уровнем, поэтому его будут видеть все остальные потоки.store bufferЭто равносильно сокрытию отнимающего много времени письма.

store bufferЭто идеально подходит для одного потока, например:

store buffer 1 thread

депозит (1) вstore bufferПосле этого (2) начинает выполняться. Обратите внимание, что поскольку это один и тот же поток, порядок выполнения операторов сохраняется.

(2) непосредственно изstore bufferзачитатьA = 1, без необходимости чтения из кэша L3 или памяти, отлично!

имеютstore bufferПонятие , давайте снова изучим предыдущий пример:

store buffer 2 threads

Сначала выполните (1) и (3), напишите их напрямуюstore buffer, а затем (2) и (4). «Чудо» вот-вот произойдет: (2) Проверьте этоstore buffer, а значение B не нашел, поэтому из памяти читался 0, (4) 0 тоже считывался из памяти. Наконец, распечатайте00.

Все современные процессоры поддерживаютstore buffer, что приводит ко многим явлениям, которые трудно понять программистам. С определенной точки зрения,A = 1Распространение в память и выполнениеprint(B)фразу можно рассматривать как读写指令重排. Некоторые процессоры еще более оптимизированы, и почти все операции могут быть переупорядочены, что является кошмаром.

Поэтому для многопоточных программ все ЦП будут обеспечивать поддержку «блокировки», которая называетсяbarrier,илиfence. Это требует:

A barrier instruction forces all memory operations before it to complete before any memory operation after it can begin.

Инструкция барьера требует, чтобы все операции с памятью были «рассеяны» в память, прежде чем другие операции с памятью смогут продолжиться.

Барьерные инструкции занимают сотни циклов процессора и подвержены ошибкам. Поэтому мы можем использовать продвинутую точкуatomic compare-and-swap, или напрямую с блокировкой более высокого уровня, обычно предоставляемой стандартной библиотекой.

Это барьерная инструкция, предоставляемая ЦП, которая позволяет нам реализовать различные примитивы синхронизации на прикладном уровне, такие как атомарная, которая является основой различных блокировок верхнего уровня.

Выше приведен принцип перестановки ЦП. Перестройка компилятора в основном основана на собственной «модели памяти» языка, а не на глубине.

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

Суммировать

Перераспределение памяти означает, что порядок, в котором программа обращается к памяти во время фактического выполнения, не соответствует порядку, в котором записывается код, в основном для повышения эффективности выполнения. на аппаратном уровнеCPU 重排и программный уровень编译器重排.

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

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

Эта статья является частью лекции Цао Да. Я не изучал подробно другое содержание, такое как протокол MESI, кэш-конфликт и т. д. Это потребует много разъяснений, поэтому я сосредоточусь на подробной расшифровке Перейти язык серии. Если вам интересно, зайдите в блог Цао Да, который предоставляет нам множество справочных ссылок, которые вы можете изучить самостоятельно.

использованная литература

【Цао ​​Да на гитхабе】GitHub.com/часто123/потяните…

【Лекция Цао Да】cch123.github.io/ooo/

[Модель согласованности памяти]дома. В это время. Вашингтон. quota/~урожденный Холт/ боится…

[Наггетс клак-клак, перевод]nuggets.capable/post/684490…

QR