К сожалению, atomic<T>
в Linux (?) не выравнивает / не дополняет размер до степени двойки. std::atomic<Something> arr[10]
имеет sizeof(arr) = 30. (https://godbolt.org/z/WzK66xebr)
Используйте struct Something { alignas(4) char a; char b,c; };
(не alignas(4) char a,b,c;
, поскольку в этом случае каждый символ будет дополнен до 4 байтов, чтобы их можно было выровнять.)
Объекты с размером, отличным от степени двойки, могут пересекать границу строки кэша, поэтому использование более широкой 4-байтовой загрузки не всегда возможно.
Кроме того, чистые хранилища всегда должны использовать CAS (например, lock cmpxchg
), чтобы не изобретать записи в байт вне объекта: очевидно, вы не можете использовать два отдельных хранилища mov
(2 байта + 1 байт), потому что это не будет атомарный, если только вы не сделаете это внутри транзакции TSX с циклом повтора.
Загрузка/сохранение x86 гарантированно атомарны только для доступа к памяти, которые не пересекают 8-байтовую границу. (На некоторых поставщиках/архивах граница строки кэша. Или для возможно некэшируемых загрузок/хранилищ, в основном, вам нужно естественное выравнивание). Почему целочисленное назначение на естественно выровненном атомарная переменная на x86?
Ваш struct Something { char a, b, c; };
не требует выравнивания, поэтому нет правила C++, которое не позволяет объекту Something
занимать 2 строки кэша. Это сделало бы его простую загрузку/сохранение определенно неатомарной.
gcc и clang решили реализовать atomic<T>
с тем же макетом/представлением объектов, что и T
(независимо от того, не блокируется он или нет). Следовательно, atomic<Something>
— это 3-байтовый объект. Таким образом, массив atomic<Something>
обязательно имеет некоторые из этих объектов, перекрывающих границы строки кэша, и не может иметь отступы за пределами объекта, потому что массивы работают иначе в C. sizeof()
= 3 говорит вам о структуре массива. Это делает atomic<Something>
без блокировок невозможным. (Если только вы не загрузите/сохраните lock cmpxchg
, чтобы быть атомарным даже при разбиении строк кэша, что приведет к значительному снижению производительности в тех случаях, когда это произошло. Лучше заставить разработчиков исправить свою структуру.)
Класс atomic<T>
может иметь более высокие требования к выравниванию, чем T
, например, atomic<int64_t>
имеет alignof(atomic_int64_t) == 8, в отличие от alignof(int64_t) == 4
на многих 32-битных платформах (включая i386 System V ABI).
Если бы gcc/clang не принял решение сохранить макет прежним, они могли бы иметь atomic<T>
дополняющие маленькие объекты до следующей степени 2 и добавлять выравнивание, чтобы они могли быть без блокировок. Это был бы правильный выбор реализации. Я не могу думать ни о каких минусах.
Забавный факт, поддержка gcc C11 _Atomic
немного нарушена на 32-битных платформах с 64-битные безблокировочные атомарные объекты: _Atomic int64_t
могут быть смещены внутри структур, что приведет к разрыву. Они до сих пор не обновили ABI для типов _Atomic
, чтобы иметь естественное выравнивание.
Но в g++ C++11 std::atomic используется класс-шаблон в заголовке, который недавно исправил эту ошибку (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65147); гарантируя, что atomic<T>
имеет естественное выравнивание (до некоторой степени размера 2), даже если T
имеет выравнивание ‹ размера. Таким образом, они никак не могут охватить любую границу шире, чем они есть.
person
Peter Cordes
schedule
03.08.2019
§[atomics.types.generic]p3
разрешает это - Представление атомарной специализации не обязательно должно иметь тот же размер, что и соответствующий тип аргумента. Я полагаю, что с этим есть проблемы с переносимостью? - person Curious   schedule 29.04.2019sizeof(S)
никогда не бывает 3. - person Passer By   schedule 29.04.2019alignas(4)
. - person Nicol Bolas   schedule 29.04.2019