Pull to refresh

Comments 21

UFO just landed and posted this here

Очень бы хотелось продолжения, т.к. нередко сталкиваюсь с последствиями недопонимания этих концепций в различном коде. А у вас очень хорошо получается объяснять.


Жду продолжения!

Отличное объяснение. Возник вопрос, связанный со слабым исполнением, по программе реализующей алгоритм Деккера, в том виде в котором он описывается:
Мы отказываемся от слабого исполнения и обязываем процессор обновить глобальную память после первой инструкции (как я это понял). Как данная модификация сказывается на быстродействии?
Скорость исполнения программы, конечно, уменьшается. Конкретные цифры для этой программы я не готов предоставить, но есть работы, которые тестировали, насколько происходит замедление для реальных программ, если форсировать отсутствие слабых исполнений. Проще всего будет сослаться на мой доклад в CSCenter (ссылка уже с нужной отметкой по времени).
Очень интересно, спасибо! Я собирался запись вашего недавнего доклада в Питере в compsciclub посмотреть, но, если вы содержание изложите в виде текста, то с гораздо большим удовольствием прочитаю текст, этот формат куда приятнее. И, если будете писать продолжение, то добавьте еще ссылок, куда дальше копать, если не трудно (особенно в связи с lock-free алгоритмами с atomic-ами).
Спасибо! Доклад гораздо более общий. Содержание этой статьи там в первых 5 минутах. Пока доклад не планирую полностью в текст переносить.
Про ссылки учту)
UFO just landed and posted this here

Ну, asm volatile("mfence" ::: "memory") — это как раз своего рода синхронизация и есть. Но да, это непереносимо и работает только на одном процессоре.

Я так понимаю здесь рассматривается именно реальное поведение на конкретном процессоре, т.к с точки зрения C++ просто нельзя писать в неатомарнвы переменные без синхронизации. Они вообще могли какую нить ерись выдать.

Вы правы, но я просто решил не упоминать atomic в первой статье. На самом деле, то же самое можно повторить, если сделать x и y atomic'ами и обращаться к ним с помощью memory_order_relaxed.
Модель C/C++ достоина отдельного вдумчивого поста)

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

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

Спасибо за очень ясную нотацию.


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

Через какое? И может быть, что не попадет вовсе в гипотетическом случае, когда кроме наших 2-х потоков ничего более не выполняется?

Это интересный вопрос) Если программа корректно работает, то архитектура гарантирует, что когда-нибудь запись попадёт в основную память. Но конкретнее в комментарии ответить не получится — это вопрос уже близок к той части формализации поведения процессоров, которая не очень хорошо изучена.

Вот и мне интересно. Но можно усугубить :) В смысле то же самое, но с NUMA. А так же на разных арх-рах: Intel, AMD, ARM...

Аппаратно — нет, такого быть не может, "спекулятивность" процессора ограничена длиной конвейера, да и кеши не для "застревания" данных проектируются...


А вот в C++ такое может произойти запросто, потому что компилятор имеет право передвинуть присваивание куда угодно или даже вовсе удалить, если докажет что присвоенное значение нигде не используется.

Через какое?

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

Подскажите пожалуйста, а если мы скомпилируем вариант SB без барьеров с флагом -O0 и запустим на x86, правильно ли я понимаю, что у нас все равно остается вероятность получить a=0; b=0 из-за аппаратных оптимизаций связанных с буфером записи?
Вам удавалось в тестах воспроизвести ситуацию нарушения SC именно из-за аппаратных оптимизаций x86?
Вам удавалось в тестах воспроизвести ситуацию нарушения SC именно из-за аппаратных оптимизаций x86?

Я воспроизвёл. i3-4170 и Ryzen 5 2400G.

тогда не совсем понятно, зачем автор вставляет cout.flush().
по-идее, даже без перестановки строк компилятором студенты могут получить нарушение SC только из-за буферов записи.
тогда не совсем понятно, зачем автор вставляет cout.flush().

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


Компилятор может переставить две строки в thread1 (в показанном варианте кода без барьеров), но не обязан. А от чего это зависит — выглядит как погода на Марсе, но является результатом взаимодействия исходника, версии компилятора, опции сборки и т.п. Вот автор и нашёл эмпирический вариант, как это сделать. Не был бы cout — нашлось бы что-то другое.

Тема интересная, имеет смысл именно такие крайние тонкие случаи подробно рассматривать. Замечания по сказанному:


​1. Есть разные уровни "слабости" моделей памяти. X86, например, не переупорядочивает чтения одного ядра (соответственно одной нити) между собой, аналогично записи, аналогично записи с более поздними чтениями… а большинство RISC может делать хотя бы часть из этого. Особенность с буфером записи, конечно, сильно изменяет обстановку, но не фатально для обычной синхронизации. Фактически, модель X86 следовало бы назвать сильной, а не слабой (как обычно и делают) — специфика буфера записи (или строковых команд) тут влияет на очень специфические случаи. Хотя по сравнению с SystemZ она, да, слабая :)


Но вот классический документ "Intel64 Architecture Memory Ordering" (318147) про store buffer не говорит ни слова, зато,


Stores are not reordered with older loads.

как бы намекает на то, что его такие действия недопустимы. Или это случай его пункта 2.4? Тут слишком легко запутаться, прошу подтверждений.


​2. Имеет смысл добавить, что использовать asm тут не нужно: в C++11 есть стандартные переносимые эквиваленты:


asm("":::"memory") -> std::atomic_signal_fence(std::memory_order_acq_rel);
asm("mfence":::"memory") -> std::atomic_thread_fence(std::memory_order_seq_cst);


это даже без атомарных переменных.


​3. А теперь интересное: если я в каждой нити вставлю синхронизацию по следующему типу:


void thread2() {
 y = 1;
 std::atomic_thread_fence(std::memory_order_acq_rel);
 b = x;
}

то GCC (8, 9), Clang (6, 10) при -O0 сохраняют mfence, а при O уже нет! Им надо заменить acq_rel на seq_cst, чтобы сохранился mfence на всех уровнях оптимизации.


Я в первой версии этого комментария начал откровенно недоумевать, почему acq_rel по его мнению не должен включать все меры, чтобы выпихнуть предыдущие сбросы в память, если он знает про существование store buffer. Но, кажется, таки понятно: на обычную синхронизацию по типу мьютексов это не влияет. А вот с lock-free хитрее, за время доступного редактирования точно не успею обдумать во всех деталях. Прошу объяснений, кто может.

про store buffer не говорит ни слова

Недоредактировал. Говорит — в пункте 2.4, который надо отдельно грок.


PS: Понял. older loads — это которые раньше в потоке команд, а я почему-то подумал в обратную сторону. Да, порядка между более ранними записями и более поздними чтениями никто не обещал, его при необходимости надо требовать явно. Но сама необходимость такого типа как раз и выходит за рамки обычной mutex-style синхронизации.

Sign up to leave a comment.