Побайтовая атомарная память и блокировки последовательности в C++23

Я хочу реализовать блокировку последовательности в C++23. По возможности не следует полагаться на нестандартные расширения или неопределенное поведение.

Есть предложение P1478R8: Побайтовая атомарная память memcpy, которая точно описывает мой вариант использования. Это предложение предлагает добавить atomic_load_per_byte_memcpy и atomic_store_per_byte_memcpy в новый заголовок bytewise_atomic_memcpy, который может копировать побайтово, используя атомарную семантику.

Как правильно реализованы блокировки последовательностей в C++ до C++23? Как сейчас реализовать функции P1478? Я не нашел эталонной реализации предложения, а также никакой другой реализации блокировки последовательности, которая специально решала бы эту проблему. Конечно, я мог бы реализовать это вручную, но это, вероятно, не привело бы к лучшей производительности, как при простой реализации memcpy. Есть ли лучший способ?

У меня такое ощущение, что, хотя по стандарту C++ это неопределенное поведение, в реальной жизни проблему часто игнорируют и просто используют простой memcpy.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
0
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как правильно реализованы блокировки последовательностей в C++ до C++23?

Это не так, если только все заблокированные переменные сами не являются atomic.

Как сейчас реализовать функции P1478?

Вы не можете.

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

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

В этом весь смысл P1478 - позволить это.

У меня такое ощущение, что, хотя это неопределенное поведение согласно стандарту C++, в реальной жизни проблема часто игнорируется и просто используется простой memcpy.

Да, и эти реализации могут сломаться в любой момент.

Что ж, но секлокки определенно полностью поддерживаются и надежны как минимум на одной платформе/компиляторе/архитектуре. Так что же они делают?

Sneftel 22.08.2024 23:25

@Sneftel О какой платформе/компиляторе/архитектуре вы говорите? Сегодня вы можете писать блокировки последовательности непосредственно на ассемблере, но не на C++.

orlp 22.08.2024 23:26

@Sneftel: некоторые компиляторы определяют поведение volatile достаточно строго, поэтому использование его для полезной нагрузки может иметь реальные гарантии. На практике это определенно работает с известными компиляторами, поскольку в реальных компиляторах, таких как GCC, барьеры памяти могут упорядочивать загрузку или сохранение. более ранний или поздний неатомарный доступ, даже способами, которые не являются выпуском или приобретением. См. Реализация 64-битного атомного счетчика с 32-битными атомами, где описана моя попытка реализовать его, который, на мой взгляд, достаточно надежен в GNU C или C++.

Peter Cordes 23.08.2024 01:33

@Sneftel: см. также Изменение порядка GCC при загрузке с помощью memory_order_seq_cst. Это разрешено? (ошибка GCC, затрагивающая SeqLocks) и Законно ли преобразование fetch_add(0, Memory_order_relaxed/release) в mfence + mov? (пример гипотетических или реальных оптимизаций компилятора, которые могут повлиять на секлок)

Peter Cordes 23.08.2024 01:34

@Sneftel: Но да, orlp прав, единственный способ написать секлок без UB — это сделать так, чтобы полезная нагрузка состояла из нескольких элементов std::atomic<long>, которые вы копируете с помощью memory_order_relaxed. Это не позволит компиляторам использовать более широкую копию (например, x86 movdqa для захвата 16 байт за раз). Если вы знаете, что целевая ISA, например. 32-битный ARM и полезная нагрузка 64-битная, вы можете выбрать 32-битный размер элемента, так как вам, вероятно, нужна полезная нагрузка в паре целочисленных регистров. Но в целом это отстой, не позволяющий оптимизировать один 64-битный регистр на 64-битных ISA для uint64_t.

Peter Cordes 23.08.2024 01:38

@Peter Cordes: Но не нарушит ли это правила псевдонимов типов, если мой настоящий тип тоже не длинный?

sedor 23.08.2024 17:24

@Peter Cordes: Учитывая, что у нас x86_64, будет ли проблемой загрузка 16 байт? Я думаю, что аппаратное обеспечение с этим справляется, единственное, что это UB для C++, и могут сработать потенциально опасные оптимизации компилятора.

sedor 23.08.2024 17:28

@sedor: 16-байтовая загрузка гарантирована атомарно только на процессорах с AVX. Таким образом, в GCC или clang std::atomic<__int128>::load(relaxed) приводит к вызову функции биатомной библиотеки, которая использует vmovaps или аналогичный, если эта функция ЦП доступна, в противном случае используется lock cmpxchg16b. А в некоторых реализациях (например, MSVC) std::atomic<> для 16-байтовой структуры используется фактическая блокировка. (GCC7 и более поздние версии сообщают, что он не защищен от блокировки, поскольку резервный вариант lock cmpxchg16b не имеет ожидаемой производительности, такой как масштабируемость на стороне чтения, но версия GCC технически не защищена от блокировки.)

Peter Cordes 23.08.2024 19:30

@sedor: Re: строгое псевдонимирование: верно, вам следует tmp[i] = payload[i].load(relaxed); скопировать в локальный tmp-массив элементов uint32_t, а затем memcpy оттуда в вашу фактическую структуру полезных данных. (memcpy безопасен со строгим псевдонимом, например std::bit_cast). Или сделайте что-нибудь вручную для более простых случаев, например (uint64_t(high) << 32) | low, чтобы объединить половинки u32 с u64.

Peter Cordes 23.08.2024 19:33

@sedor: поскольку компиляторы оптимизируют 4 соседние std::atomic<uint32_t> загрузки в одну 16-байтовую загрузку, это теоретически разрешено для целей, где 16-байтовая выровненная загрузка гарантированно атомарна. Но, к сожалению, в руководствах Intel или AMD нет документированной гарантии относительно атомарности векторной загрузки/сохранения и сбора/рассеивания для каждого элемента? хотя на самом деле неправдоподобно иметь разрывы внутри 4-байтовых границ, когда они являются частью более широкой нагрузки.

Peter Cordes 23.08.2024 19:38

И тем не менее, компиляторы в настоящее время не оптимизируют атомарность. См. Почему компиляторы не объединяют избыточные записи std::atomic? - то, как они не оптимизируются, очень похоже на то, как настоящие компиляторы справляются с volatile: они не объединяют непрерывный доступ и не удаляют мертвые хранилища даже без промежуточных операций чтения.

Peter Cordes 23.08.2024 19:40

@Peter Cordes: Но может ли неатомарность 16-байтовых загрузок быть проблемой? Результат может оказаться поддельным, но это будет обнаружено секлокировкой. Что касается аппаратной части, то если она не вызывает исключения, все должно быть в порядке, не так ли? Что касается компилятора, нам нужно защититься от UB. Или спросить иначе: если я буду использовать 16-байтовую загрузку через встроенную сборку, это будет нормально?

sedor 23.08.2024 21:50

@sedor: для секлока нет, это не может быть проблемой. Но std::atomic<T> или std::atomic_ref<T> — наш единственный способ избежать UB гонок данных для полезной нагрузки в текущем C++. Конечно, вы можете делать все, что эффективно, если писать секлокку на ассемблере или использовать расширения C++, специфичные для компилятора, такие как встроенный asm или семантику volatile (которая в GNU C поддерживается для подобных вещей; именно так ядро ​​Linux реализует свою собственную реализацию). атомика.)

Peter Cordes 24.08.2024 04:26

Спасибо за все ссылки, очень интересно читать.

sedor 24.08.2024 10:33

Другие вопросы по теме