Комментарии 72
Что-то развенчаных мифов вопреки многообещающему заголовку негусто.
Ну почему же. На АРМах есть Cache Coherent Interconnect для обеспечения когерентности кешей между кластерами и вариация MESI в пределах кластера.
Я бы не называл это «хуже» по ряду причин, начиная от очевидно более высокой достижимой производительности (чем компилятору/шедулеру больше позволено, тем лучше) и заканчивая, если хотите, дисциплинированием.
Если говорить о современных армах в мобильниках, то там когерентность кэшей имеется. Может в каких-то древних реализациях и не было, не знаю. Чего в армах по-другому, так это memory ordering, но это кешей и не касается.
Чего в армах по-другому, так это memory ordering, но это кешей и не касается.
Очень даже касается. DSB не поставите — и всё, можно глюки ловить… долго.

А он сам по себе не возьмётся из ниоткуда, если у вас переменная как volatile не помечена.
Каким образом перестановка операций чтения и записи касается когерентности кэшей? Барьеры ставят, чтобы соблюдать последовательность load/store операций как написал программист, а не как задумал процессор. Даже в amd64 с его гарантиями всего и вся есть барьеры, т.к. там допускается один вариант перестановки.
В amd64 нормальные инструкции сюрпризов не преподносят, нужно использовать MOVNTxxx инструкции, чтобы «выстрелить себе в ногу». У ARMа же один список хинтов (все эти «inner shareable domain», «outer shareable domain», «point of unification» и прочее) как бы намекает на то, что не всё так так просто.
Потому что когерентность кешей и соблюдение последовательности load/store операций — это эквивалентные проблемы.
Только при некоторых дополнительных предположениях. Вы можете убрать кеш, но реордеринг/elision это автоматически не уберёт.
Именно об этом я и говорю. Система в которой нет кеша но есть реордеринг имеет все те же самые проблемы что и система с некогерентными кешами но без реордеринга.
Система с реордерингом, но без кешей — это такой «сферический конь в вакууме». В природе не встречается, насколько я знаю.
Но ничего не мешает убрать когерентность кэшей и оставить реордеринг. Это совершенно не связанные между собой вещи. То, что они там программисту примерно теже самые проблемы создают, никакого значения особо не имеет. Статья о кэшах, поэтому и реордеринг в ней упоминать не по теме. Была бы статья о проблемах многопоточных программ с общим изменяемым состоянием, то можно было бы упоминать и то, и другое.
То, что они там программисту примерно теже самые проблемы создают, никакого значения особо не имеет.
А что тогда имеет? Куда вы собираетесь «сферическое знание» о кешах применять? Для написания фантастики? Это не тематика Хабра, вроде бы…
Но тут больше проблема подготовки данных и мастерства бенчмаркать эти вещи. А не «вы всё врёти». Тоесть вероятность того, что на собеседовании с вас спросят тайминги доступа к кешу и длину кеш-линии — нулевая. Только если вы не идете в какие-нибудь жуткие data oriented вещи, типа программирования графики/физики.
Так они у каждого семейства разные, какой толк их спрашивать. А вот представлять, что процессор это не волшебная коробка, а вполне себе детерминированное устройство, очень полезно.
А вот представлять, что процессор это не волшебная коробка, а вполне себе детерминированное устройство, очень полезно.

Правда Spectre/Meltdown показали, что процессоры — не слишком то уж детерменированные устройства :)
Правда Spectre/Meltdown показали, что процессоры — не слишком то уж детерменированные устройства :)
Как раз Spectre/Meltdown работают именно из-за детерменированности кешей. Если бы там времена доступа случайными были бы — ничего бы не работало…
Нене, с точки зрения обычного, нормального программиста, никакого meltdown в принципе существовать не может. А вот с точки зрения Интела да, кэш отработал отлично.
Мне кажется, автор зря здесь упомянул Java. В данном контексте его утверждения просто опасны.
Да, на интеловых процессорах есть протокол когерентности кешей. Но java-приложение может работать на других процессорах. Этот протокол может быть выключен. В конце концов, есть распределенные java-машины.
Плюс, не забываем, что компилятор банально может такой код:
Integer x = 1;
while(x > 0) { тут не изменяем x };

