#c #gcc #clang #atomic #c11
#c #gcc #лязг #атомарный #c11
Вопрос:
Согласно моему прочтению стандарта, *(_Atomic TYPE*)amp;(TYPE){0}
(словами, приведение указателя на неатомный элемент к указателю на соответствующий атомарный элемент и разыменование) не поддерживается.
Распознают ли gcc и / или clang это как расширение, если TYPE
оно не заблокировано? (Вопрос 1)
Второй и связанный с ним вопрос: у меня создалось впечатление, что если TYPE
не может быть реализован как атомарный без блокировки, блокировка должна быть встроена в соответствующий _Atomic TYPE
. Но если я создам TYPE
большую структуру, то для обоих clang
и gcc
она будет иметь тот же размер, что и _Atomic TYPE
.
Код для обеих проблем:
#include <stdatomic.h>
#include <stdio.h>
#if STRUCT
typedef struct {
int x;
char bytes[50];
} TYPE;
#else
typedef int TYPE;
#endif
TYPE x;
void f (_Atomic TYPE *X)
{
*X = (TYPE){0};
}
void use_f()
{
f((_Atomic TYPE*)(amp;x));
}
#include <stdio.h>
int main()
{
printf("%zu %zun", sizeof(TYPE), sizeof(_Atomic TYPE));
}
Теперь, если я скомпилирую приведенный выше фрагмент с помощью -DSTRUCT
, и gcc, и clang сохранят структуру и ее атомарный вариант с одинаковым размером, и они генерируют вызов функции с именем __atomic_store
для хранилища (решается путем связывания с -latomic
).
Как это работает, если в _Atomic
версию структуры не встроена блокировка? (Вопрос 2)
Ответ №1:
_Atomic
изменяет выравнивание в некоторых угловых случаях на Clang, и GCC, вероятно, также будет исправлен в будущем (PR 65146). В этих случаях добавление _Atomic
с помощью приведения не работает (что нормально со стандартной точки зрения C, потому что это неопределенное поведение, как вы указали).
Если выравнивание правильное, более уместно использовать __atomic
встроенные компоненты, которые были разработаны именно для этого варианта использования:
Как описано выше, это не будет работать в случаях, когда ABI обеспечивает недостаточное выравнивание для простых (неатомных) типов, и где _Atomic
изменилось бы выравнивание (только с Clang на данный момент).
Эти встроенные функции также работают в случае неатомных типов, поскольку они используют встроенные блокировки. Это также причина, по которой для _Atomic
типов, которые используют тот же механизм, не требуется дополнительное хранилище. Это означает, что возникает некоторая ненужная конкуренция из-за непреднамеренного совместного использования блокировок. То, как реализованы эти блокировки, является деталью реализации, которая может измениться в будущих версиях libatomic
.
В общем случае, для типов с атомарными встроенными элементами, которые включают блокировку, их использование с сопоставлениями памяти с общим доступом или псевдонимами не работает. Эти встроенные компоненты также не являются безопасными для асинхронного сигнала. (В любом случае, все эти функции технически выходят за рамки стандарта C.)
Ответ №2:
Этот метод не является законным C11, но мне удалось обмануть мой компилятор (Intel 2019) при приведении между атомарными и неатомными «простыми» типами следующим образом.
Сначала я заглянул в stdatomic.h в моей системе (x86_64), чтобы увидеть, каким на самом деле было фактическое определение различных атомарных типов. Насколько я мог разобрать для простых целых типов и указателей, атомарный тип был идентичен обычному типу, и, более того, они были явно «без блокировки».
Следующим шагом было использование оператора sizeof(), чтобы увидеть, сколько байт фактически использовали атомарные типы, и снова я обнаружил, что атомарный int равен 4 байтам, а атомарный указатель равен 8 — как я и ожидал в 64-разрядной системе.
Компилятор запретил явное приведение, но это сработало:
typedef struct { void *ptr; } IS_NORMAL;
typedef struct { atomic_address ptr; } IS_ATOMIC;
IS_NORMAL a;
IS_ATOMIC *b = (IS_ATOMIC *)amp;a;
a.ptr = <address>
/* then inspection in the debugger shows that b->ptr is also <address> */
Это, к счастью, позволило бы мне выполнять приведение между этими двумя типами структур, как показано выше, и когда я использовал атомарные функции (например, atomic_exchange ()) в варианте указателя IS_ATOMIC, мой отладчик показал мне, что содержимое адреса неатомной структуры изменилось на ожидаемое значение.
В этот момент вы можете спросить «зачем это делать?» Ответ заключается в том, что у меня есть многопоточное приложение, в котором я хочу заблокировать запись базы данных на короткий период времени, чтобы один поток мог обновить ее без вмешательства других потоков, а затем снять блокировку, когда я закончу. Исторически я защищал эту операцию с помощью критической секции, но это очень пессимистично, поскольку у меня может быть, скажем, 10 000 000 записей, и я буду обновлять их случайным образом, поэтому вероятность того, что два потока действительно попытаются обновить одну и ту же запись, довольно мала, но критическая секция безоговорочно блокирует все потоки. На каждую запись ссылается указатель, поэтому процесс:
- Атомарно получить указатель на нужную запись и заменить его статически определенным «занятым»
- Проверьте, был ли он уже «занят», если да, вращайте и повторяйте попытку, пока мы не получим «не занят».
- Теперь у нас есть уникальный доступ к этой записи, поэтому обновите ее.
- Замените указатель «занято» на исходный.
Таким образом, шаг (1) блокирует, а шаг (4) разблокирует, и, в отличие от метода критической секции, access должен ждать, только если два потока пытаются получить доступ к одному и тому же адресу. Кажется, это работает, и в моей 6-ядерной системе (с гиперпоточностью, то есть с 12 потоками) это примерно в 5 раз быстрее, чем использование одного критического раздела при работе с реальным набором данных.
Так почему бы в первую очередь не определить указатель на запись как атомарный?. Ответ заключается в том, что этот конкретный код может предоставлять непоточный доступ к этой информации в других местах, и он также может предоставлять потоковый доступ способом, который, как известно, является непредусмотренным; фактически, в большинстве ситуаций я не хочу иметь механизм блокировки из-за его стоимости. Временные тесты показывают, что типичная операция атомной блокировки / разблокировки в моей системе занимает от 5 до 10 наносекунд, и я хочу избежать этих накладных расходов, когда мне это не нужно, поэтому в таких ситуациях я просто использую необработанный указатель.
Я предлагаю это как способ, которым я решил эту конкретную проблему. Я знаю, что это неправильный C11, я знаю, что он может работать только в архитектуре типа x86 — или, по крайней мере, только в архитектурах, где целочисленные типы и типы указателей не имеют блокировок и «по сути атомарны» — и я также признаю, что, вероятно, есть лучшие способы блокировки данного адреса, если вы знаете, как писать на ассемблере (чего я не знаю). Я был бы рад услышать о лучшем решении.
Кстати, я также попробовал транзакционную память (т. Е. _xbegin() .. _xend ()) как способ решения этой проблемы. Я обнаружил, что это работает с небольшими проблемами при тестировании, но как только я увеличил его до реальных данных, я получил частые сбои _xbegin (), и я думаю, это было потому, что, когда адреса, к которым вы обращаетесь, не находятся в кэш-памяти, это имеет тенденцию выходить из строя, заставляя вас использовать запасной путь к коду. Intel не очень откровенна в деталях того, как это работает, поэтому это объяснение может быть неправильным.
Я также рассмотрел устранение аппаратной блокировки как способ ускорения метода критической секции, но, насколько я вижу, он устарел из-за уязвимости к взломам .. и в любом случае, я был слишком туп, чтобы понять, как его использовать!