Re[9]: Volatile и предупреждение C5220
От: fk0 Россия https://fk0.name
Дата: 06.01.22 17:55
Оценка: 26 (2)
Здравствуйте, Евгений Музыченко, Вы писали:


ЕМ>Люди, которые не хотят изучать ничего нового, являются, как правило, клиентами психиатров. А нежелание изучать новое, несущее больше геморроя, чем пользы, просто потому, что оно ново — вполне разумная тактика управления личными ресурсами. Далеко не все востребованные продукты изготовлены с применением новейших технологий.


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

ЕМ>>>И как это связано с тривиальностью/нетривиальностью?

fk0>>Так, что невозможно компилятор сделать, чтоб он удовлетворял требованиям по обращению с volatile-переменными.
ЕМ>Не вижу ровно никаких причин для невозможности.

Я ж только что написал, что (N)RVO требуемые стандартом не выполнимы для классов с volatile переменными,
так как два раздела стандарта вступают в противоречие.

fk0>>Их, например, через memcpy() даже копировать нельзя. Потому, что в библиотеке сильно-оптимизированный вариант

fk0>>функции memcpy()

ЕМ>При чем здесь библиотека и функция memcpy? Под тривиальностью всегда понималась возможность сгенерировать код, который не обращается к методам класса, не использует виртуальности и т.п. То есть, тривиальность определяется по отсутствию вовлечения языковых средств, находящихся выше генератора кода. В частности, тривиальными являются конструкторы POD. POD с квалификатором volatile тоже всегда относился к тривиальным. Потому я и удивился, увидев, что VC++ 19.x вдруг стал считать их нетривиальными.


Мотивировка в том, это же очевидно, что для каждого класса с volatile членом нужна генерация уникального
конструктора копирования, уникального конструктора перемещения. Уникального для данного класса. Для обычного
класса, без volatile-переменных, с POD-членами, попросту вызывается memcpy, который даже тут же инлайнится
в месте использования. А в случае с классом у которого внутри volatile -- нужен вызов конкретной функции,
специфичной для данного класса. Что это как не тривиальный конструктор?

fk0>>хрен знает как разорвёт блок памяти по словам, что-то скопирует по байтам, что-то по длинным словам (через FPU, векторные расширения).

ЕМ>Всем этим традиционно занимается генератор кода. При таком подходе нужно заодно объявить нетривиальными и float/double, поскольку у многих процессоров вообще нет никакой аппаратной плавучки, и все операции преобразуются в неявные вызовы функций, а в многопроцессорных системах с общим процессорам плавучки нужны еще и приседания с синхронизацией доступа и запретом прерываний.

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

fk0>>Буквально, скопировать объект с volatile-переменной -- нетривиальная задача. Для этого нужно сгенерировать специальную функцию, которая умеет работать с этим объектом, будет знать там offset для volatile-переменной и её скопирует аккуратно одной инструкцией.

ЕМ>Во-первых, это типовая задача кодогенератора, с нею успешно справлялись во все времена.

Не типовая, так как требуется конструктор копирования специфичный для данной функции. Тривиальный
конструктор копирования -- это тот, который не сгенерирован для конкретного класса.

TriviallyCopyable objects can be copied by copying their object representations manually, e.g. with std::memmove. All data types compatible with the C language (POD types) are trivially copyable.


Так вот очевидно, что класс с volatile-переменными невозможно скопировать с помощью memmove.
Это следует из определения volatile-переменной:

Any attempt to refer to a volatile object through a glvalue of non-volatile type (e.g. through a reference or pointer to non-volatile type) results in undefined behavior.


ЕМ> Во-вторых, откуда взялось требование "аккуратно одной инструкцией"? volatile-переменные никогда не заявлялись атомарными, для них гарантировалось всего лишь неприменение отдельных оптимизаций.


Volatile переменная тем не менее не может считываться компилятором по два раза вместо одного (что произойдёт при побайтовом
копировании, например). Типичный пример -- SFR-регистры, которые изменяют состояние периферии при считывании. Да у них ширина
в машинное слово и их нужно читать одной инструкцией. И нельзя читать два раза если у компилятора не хватает регистров и он
соптимизировал вникуда локальную переменную в которой хранится копия.

По факту чтение volatile запросто атомарно до тех пор, пока такая переменная выровненная и имеет соответствующий размер.

fk0>>

The problem with this is that a volatile qualified type may need to be copied in a specific way (by copying using only atomic operations on multithreaded platforms, for example) in order to avoid the “memory tearing” that may occur with a byte-by-byte copy.


ЕМ>Вы таки определитесь — то ли volatile "не атомарно", "не является барьером" и якобы вообще не имеет смысла, и тогда для него не нужны никакие дополнительные техники (а лишь отказ от того, что охотно используется для остального), или же это вполне себе рабочий инструмент.


Изменение volatile-переменной -- не атомарно. RISC-процессоры вообще не имеют инструкций (кроме SWAP на ARM) для
чтения-модификации-записи в одной инструкции, например... Чтение или запись де-факто в определенных обстоятельствах --
атомарно и этим часто пользуются.