оптимизировать до
while(true) {...}

(справедливости ради, самые известные компиляторы, насколько я знаю, так не делают)

Ну и volatile — это болше чем просто запись\чтение мимо кешей. Это happens before.
В остальном — очень интересно.
1. Тут пример плохой (я ведь не статью писал). Здесь не очевидно, что в x может прилететь из другого треда. И вот тут и проблема: согласно java memory model, этого может никогда и не произойти. И компилятор имеет право так считать. А может и произойти.
2. Там же С++
Здесь не очевидно, что в x может прилететь из другого треда. И вот тут и проблема: согласно java memory model, этого может никогда и не произойти. И компилятор имеет право так считать. А может и произойти.

Это называется undefined behavior, это когда чего-то делать нельзя, но программист всё равно это делает. Компилятор имеет право делать вид, будто UB никогда не происходит, что очень сильно развязывает ему руки в плане оптимизаций. Как следствие, когда программист пытается делать то, чего делать нельзя, в общем случае это заканчивается плохо.
2. Там же С++

А какая разница, оптимизация одна и та же — обычный constant propagation.
Понятно, что это UB. Речь шла о том, что на практике (не)делают самые популярные компиляторы java — оракловый и gnu.
Но как уже отметил товарищ из соседней ветки, что не делают компиляторы, делает jit.
Сорри, я имел в виду компилер из состава openjdk.
Нагуглил первый попавшийся линуксовый — оказался не тот.

Я не понимаю, почему для f_atomic компилятор не имеет права сначала доказать, что terminate нигде не видно, и потом сделать тот же constant propagation.


А, я зачем-то смотрел только на вывод gcc. clang ровно это и делает. Теперь мне интересно, на самом ли деле он имеет на это право.

Не знаю как в Java, но в C/C++ volatile просто говорит компилятору не пытаться оптимизировать операции с этой переменной. Т.е. каждый раз считывать значение переменной из памяти при обращении к ней. К кешам это никакого отношения не имеет.
Лично мне это кажется недоработкой Си/С++: отдельные языковая модель исполнения и «железная» модель памяти.

В Java и C# все намного проще: есть только одна модель исполнения и памяти, которой JVM/CLR следует; и видя ключевое слово volatile JIT не только отключает оптимизации операций над этим полем — но и сам расставляет барьеры чтобы процессор тоже ничего не напортачил. В частности, volatile read всегда дает барьер чтения, а volatile write — барьер записи.
Ну, так С\С++ гораздо ближе к железу. Это их «экологическая ниша». Они созданы для того, чтобы решать проблемы, которые в более высокоабстрактных (не знаю нужного термина) отсутствуют.
На самом деле всё проще: C был создан для того, чтобы писать операционную систему (одну).

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

То, что он оказался удачным и его приспособили в кучу разных других мест — совсем другая история. Хотя тот факт, что низкоуровный механизм, который полвека назад был очень даже к месту там, где его разработали, применили в последующем для кучу разных других задач… ну, плохо, конечно… а чего делать? Других механизмов долгое время не было.
Какая разница откуда у него «растут ноги»? Чтобы работать с «железом» на PDP вам нужно точно управлять тем, что и где вы читаете и пишите. Позже, во всемена DOS'а — это тоже было очень важное умение (всякие VGA и прочее просто-таки по спецификации так устроены — там можно записывать в одну и ту же ячейку памяти дважды и получать разные результаты).

А вот уже как раз когда появились Linux/Windows, кеши и многопоточность — требования изменились… а применяемый инструмент — остался.
В embedded все так же актуальна возможность дважды записать по одному адресу одни и те же данные.
В embedded другая проблема: для них зачастую очень полезно уметь писать код, использующий особенности железа. А не код, заточенный под PDP и при этом переносимый на миллион платформ.

Для этого C подходит плохо, но его используют за неимением лучшего.
И чем простите мало подходит? Мне известно только то что нет операций работы с битами, приходиться возится с масками. На уровне железа это исправляет компилятор, а на уровне читаемости это исправляется макросами или инлайн функциями.

