Приведение указателей к _атомным указателям и _атомным размерам

#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. Атомарно получить указатель на нужную запись и заменить его статически определенным «занятым»
  2. Проверьте, был ли он уже «занят», если да, вращайте и повторяйте попытку, пока мы не получим «не занят».
  3. Теперь у нас есть уникальный доступ к этой записи, поэтому обновите ее.
  4. Замените указатель «занято» на исходный.

Таким образом, шаг (1) блокирует, а шаг (4) разблокирует, и, в отличие от метода критической секции, access должен ждать, только если два потока пытаются получить доступ к одному и тому же адресу. Кажется, это работает, и в моей 6-ядерной системе (с гиперпоточностью, то есть с 12 потоками) это примерно в 5 раз быстрее, чем использование одного критического раздела при работе с реальным набором данных.

Так почему бы в первую очередь не определить указатель на запись как атомарный?. Ответ заключается в том, что этот конкретный код может предоставлять непоточный доступ к этой информации в других местах, и он также может предоставлять потоковый доступ способом, который, как известно, является непредусмотренным; фактически, в большинстве ситуаций я не хочу иметь механизм блокировки из-за его стоимости. Временные тесты показывают, что типичная операция атомной блокировки / разблокировки в моей системе занимает от 5 до 10 наносекунд, и я хочу избежать этих накладных расходов, когда мне это не нужно, поэтому в таких ситуациях я просто использую необработанный указатель.

Я предлагаю это как способ, которым я решил эту конкретную проблему. Я знаю, что это неправильный C11, я знаю, что он может работать только в архитектуре типа x86 — или, по крайней мере, только в архитектурах, где целочисленные типы и типы указателей не имеют блокировок и «по сути атомарны» — и я также признаю, что, вероятно, есть лучшие способы блокировки данного адреса, если вы знаете, как писать на ассемблере (чего я не знаю). Я был бы рад услышать о лучшем решении.

Кстати, я также попробовал транзакционную память (т. Е. _xbegin() .. _xend ()) как способ решения этой проблемы. Я обнаружил, что это работает с небольшими проблемами при тестировании, но как только я увеличил его до реальных данных, я получил частые сбои _xbegin (), и я думаю, это было потому, что, когда адреса, к которым вы обращаетесь, не находятся в кэш-памяти, это имеет тенденцию выходить из строя, заставляя вас использовать запасной путь к коду. Intel не очень откровенна в деталях того, как это работает, поэтому это объяснение может быть неправильным.

Я также рассмотрел устранение аппаратной блокировки как способ ускорения метода критической секции, но, насколько я вижу, он устарел из-за уязвимости к взломам .. и в любом случае, я был слишком туп, чтобы понять, как его использовать!