Переупорядочение инструкций чтения / записи в GCC
Примитивы синхронизации Linux (spinlock, mutex, RCU) используют инструкции барьера памяти, чтобы заставить инструкции доступа к памяти переупорядочиваться. И это переупорядочение может быть сделано либо самим процессором, либо компилятором.
Может кто-нибудь показать несколько примеров кода, созданного GCC, где такое переупорядочение выполняется? Меня интересует в основном x86. Причина, по которой я спрашиваю это, состоит в том, чтобы понять, как GCC решает, какие инструкции можно изменить. Различные архитектуры x86 mirco (например, Sandy Bridge и Ivy Bridge) используют разную архитектуру кэширования. Поэтому мне интересно, как GCC осуществляет эффективное переупорядочение, которое помогает в производительности выполнения независимо от архитектуры кэша. Некоторые примеры кода C и переупорядоченного кода, сгенерированного GCC, были бы очень полезны. Спасибо!
2 ответа
Переупорядочение, которое может выполнить GCC, не связано с переупорядочением процессора (x86).
Давайте начнем с переупорядочения компилятора. Правила языка C таковы, что GCC запрещено переупорядочивать volatile
загружает и сохраняет доступ к памяти относительно друг друга или удаляет их, когда между ними возникает точка последовательности (спасибо bobc за это разъяснение). То есть в выводе сборки эти обращения к памяти появятся и будут упорядочены точно в указанном вами порядке. не-volatile
доступы, с другой стороны, могут быть переупорядочены по отношению ко всем другим доступам, volatile
или нет, при условии, что (по правилу "как будто") конечный результат расчета одинаков.
Например, неvolatile
загрузка в коде C может выполняться столько раз, сколько говорится в коде, но в другом порядке (например, если компилятор считает, что удобнее делать это раньше или позже, когда доступно больше регистров). Это может быть сделано меньше, чем написано в коде (например, если копия значения все еще была доступна в регистре в середине большого выражения). Или его можно даже удалить (например, если компилятор может доказать бесполезность загрузки или если он полностью переместил переменную в регистр).
Чтобы предотвратить переупорядочивание компилятора в другое время, вы должны использовать специфичный для компилятора барьер. GCC использует __asm__ __volatile__("":::"memory");
для этого.
Это отличается от переупорядочения ЦП, известного как модель упорядочения памяти. Древние процессоры выполняли инструкции именно в том порядке, в котором они появились в программе; Это называется программным упорядочением или моделью строгого упорядочения памяти. Современные процессоры, однако, иногда прибегают к "читам", чтобы работать быстрее, немного ослабляя модель памяти.
То, как процессоры x86 ослабляют модель памяти, описано в Руководствах по разработке программного обеспечения Intel, том 3, глава 8, раздел 8.2.2 "Упорядочение памяти в P6 и более поздних семействах процессоров". Это, отчасти, то, что написано:
- Чтения не переупорядочиваются с другими чтениями.
- Записи не переупорядочиваются со старыми чтениями.
- Записи в память не переупорядочиваются с другими записями, за некоторыми исключениями.
- Чтения могут быть переупорядочены со старыми записями в разных местах, но не со старыми записями в том же месте.
- Чтение или запись не могут быть переупорядочены с помощью инструкций ввода / вывода, заблокированных инструкций или инструкций сериализации.
- Чтение не может пройти более ранние инструкции LFENCE и MFENCE.
- Пишет не может пройти более ранние инструкции LFENCE, SFENCE и MFENCE.
- Инструкции LFENCE не могут пройти ранее прочитанные.
- Инструкции SFENCE не могут проходить ранее записи.
- Инструкции MFENCE не могут передавать ранее прочитанные или записанные данные.
В разделе 8.2.3 "Примеры, иллюстрирующие принципы упорядочения памяти" приведены очень хорошие примеры того, что можно и нельзя переупорядочивать.
Как вы можете видеть, каждый использует инструкции FENCE, чтобы предотвратить неправильное изменение порядка доступа к памяти процессором x86.
Наконец, вас может заинтересовать эта ссылка, которая углубляется в подробности и содержит примеры сборок, которые вам нужны.
- Правила языка C таковы, что GCC запрещено переупорядочивать энергозависимые нагрузки и хранить обращения к памяти относительно друг друга или удалять их.
Это не так и вводит в заблуждение. Спецификация C не дает такой гарантии. См. Когда доступен летучий объект?
Стандарт поощряет компиляторы воздерживаться от оптимизаций доступа к изменчивым объектам, но оставляет его реализацию определенной относительно того, что составляет изменчивый доступ. Минимальное требование состоит в том, чтобы в точке последовательности все предыдущие обращения к изменчивым объектам стабилизировались, и никаких последующих обращений не происходило. Таким образом, реализация может свободно переупорядочивать и комбинировать изменчивые обращения, которые происходят между точками последовательности, но не может делать это для обращений через точку последовательности.
Традиционно программисты полагались на volatile как на дешевый метод синхронизации, но это уже не надежный метод.