В embedded, как раз, как нигде важна переносимость на другие платформы. И совсем не понятно чем мешает «заточенность под PDP»
И чем простите мало подходит?
Там что до банальных вещей, которые процессор вычисляет «на раз» невозможно никак добраться. Ни до флага «overflow», ни до «carry», никуда.

Да, можно пытаться делать странные вещи и надеяться на то, что оптимизатор поймёт и сделает «как надо», как-нибудь так:
#include <inttypes.h>

struct pair {
  uint64_t low;
  uint64_t hi;
};

pair add(pair& a, pair& b) {
 pair s;
 s.low = a.low + b.low;
 s.hi = a.hi + b.hi + (s.low < a.low); //carry
 return s;
}
Вот только…
GCC5 — нормально
GCC6 — ай-ай-ай
GCC7 — всё ещё ай-ай-ай
GCC8 — пофиксили
И это — простейший пример! А если чего посложнее навернуть?

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

И совсем не понятно чем мешает «заточенность под PDP»
К тому, что заточенность под «железо PDP» в виде volatile, ++/-- и прочего — поддерживаются на всех платформах, а вот вещи, которые современные DSP умеют (скажем переменные с фиксированной точкой) — вынесены в зависящие от конмпилятора расширения (если вообще есть поддержка).
Понятно, но тут или расширения для компилятора или разные языки для разных процессоров. Хотя в принципе могли и ввести в стандарт опциональные платформозависимые вещи, как ввели COMPLEX.

Но на PDP-11 Вы зациклились, volatile, INC, DEC нужны почти на всем (за DSP не ручаюсь).
Но на PDP-11 Вы зациклились, volatile, INC, DEC нужны почти на всем (за DSP не ручаюсь)
volatile (в том виде, в каком он изначально появился) не нужен в системе без мапирования регистров на память, а INC и DEC — в большинстве систем устроены совсем не так, как в PDP/VAX, где вы можете одной инструкцией процессора считать данные из массива, сдвинуть индекс и ещё что-нибудь в этими данными посчитать «этакое».

Из распространённых процессоров такое есть разве что в ARM'е, большинство процессоров постфиксный ++/-- в том виде, в каком он есть в C на уровне машинных кодов не поддерживают.

Хотя в принципе могли и ввести в стандарт опциональные платформозависимые вещи, как ввели COMPLEX.
Опционально — оно есть. Но тут мы сходу вляпываемся в переносимость: стоит вам этими типами воспользоваться — так сразу получается, что ваш алгоритм вы уже на декстопе не запустите и под embedded — тоже далеко не под любой.

В сухом остатке: работа с saturation arithmeticой есть на всех современных процессорах (в x86 меньше, в ARM и DSP — больше), но вот как раз её в языке нет, а зато вот те самые «штучки от PDP», которые из большинства процессоров давно пропали — есть…
(справедливости ради, самые известные компиляторы, насколько я знаю, так не делают)

Компиляторы — да, не делают. А вот JIT вполне может такое устроить...

Источник не приведу, но вроде как это общеизвестная информация. Во всех языках семейств JVM и .NET компиляторы генерируют байт-код без хитрых оптимизаций, а оптимизацией занимается уже JIT.

Это нужно хотя бы потому что только JIT знает целевую платформу в которой будет исполняться код, а также информацию которая доступна только в рантайме; то есть у него попросту больше возможностей оптимизации.
Да, спасибо. Я выключил jit — и все отработало.
Надо будет почитать на эту тему. Как оказалось, здесь я «плаваю».
«Выключил jit» — это вообще как? Если вы имеете в виду использование AOT-компилятора, то он тоже занимается оптимизациями, только у него ограничения иные.
Миф только один — что кэш «прозрачен» для программиста. Все остальное — детали.
если два разных потока в любом месте системы читают с одного и того же адреса памяти, то они никогда не должны одновременно считывать разные значения.

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

Одна из немногих статей, которые заставили меня улыбаться во время чтения; чистое удовольствие!

Автору спасибо.
Спасибо большое за статью, за объяснение. У меня остался один вопросик.

Мне интересно, вот в части внешних устройств, когда некоторые адреса памяти мапятся какому-нибудь устройству, как в этом случае MESI и кэш L2 поступает, когда надо синкать данные из этих адресов?

