За исключением простейшего алгоритма спин-блокировки, достаточно задействован код мьютекса: хороший оптимизированный код блокировки / разблокировки мьютекса содержит такой код, который трудно понять даже отличному программисту. Он использует специальные инструкции сравнения и установки, управляет не только состоянием разблокировки / блокировки, но и очередью ожидания, при необходимости использует системные вызовы для перехода в состояние ожидания (для блокировки) или пробуждения других потоков (для разблокировки).
Среднестатистический компилятор не может декодировать и «понимать» весь этот сложный код (опять же, за исключением простой спин-блокировки), так что даже для компилятора, не знающего, что такое мьютекс и как он связан к синхронизации, на практике компилятор не может оптимизировать что-либо вокруг такого кода.
Это если код был «встроенным», или доступным для анализа с целью межмодульной оптимизации, или если доступна глобальная оптимизация.
Я предполагаю, что компилятор на самом деле не понимает, что pthread_mutex_lock () - это специальная функция, так что мы просто защищены точками последовательности?
Компилятор не знает, что делает, поэтому не пытается оптимизировать его.
Как это «особенное»? Он непрозрачен и рассматривается как таковой. Он не особенный среди непрозрачных функций.
Нет семантической разницы с произвольной непрозрачной функцией, которая может получить доступ к любому другому объекту.
Меня беспокоит кеширование. Может ли компилятор разместить копию _protected в стеке или в регистре и использовать это устаревшее значение в назначении?
Да, в коде, который действует с объектами прозрачно и напрямую, используя имя переменной или указатели таким образом, чтобы компилятор мог следовать. Не в коде, который может использовать произвольные указатели для косвенного использования переменных.
Так что да, между вызовами непрозрачных функций. Не поперек.
А также для переменных, которые могут использоваться только в функции, по имени: для локальных переменных, у которых нет ни адреса, ни ссылки на них (так что компилятор не может выполнить все дальнейшие действия) использует). Их действительно можно «кэшировать» по произвольным вызовам, включая блокировку / разблокировку.
Если нет, то что этому мешает? Уязвимы ли вариации этого паттерна?
Непрозрачность функций. Без встраивания. Код сборки. Системные вызовы. Сложность кода. Все, что заставляет компиляторы выходить из строя и думать, что «это сложная штука, просто обращается к ней».
Позиция компилятора по умолчанию всегда - «давайте выполнять глупо, я все равно не понимаю, что делается», а не «я оптимизирую это / давайте перепишем алгоритм, который мне лучше знаком». Большая часть кода не оптимизирована сложным нелокальным образом.
Теперь допустим абсолютное худшее (с нашей точки зрения, компилятор должен сдаться, что является абсолютным лучшим с точки зрения оптимизирующего алгоритма):
- функция является «встроенной» (= доступна для встраивания) (либо включается глобальная оптимизация, либо все функции морально «встроены»);
- никакого барьера памяти не требуется (как в однопроцессорной системе с разделением времени, так и в многопроцессорной строго упорядоченной системе) в этом примитиве синхронизации (блокировка или разблокировка), поэтому он не содержит такой вещи;
- не используется никаких специальных инструкций (например, сравнения и установки) (например, для блокировки спина операция разблокировки представляет собой простую запись);
- отсутствует системный вызов для приостановки или пробуждения потоков (не требуется при блокировке вращения);
тогда у нас может возникнуть проблема, поскольку компилятор может оптимизировать вызов функции. Это тривиально устраняется путем вставки барьера компилятора, такого как пустой оператор asm с "clobber" для других доступных переменных. Это означает, что компилятор просто предполагает, что все, что может быть доступно для вызываемой функции, является " затертый ".
или должна ли защищенная переменная быть изменчивой.
Вы можете сделать его нестабильным по той же причине, по которой вы делаете вещи нестабильными: чтобы иметь возможность получить доступ к переменной в отладчике, чтобы переменная с плавающей запятой не имела неправильный тип данных во время выполнения и т. Д.
Сделать его изменчивым на самом деле даже не решит проблему, описанную выше, поскольку volatile - это, по сути, операция памяти в абстрактной машине, которая имеет семантику операции ввода-вывода и, как таковая, упорядочивается только в отношении
- реальный ввод-вывод, такой как iostream
- системные вызовы
- другие нестабильные операции
- asm memory clobbers (но тогда побочный эффект памяти не переупорядочивается вокруг них)
- вызовы внешних функций (как они могли бы сделать одно из указанных выше)
Энергозависимость не упорядочена с учетом побочных эффектов энергонезависимой памяти. Это делает энергозависимую практически бесполезной (бесполезной для практического использования) для написания поточно-безопасного кода даже в самом конкретном случае, когда volatile может a priori помочь в том случае, когда не требуется никакого ограждения памяти: при программировании потоковых примитивов в системе разделения времени на одном процессоре. (Это может быть одним из наименее понятых аспектов C или C ++.)
Таким образом, в то время как volatile действительно предотвращает «кеширование», volatile даже не предотвращает переупорядочение операции блокировки / разблокировки компилятором, если все общие переменные не являются изменчивыми.
person
curiousguy
schedule
22.11.2019