Смысла процитированной фразы я не смог понять. Volatile имеет смысл для того, для чего предназначен -- для работы
с SFR-регистрами преимущественно (чтение или запись которых изменяет состояние аппаратуры). Volatile позволяет
гарантировать, что обращение к переменной произойдёт ровно так как написано в коде. Ровно один раз, например,
а не два или три подряд (потому, что компилятор сгенерировал такой код, потому, что регистровому аллокатору не
хватило регистров).

Volatile не предназначен для синхронизации потоков и ничего для этого не предоставляет. И уж тем более не предназначен
для синхронизации процессоров с общим полем памяти.

Для синхронизации потоков нужно заставить компилятор физически произвести запись в память в нужное время, в нужном
месте. Volatile этого не гарантирует (повторю, что компилятор может запросто "оптимизировать" логику так, что все
volatile переменные заипишет либо раньше времени, либо позже, относительно записи не-volatile ячеек памяти, относительно
других инструкций процессора, включая запрет прерываний, и т.п.) Для упорядочивания обращений к памяти со стороны
компилятора существует понятие компиляторного барьера памяти, чем является выражение вида asm("nop" ::: "memory) в GCC,
или даже просто вызов любой функции (кроме случая static-функции...) Компилятор должен записать всё в память перед барьером,
и чтения-записи которыев программе после барьера они будут сделаны точно после.

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

Volatile всего этого -- не даёт. Использование volatile скорей имеет корни в том, что есть такой частый паттерн,
когда запускается поток, в котором в цикле выполняется какая-то работа, и цикл прерывается по переменной-флагу.
И компилятор для обычной, не-volatile переменной попросту оптимизирует цикл в бесконечный. Так как видит, что
переменная не может никак быть изменена. По идее, если переменная-флаг может использоваться хоть в какой-то другой функции,
то такой оптимизации происходить не может. Однако если весь код состоит из static-переменных или функций и компилятор
построив граф вызовов понимает, что переменная изменена быть не может -- вправе выполнить такую оптимизацию.
И способ избежать такой оптимизации -- использовать volatile. Либо использовав memory barrier, либо отметить только
одну проблемную переменную, как в примере по ссылке: https://godbolt.org/z/37hfeY76M

И если для описанного случая volatile как-то справляется со своей работой, то для случая когда есть хотя бы
две переменные обращение к котором нужно синхронизировать -- уже ничего не получится. Да, можно вообще всё подряд
сделать volatile и правильный порядок обращения гарантирован, казалось бы. Но порядок синхронизации кешей процессора
остаётся каким попало и на многопроцессорной машине такое всё равно не заработает.

Volatile просто не средство для синхронизации потоков и процессоров, вот и всё что следует знать.
Для синхронизации потоков есть барьеры, для синхронизации процессоров -- специальные инструкции процессора.
И в целом это всё вручную делать обычно нет необходимости, так как существуют, предоставляемые C/C++ библиотекой
более высокоуровневые примитивы синхронизации, такие как мьютексы (являющиеся барьерами с нужной acquire/release
семантикой) и атомарные переменные. Которые просто "работают из коробки". И их следует в нормальном случае
использовать. А код с volatile -- это нечто странно выглядящее и таящее в себе неведомые сюрпризы.
Re[3]: Volatile и предупреждение C5220
От: fk0 Россия https://fk0.name
Дата: 06.01.22 18:19
Оценка:
Здравствуйте, Евгений Музыченко, Вы писали:

fk0>>В чистом виде сам volatile вообще ни за чем не нужен, кроме случаев когда переменная -- регистр специального назначения изменяемый аппаратурой.

ЕМ>Все встроенные функции _Interlocked* в MSVC++, выполняющие аппаратно атомарные операции, определены с volatile для параметров-указателей. Другое дело, что компилятор не считает ошибкой передачу такой функции переменной без volatile.

volatile в прототипе функции достаточно бессмысленная вещь.

fk0>>А использование volatile не по делу только создаёт трудности компилятору по оптимизации кода.

ЕМ>Я Вас умоляю. На фоне трудностей, которые ему создают всякие локальные функции, это мышкины слезки.

Вот эти слезки: https://godbolt.org/z/Wdo7hP4bj https://godbolt.org/z/E6M7xTPax

У микрософта volatile ломает оптимизацию (скорей потому, что микрософт строго соответствует букве стандарта)
и заставляет генерировать как раз специфичный для класса конструктор копирования, здесь он заинлайнен в цикл,
но сам факт, что memcpy -- не работает. Для сколько-нибудь сложного класса вместо memcpy будет уже вызов
функции конструктора копирования в цикле. И не работает RVO.
Re[2]: Volatile и предупреждение C5220
От: Андрей Тарасевич Беларусь  
Дата: 06.01.22 19:05
Оценка:
Здравствуйте, Alexander G, Вы писали:
AG>Сравните https://godbolt.org/z/59oWE6vx6

И что же именно мы там должны увидеть? В окне справа я вижу value-initialization. Но вот "конструктор копирования" там странслировался в какой-то бессмысленный набор ничего не делающих инструкций.
Best regards,
Андрей Тарасевич
http://files.rsdn.org/2174/val.gif
Re[3]: Volatile и предупреждение C5220
От: Alexander G Украина  
Дата: 06.01.22 20:31
Оценка:
Здравствуйте, Андрей Тарасевич, Вы писали:

АТ> Но вот "конструктор копирования" там странслировался в какой-то бессмысленный набор ничего не делающих инструкций.


Ха, действительно. Не всмотрелся, ожидал именно копирования. Тогда согласен, не знаю, что там мы должны увидеть.
Русский военный корабль идёт ко дну!
Re[8]: Volatile и предупреждение C5220
От: ksandro Мухосранск  
Дата: 07.01.22 23:21
Оценка:
Здравствуйте, Максим, Вы писали:

М>Ну так да. Самый простой сценарий. Есть пременная flag которая в одном потоке читается, что-то в духе

М>
М>while(flag) {...}
М>

М>а во втором изменяется
М>
М>flag = false
М>


М>Так вот, изменяя переменную во втором потоке, мы не знаем когда эти изменения "прилетят" в первый поток. Все зависит от того, где находится эта самая переменная, когда будут сброшены процессорные кеши итд.


Я кстати, видел именно такой код с остановкой цикла по выставлению флага. Эта конструкция использовалась для корректного завершения программы (останавливаем все потоки, освобождаем все ресурсы). И оно много лет прекрасно работало (возможно и сейчас работает), компилировалось это дело сначала разными версиями gcc потом clang, но только под x86. Проблем никогда не возникало. Мне тогда очень хотелось заменить это на atomic, но мне сказали "работает не трогай!".

Я так понимаю, на x86 такой код не должен вызывать проблем. Да, изменяя переменную во втором потоке мы не знаем точно, когда изменения прилетят в первый поток, но они гарантированно прилетят, и это будет за небольшое время. Если у нас просто один флаг который нужен для остановки цикла, нам совсем не важен точный порядок записи чтения. Но вообще было бы интересно узнать, возможны ли какие-то реальные проблемы именно с этим кодом на x86? И какие реальные проблемы могут быть на других платформах (я никогда не работал с ARM, но правильно ли я понимаю, что там изменение переменной может так и не прилететь в читающий поток)?
Re[9]: Volatile и предупреждение C5220
От: Евгений Музыченко Франция https://software.muzychenko.net/ru
Дата: 08.01.22 13:12
Оценка: 1 (1)
Здравствуйте, ksandro, Вы писали:

K>И оно много лет прекрасно работало (возможно и сейчас работает)


Не "возможно", а точно работает. И будет работать до тех пор, пока обеспечивается двоичная совместимость.

K>(я никогда не работал с ARM, но правильно ли я понимаю, что там изменение переменной может так и не прилететь в читающий поток)?


Никогда не прилетит оно только в одном случае — когда ядро, на котором работает записавший поток, никогда не изменит режима выполнения (прерывание, исключение, вызов привилегированного кода и т.п.). А такое возможно либо при глухом зависании ядра (при котором нормальная работа остальных ядер будет под вопросом), либо при работе софта на голом железе (без ОС).

В случае типовой работы под ОС, типовой же сценарий — установить флаг и перейти к ожиданию, вызвав примитив ОС типа Wait, Sleep, Delay и т.п. Эти примитивы выполняются через исключение или специальные операции вызова привилегированного кода. Даже если при этом процессор аппаратно не сбросит буферы — это сделает обработчик ОС.
Re[9]: Volatile и предупреждение C5220
От: vsb Казахстан  
Дата: 10.01.22 13:44
Оценка: +1
Здравствуйте, ksandro, Вы писали:

K>Я кстати, видел именно такой код с остановкой цикла по выставлению флага. Эта конструкция использовалась для корректного завершения программы (останавливаем все потоки, освобождаем все ресурсы). И оно много лет прекрасно работало (возможно и сейчас работает), компилировалось это дело сначала разными версиями gcc потом clang, но только под x86. Проблем никогда не возникало. Мне тогда очень хотелось заменить это на atomic, но мне сказали "работает не трогай!".


K>Я так понимаю, на x86 такой код не должен вызывать проблем. Да, изменяя переменную во втором потоке мы не знаем точно, когда изменения прилетят в первый поток, но они гарантированно прилетят, и это будет за небольшое время. Если у нас просто один флаг который нужен для остановки цикла, нам совсем не важен точный порядок записи чтения. Но вообще было бы интересно узнать, возможны ли какие-то реальные проблемы именно с этим кодом на x86? И какие реальные проблемы могут быть на других платформах (я никогда не работал с ARM, но правильно ли я понимаю, что там изменение переменной может так и не прилететь в читающий поток)?


Без volatile компилятор может скомпилировать код "не так" в какой-нибудь новой версии и убрать "ненужные" обращения к памяти, что приведёт к тому, что работать перестанет.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.