Или память копируется из замапленных адресов куда-то в общее пространство? Или же L2 все-таки видит, что память изменилась и инвалидирует свой кэш?
Скорее кэширование для таких регионов памяти вообще отключено.
И тогда volatile работает как и написано — из памяти, а не из кэшей. Тогда получается, это не миф.
Не знаю как в Java, но в С/C++ volatile означает, что переменная может измениться извне. Как раз внешним устройством.
Собственно это как раз семантика, ради которой всё было изначально придумано… и она тоже не работает.

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

И вот всё это всё рано не выражается на C — приходится вкорячивать ассемблерные вставки.

volatile в C/C++ на современных процессорах — это такой же рудимент, как register
Потому что volatile к процессорам отношения не имеет. Это инструкция компилятору, который про процессор вообще ничего не знает. Его абстрактная машина никаких кэшей даже не имеет. Без volatile просто невозможно было бы писать что либо, что с устройствами общается.
Без volatile просто невозможно было бы писать что либо, что с устройствами общается.
Ассеблерные вставки его более, чем заменяют. А поскольку без них низкоуровневые штуки не обходятся, то и смысла в нём особо нету…
Каждый раз делать эти вставки, когда требуется что-то с железом делать? Да еще на разных платформах? Как-то не очень. Драйвера как-то без ассемблера спокойно обходятся чуть ли не полностью. А те самые 42нс можно как раз оставить на реализацию ассемблеру, обернутому в С функцию. Благо хелперы в ядре такие у всех наверное есть.
Драйвера как-то без ассемблера спокойно обходятся чуть ли не полностью.
Тот факт, что вы ассемблера не видите — не значит, что его нет. Функции доступа «к железу» почти все являются ассемблерными вставками.

Да собственно уже даже само название документа Why the «volatile» type class should not be used в исходниках ядра достаточно недвусмысленно намекает — как в этому всему относятся разработчики, которым реально нужно работать с «железом».

P.S. У разработчиков для микроконтроллеров есть, конечно, своё видение мира… ну то такое — этот подход и приводит к тому, что начего нельзя нигде менять, проапгрейдишь компилятор — всё сразу развалится…
В этом документе больше речь о том, что volatile не так понимают люди, используют заместо примитивов синхронизации, надеются на какое-то поведение железа и компилятора и т.д., а не то, что оно вообще не нужно. Там же примеры, когда использовать можно. Неправильное применение volatile — вот это как раз проблема. Собственно, почему некоторые хотят внести в стандарт другие примитивы, которые дадут действительно то, что обычно ожидают от volatile. И почему ядро дает обертки, скорее всего над тем же volatile, чтобы неправильное применение пресечь.

В этом документе описывается почему не следует использовать volatile для синхронизации потоков.


А вот ниже есть вот такая приписка:


There are still a few rare situations where volatile makes sense in the kernel:
  • The above-mentioned accessor functions might use volatile on architectures where direct I/O memory access does work.
    [...]
  • Pointers to data structures in coherent memory which might be modified by I/O devices can, sometimes, legitimately be volatile.

А вы специально пропустили тот вариант, который объясняет всё остальное?

  • Jiffies is considered to be a «stupid legacy» issue (Linus's words) in this regard; fixing it would be more trouble than it is worth.

Собственно такая же приписка может быть сделана ко всем остальным пунктам (кроме asm volatile).
Вы может почитаете уже исходники ядра? В нем дофига volatile. Те самые «правильные» функции, через которые надо обращаться к железу, как раз через volatile и написаны.
Они по разному на разных архитектурах написаны. А принцип «работает — не трогай» никто не отменял.
А вы специально пропустили тот вариант, который объясняет всё остальное?

А зачем его приводить, если приведенных вариантов — достаточно?


Собственно такая же приписка может быть сделана ко всем остальным пунктам

Но она не сделана. Если уж вы ссылаетесь на авторитеты для подтверждения своего мнения — не нужно им приписывать того, чего они не говорили.

Знаете — вы сейчас ведёте себя в точности, как преподаватели марксизма-ленинизма. Способные надёргать цитат из работ классиков и с их помощью оправдать всё, что угодно.

Если вы хотите этим заниматься — ну ради бога… только без меня.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.