Давайте обманем атомик. Вот две функции - можно ли, общаясь к атомику только с их помощью, увидеть некорректное состояние ?
void write(std::atomic<int64_t>& x, int64_t v) { x.store(v, std::memory_order_seq_cst); } int64_t read(std::atomic<int64_t>& x) { return x.load(std::memory_order_seq_cst); }
Пишем тест:
1-ый тред упорно пишет -1 в атомик (то есть все биты равны единице)
2-ой тред пишет 2 (то есть ровно 1 бит - второй младший равен единице)
3-ий тред читает и стопает программу, если прочитает нечётное отрицательное число
void test(std::atomic<int64_t>& x) { constexpr size_t ITERS = 100'000'000; std::thread writePart1([&x]() { for(size_t i = 0; i < ITERS; ++i) { write(x, -1); } }); std::thread writePart2([&x]() { for(size_t i = 0; i < ITERS; ++i) { write(x, 2); } }); std::thread reader([&x]() { for(size_t i = 0; i < ITERS; ++i) { int64_t r = read(x); if ((r & 1) == 0 && r < 0) { std::cout << "found non atomic behaviour, iter=" << i/1e6 << "m" << std::endl; exit(0); return; } } }); writePart1.join(); writePart2.join(); reader.join(); std::cout << "finish, not found non atomic behaviour" << std::endl; }
И, с помощью легко пропускаемой ошибки, эта программа у меня стабильно падает где-то за 7-10млн итераций чтения
+ clang++ -std=c++20 main.cpp -o nonatomic.exe -Wall -O2 -DNDEBUG + ./nonatomic.exe found non atomic behaviour, iter=8.15546m
И вот, пора переходить к разгадке
int main(int argc, const char* _ []) { constexpr size_t alignment = 4096; char* buf [alignment * 3]; size_t ptr = size_t(buf) / alignment * alignment + alignment - 4; std::atomic<int64_t>* nonAligned = new((void*)ptr) std::atomic<int64_t>(0); std::atomic<int64_t> aligned; std::atomic<int64_t>& x = (argc == 1) ? *nonAligned : aligned; test(x); return 0; }
Если не совершать ошибку (запустится с любым аргументом) то не промежуточного стейта мы не увидим
+ ./nonatomic.exe aligned finish, not found non atomic behaviour
Думаю уже понятно, что происходит - мы располагаем наш атомик на 8 байт по середине двух страниц.
В частности - мы располагаем старшие и младшие 4 биты в разных кеш-линиях
А алгоритмы синхронизации в процессоре между ядрами работают только в рамках кеш-линий.
Что же конкретное мы нарушили? Мы нарушили выравнивание объекта. alignof(std::atomic<int64_t>) == 8, а нарушение алайнмента - UB.
Как же посадить такую ошибку? Я точно видел такую ошибку в районе попытки реализовывать что-то вроде арен или memory-pool-ов.
Выделяют память, выдают её кусками, а про выравнивание при выделении забыли (это встроенные new сделают как надо, ибо знают тип, а malloc или самописные функции получения буфера из мемори-пула, этого не делают)
UPD: malloc возвращает максимально выровненный адрес во всех нормальных реализациях
Что ещё интересного можно заметить? Давайте посмотрим в godbold
https://godbolt.org/z/j5jP8ozEb
сами операции чтения и записи не требуют выравнивания (мы не получаем sigill, хотя например с simd инструкциями это легко случается даже на x86)
изменение memory-order ничего не меняет (видимо, можно попытаться заявить, что в случае x86 memory-order аргументы прежде всего описывают ограничений перестановки инструкций компилятором, но не порождают дополнительных явных инструкций (сброса буферов чтения/записи например))
компиляция функции про чтение вообще ничего про атомарность не содержит, а вот запись использует спец функцию xchg, про которую и написано что оно будет реализовывать внутрипроцессорный алгоритм блокировок
Комментарии (10)

kibb
20.02.2026 06:36Есть бит RFLAGS.AC, который вызывает исключение #AC при невыравненном доступе. Им пользоваться невозможно, потому что почти все компиляторы (не только C) генерируют код, нарушающий это правило.
В сравнительно новых интеловских процессорах появился MSR_MEMORY_CTRL.SPLIT_LOCK_DISABLE, разрешение которого приводит к #AC при невыравненных locked доступах.
В вашем примере это бы сработало из-за иначе ненужного seq-cst режима. Если бы использовался relaxed или acq/rel, то на x86 они обычно моделируются простыми load и store, и split lock detection бы не сработал.
P.S. Явление называется 'torn writes' и в дикой природе встречается начиная с Core2 (~2008 г).

ilnurKh Автор
20.02.2026 06:36Генерируемый асемблер в годболте приложенном не меняется при смене мемори ордеров

kibb
20.02.2026 06:36Для load'ов да, plain load будет seq-cst если store с lock-префиксом. gcc генерирует XCHG для записи, эта инструкция с неявным префиксом lock.
Но собственно, это ничего в моем ответе не меняет.

black_warlock_iv
20.02.2026 06:36а malloc <...> этого не делают
Делает. Он возвращает всегда максимально выровненный указатель.
unreal_undead2
У меня она стабильно сразу выдаёт
Ваша x86 архитектура позволяет слишком много вольностей )
PS -fsanitize=undefined явно детектит проблему и в gcc и в clang.
ilnurKh Автор
а в режиме бин трансляции если таргет платформа была x86 что будет?
емнип, ошибку при невыровненном чтении на x86 тоже можно получить, если в какую-нибудь avx инструкцию, требующую выравнивание, подать невыровненный адрес
А так, да - дебажить bus-error проще чем что-то стреляющее с такой малой вероятностью (и космические лучи обвинишь, ища разгадку или репродьюс)
unreal_undead2
Тут уже от транслятора зависит, сконвертирует он один атомик в другой или навернёт логику с локами. Под рукой ничего нет чтобы быстро проверить.
Да, SIMD на x86 часто требует выравнивания (хотя есть, скажем, movups).
Согласен. Но санитайзер тоже порадовал, надо бы почаще использовать...
ilnurKh Автор
ubsan ловит, да
у себя ubsan чёт не удаётся завести из-за того что очень много в зависимостях надо пограничного вычищать, а ещё очень долгий
но мысли сделать хотя бы "раз в неделю" запуски приходят