TL: DR да, LL / SC (STREX / LDREX) могут быть хороши для задержки прерывания по сравнению с отключением прерываний, делая атомное RMW прерываемым при повторной попытке.
Это может происходить за счет пропускной способности, потому что, очевидно, отключение / повторное включение прерываний в ARMv7 очень дешево (например, 1 или 2 цикла каждый для cpsid if
/ cpsie if
), особенно если вы можете безоговорочно включать прерывания вместо сохранения старого состояния. ( Временно отключить прерывания на ARM ).
Дополнительные затраты на пропускную способность: если LDREX / STREX медленнее, чем LDR / STR на Cortex-M4, cmp / bne (не принимается в успешном случае), и каждый раз, когда цикл должен повторить весь цикл тело снова бежит. (Повтор должен быть очень редким; только если прерывание действительно происходит, когда он находится в середине LL / SC в другом обработчике прерывания.)
Компиляторы C11, такие как gcc, не имеют специального режима для однопроцессорных систем или однопоточного кода, к сожалению . Поэтому они не знают, как создать код, который использует тот факт, что все, что работает на одном и том же ядре, будет видеть все наши операции в программном порядке до определенного момента, даже без каких-либо барьеров.
(Кардинальное правило неупорядоченного выполнения и переупорядочения памяти заключается в том, что оно сохраняет иллюзию однопоточных или одноядерных рабочих инструкций в программном порядке.)
Последовательные команды dmb
, разделенные только парой инструкций ALU, являются избыточными даже в многоядерной системе для многопоточного кода. Это gcc пропущенная оптимизация, потому что текущие компиляторы в основном не оптимизируют атомику. (Лучше быть безопасным и медленным, чем рисковать быть слишком слабым. Достаточно сложно рассуждать, тестировать и отлаживать код без блокировки, не беспокоясь о возможных ошибках компилятора.)
Атомика на одноядерном процессоре
В этом случае вы можете значительно упростить его, маскируя после и atomic_fetch_add
вместо имитации атомарного сложения с более ранними ролловерами с использованием CAS. (Тогда читатели тоже должны маскироваться, но это очень дешево.)
И вы можете использовать memory_order_relaxed
. Если вы хотите переупорядочить гарантии для обработчика прерываний, используйте atomic_signal_fence
для принудительного упорядочивания во время компиляции без asm-барьеров для переупорядочения во время выполнения. Сигналы пользовательского пространства POSIX асинхронны в одном и том же потоке точно так же, как прерывания асинхронны в одном и том же ядре.
// readers must also mask _head & (FIFO_LEN - 1) before use
// Uniprocessor but with an atomic RMW:
int32_t acquire_head_atomicRMW_UP(void)
{
atomic_signal_fence(memory_order_seq_cst); // zero asm instructions, just compile-time
int32_t old_h = atomic_fetch_add_explicit(&_head, 1, memory_order_relaxed);
atomic_signal_fence(memory_order_seq_cst);
int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
return new_h;
}
В проводнике компилятора Godbolt
@@ gcc8.2 -O3 with your same options.
acquire_head_atomicRMW:
ldr r3, .L4 @@ load the static address from a nearby literal pool
.L2:
ldrex r0, [r3]
adds r2, r0, #1
strex r1, r2, [r3]
cmp r1, #0
bne .L2 @@ LL/SC retry loop, not load + inc + CAS-with-LL/SC
adds r0, r0, #1 @@ add again: missed optimization to not reuse r2
ubfx r0, r0, #0, #10
bx lr
.L4:
.word _head
К сожалению, я не знаю, как в C11 или C ++ 11 можно выразить LL / SC атомарный RMW, который содержит произвольный набор операций, таких как add и mask, поэтому мы могли бы получить ubfx внутри цикла и части того, что хранится в _head
. Для LDREX / STREX существуют специфичные для компилятора особенности: Критические разделы в ARM .
Это безопасно, потому что _Atomic
целочисленные типы гарантированно будут дополнены 2 с четко определенным поведением overflow = wraparound. (int32_t
уже гарантированно будет дополнением 2, потому что это один из типов фиксированной ширины, но обход без UB только для _Atomic
). Я бы использовал uint32_t
, но мы получаем то же самое.
Безопасное использование STREX / LDREX из обработчика прерываний:
Примитивы синхронизации ARM® (с 2009 г.) содержит некоторые сведения о правилах ISA, которые управляют LDREX / STREX. Запуск LDREX инициализирует «эксклюзивный монитор» для обнаружения изменений другими ядрами (или другими процессорами, не относящимися к процессору, в системе? Я не знаю). Cortex-M4 - одноядерная система.
У вас может быть глобальный монитор для памяти, совместно используемой несколькими ЦП, и локальные мониторы для памяти, помеченной как не подлежащая обмену. Эта документация гласит: «Если регион, настроенный как Shareable, не связан с глобальным монитором, операции Store-Exclusive в этом регионе всегда завершаются неудачно, возвращая 0 в регистре назначения». Так что если STREX кажется, что всегда терпит неудачу (так что вы застреваете в цикле повтора), когда вы тестируете свой код, это может быть проблемой.
Прерывание не прерывает транзакцию, запущенную LDREX . Если вы переключали контекст в другой контекст и возобновляли что-то, что могло бы остановиться прямо перед STREX, у вас могла бы быть проблема. ARMv6K ввел clrex
для этого, иначе более старая ARM использовала бы фиктивный STREX для фиктивной локации.
См. Когда на самом деле нужен CLREX на ARM Cortex M7? , что говорит о том, что CLREX часто не требуется в ситуации прерывания, когда не контекстно-зависимый переключение между потоками.
Но для этой проблемы переключение на - это всегда запуск обработчика прерываний. Вы не делаете упреждающую многозадачность. Таким образом, вы никогда не сможете переключиться с середины одного цикла повторения LL / SC на середину другого. Пока STREX дает сбой в первый раз в прерывании с более низким приоритетом, когда вы возвращаетесь к нему, это нормально.
Это будет иметь место здесь, потому что прерывание с более высоким приоритетом будет возвращаться только после того, как оно выполнит успешный STREX (или вообще не будет делать никаких атомарных RMW).
Так что я думаю, что вы в порядке, даже не используя clrex
из встроенного asm или из обработчика прерываний перед отправкой в функции C. В руководстве говорится, что исключение Data Abort оставляет мониторы архитектурно неопределенными, поэтому убедитесь, что вы как минимум CLREX в этом обработчике.
Если прерывание возникает, когда вы находитесь между LDREX и STREX, LL загрузил старые данные в регистр (и, возможно, вычислил новое значение), но еще ничего не сохранил обратно в память, потому что STREX имел не работает.
Код с более высоким приоритетом будет LDREX, получая то же значение old_h
, а затем выполнит успешный STREX old_h + 1
. (Если оно также не прервано, но это рассуждение работает рекурсивно). Это может произойти сбой в первый раз через цикл, но я так не думаю. Даже если так, я не думаю, что может быть проблема правильности, основанная на документе ARM, который я связал. В документе упоминается, что локальный монитор может быть таким же простым, как конечный автомат, который просто отслеживает инструкции LDREX и STREX, позволяя STREX успешно выполняться, даже если предыдущая инструкция была LDREX для другого адреса. Предполагая, что реализация Cortex-M4 упрощена, это идеально подходит для этого.
Запуск другого LDREX для того же адреса, в то время как процессор уже отслеживает предыдущий LDREX, похоже, что это не должно иметь никакого эффекта. Выполнение монопольной загрузки на другой адрес вернет монитор в открытое состояние, но для этого он всегда будет иметь тот же адрес (если у вас нет другой атомики в другом коде?)
Затем (после выполнения некоторых других действий) обработчик прерываний вернется, восстановив регистры и вернувшись к середине цикла LL / SC прерывания с более низким приоритетом.
Возвращаясь к прерыванию с более низким приоритетом, STREX не будет работать, потому что STREX в прерывании с более высоким приоритетом сбрасывает состояние монитора. Это хорошо, нам нужно , чтобы он потерпел неудачу, потому что он сохранил бы то же значение, что и прерывание с более высоким приоритетом, которое заняло свое место в FIFO. cmp
/ bne
обнаруживает сбой и снова запускает весь цикл. На этот раз он успешен (если не прерван снова ), считывает значение, сохраненное прерыванием с более высоким приоритетом, и сохраняет и возвращает это + 1.
Так что я думаю, что мы можем обойтись без CLREX в любом месте, потому что обработчики прерываний всегда выполняются до завершения, прежде чем вернуться к середине чего-то, что они прервали. И они всегда начинаются с самого начала.
Версия для одного писателя
Или, если ничто иное не может изменить эту переменную, вам вообще не нужен атомарный RMW, только атомарная загрузка, а затем атомарное хранилище нового значения. (_Atomic
для пользы или любых читателей).
Или, если никакой другой поток или прерывание вообще не затрагивают эту переменную, это не обязательно должно быть _Atomic
.
// If we're the only writer, and other threads can only observe:
// again using uniprocessor memory order: relaxed + signal_fence
int32_t acquire_head_separate_RW_UP(void) {
atomic_signal_fence(memory_order_seq_cst);
int32_t old_h = atomic_load_explicit(&_head, memory_order_relaxed);
int32_t new_h = (old_h + 1) & (FIFO_LEN - 1);
atomic_store_explicit(&_head, new_h, memory_order_relaxed);
atomic_signal_fence(memory_order_seq_cst);
return new_h;
}
acquire_head_separate_RW_UP:
ldr r3, .L7
ldr r0, [r3] @@ Plain atomic load
adds r0, r0, #1
ubfx r0, r0, #0, #10 @@ zero-extend low 10 bits
str r0, [r3] @@ Plain atomic store
bx lr
Это то же самое, что мы получили бы для неатомных head
.