Linux, C, GCC, ARM (Allwinner H3). Можно ли получить следующий эквивалентный функционал (псевдокод):
#pragma optimizations-stop
#pragma registers-flush
a=10;
while(*b != 0) c++;
if (a == 0) { do something; }
#pragma optimizations-start
если результат местоположения a
явно записан со значением 10, то значение каждого цикла цикла b
считывается из памяти, а затем считанное значение из местоположения b
сравнивается с 0, а затем, если не ноль, c
считывается с +1 , а когда цикл прерывается, местоположение a
явно считывается из памяти и сравнивается с 0?
И, что важно, инструкции выполняются в том порядке, в котором программист заложил их в программе, а не в том порядке, в котором компилятор или процессор считают лучшим.
Пожалуйста, воздержитесь от обсуждения того, что невозможно, давайте обсудим, как добиться необходимого результата.
Если это невозможно на уровне C/GCC, я буду рад предложениям реализовать это как встроенную сборку.
Конечно, нет. Смотрите вопрос, который я задал здесь stackoverflow.com/questions/78722496/…. Кроме того, я узнал, что сам процессор (особенно ARM) будет решать за меня, как выполнять МОЮ программу.
Хм. Как отмечают комментарии, volatile
не выполняет синхронизацию, необходимую для многопоточности. Тем не менее, volatile
гарантирует, что каждая переменная читается и записывается так, как указано в вашей программе. Причина, по которой параллельный доступ не определен четко, связана с тем, где эти переменные хранятся в многопроцессорных архитектурах (упрощенно).
В любом случае: если ваш вопрос: «Как отключить кеш ЦП и предотвратить предварительную выборку инструкций и т. д.?» тогда я думаю, что ответ: вы не можете. (Хотя я могу ошибаться)
Есть три разных проблемы, которые нужно разделить: 1) предотвратить оптимизацию компилятора, 2) предотвратить ошибки состояния гонки и 3) предотвратить переупорядочение инструкций. volatile
определенно делает 1), определенно не делает 2) и может или не может 3) в зависимости от того, насколько буквально читается стандарт C. Самый разумный способ прочитать стандарт состоит в том, что реализации (компилятор + система) не разрешено выполнять переупорядочение через изменчивый барьер доступа = памяти. Однако некоторые решили прочитать стандарт, поскольку непостоянный доступ не может быть переупорядочен из-за другого побочного эффекта.
Я разместил ответ ниже другой ваш вопрос, касающийся этих трех отдельных проблем.
#pragma GCC optimize(…)
, который отвечает на вопрос заголовка. Мне не нужно было использовать «большой молот» (это Мьёльнир?), поэтому я не уверен, лучше это, хуже или просто отличается от прагмы. IME, прагма обычно используется в отношении сомнительного кода, который неправильно обрабатывается компилятором — часто это целая функция, иногда несколько функций, иногда часть функции.
«Кроме того, я узнал, что сам процессор (особенно ARM) будет решать за меня, как выполнять МОЮ программу». Да, именно так работают современные процессоры, и во многом именно поэтому они работают так быстро. Если вы категорически против этой концепции, то лучшим выбором, вероятно, будет машина времени, позволяющая вернуться в 1982 год.
«Большой молоток» — это расширение GCC, asm __volatile__("": : :"memory")
. Это полный барьер памяти, используемый ядром Linux.
Обратите внимание, что это не делает то, что вы просите в заголовке. Это совершенно не мешает оптимизации. Однако он достигает того порядка, который вы описываете.
Также обратите внимание, что c++
— это чтение-изменение-запись. Он не атомарный, и между частями нет барьеров памяти. Компилятор может разумно хранить c
в регистре и выполнять в реестре чтение-изменение-запись. Чтобы избежать этого, вам понадобится что-то вроде volatile int* pc = &c;
.
Наконец, если принять во внимание ваш другой пост, барьер памяти в одном потоке не заставит волшебным образом сотрудничать другой поток. Вам понадобятся барьеры на обоих потоках. И именно поэтому они редки — вместо этого обычно можно использовать правильную синхронизацию.
Можно ли использовать atomic_thread_fence(memory_order_seq_cst);
(#include <stdatomic.h>
) в качестве барьера памяти?
«чтобы избежать этого, вам понадобится что-то вроде volutous int* pc = &c;»: даже если доступ осуществляется через volatile
, я не думаю, что существует какая-либо гарантия, что приращение будет выполнено за одну комбинированную загрузку/сохранение. Если я правильно помню, между GCC и Clang были даже расхождения в реализации.
@user17732522 user17732522: Верно. Вы запрещаете только часть «хранить в регистре при увеличении». Он определенно не атомарный и не синхронизируется с другими потоками.
asm __volatile__("": : :"memory")
кажется, это всего лишь барьер переупорядочения во время компиляции. Никаких инструкций по синхронизации памяти он не добавляет (godbolt.org/z/3z6o3h9xb). Поэтому я не уверен, достаточно ли этого для решения проблем ОП. На x86 это будет вести себя как получение/выпуск, но не как последовательная согласованность. Этого достаточно для ОП, но, например. на ARM я не думаю, что это дает достаточные гарантии порядка памяти.
Использование _Atomic
для всех рассматриваемых переменных позволит достичь этого:
Каждое чтение и запись в переменную _Atomic
будет соответствовать фактической выполняемой инструкции загрузки или сохранения. (Хотя это не является формальной гарантией стандарта C, на практике компиляторы не пытаются его оптимизировать.)
Компилятор не будет переупорядочивать эти загрузки и хранилища.
Инструкции барьера памяти будут вставлены там, где это необходимо, чтобы предотвратить любое изменение порядка ЦП и гарантировать, что они соблюдаются в том же порядке всеми остальными потоками/ЦП, которые также используют соответствующие барьеры. (По умолчанию порядок памяти для атомарности — последовательная согласованность. Чтобы избежать любого из этих барьеров памяти, вам нужно использовать такие функции, как atomic_load_explicit
).
c++
будет выполняться с помощью атомарной инструкции чтения-изменения-записи или последовательности инструкций (например, ARM LDREX/STREX
), так что никакая запись в c
не может быть выполнена каким-либо другим потоком между чтением и записью.
Доступ к переменным, отличным от _Atomic
, не будет переупорядочен ни после сохранения, ни перед загрузкой. (Вы не можете предотвратить переупорядочение в других направлениях, кроме как путем создания других переменных _Atomic
.)
Обратите внимание, что использование volatile
, как предложено в другом месте, достигает 1 и 2, но обычно не 3, 4, 5. Более того, любой несинхронизированный одновременный доступ нескольких потоков к переменной, которая не является _Atomic
, даже если это volatile
, вызывает неопределенное поведение для вся программа по стандарту C (гонка данных).
Это довольно просто, объявив
a
,b
иc
какvolatile
. Это то, что вам нужно?