Присвоение указателей атомарного типа указателям неатомарного типа - PullRequest
11 голосов
/ 06 апреля 2019

Является ли поведение этого кода хорошо определенным?

#include <stdatomic.h>

const int test = 42;
const int * _Atomic atomic_int_ptr;
atomic_init(&atomic_int_ptr, &test);
const int ** int_ptr_ptr = &atomic_int_ptr;
printf("int = %d\n", **int_ptr_ptr); //prints int = 42

Я назначил указатель на атомарный тип, чтобы указатель на неатомарный тип (типы:тот же самый).Вот мои мысли об этом примере:

Стандарт прямо определяет отличие квалификаторов const, volatile и restrict от квалификатора _Atomic 6.2.5(p27):

в настоящем стандарте явно используется фраза «атомарный, квалифицированный или неквалифицированный тип» всякий раз, когда допускается атомарная версия типа вместе с другими квалифицированными версиями типа.Фраза «квалифицированный или неквалифицированный тип», без конкретного упоминания атомарных, не включает атомарные типы.

Также совместимость квалифицированных типов определяется как 6.7.3(p10):

Для совместимости двух квалифицированных типов оба должны иметь идентично квалифицированную версию совместимого типа;порядок определителей типов в списке спецификаторов или определителей не влияет на указанный тип.

Комбинируя цитаты, приведенные выше, я пришел к выводу, что атомарные и неатомарные типы являются совместимыми типами.Таким образом, применяя правило простого присваивания 6.5.16.1(p1) (emp. Mine):

левый операнд имеет атомный, квалифицированный или неквалифицированный тип указателя и (учитывая тип, который левый операнд будет иметь послеПреобразование lvalue) оба операнда являются указателями на квалифицированных или неквалифицированных версий совместимых типов , а тип, указанный слева, имеет всеквалификаторы типа, указанного справа;

Итак, я пришел к выводу, что поведение хорошо определено (даже несмотря на присвоение атомарного типа неатомарному типу).

Проблема со всем тем, что, применяя вышеуказанные правила, мы также можем сделать вывод, что простое назначение неатомарного типа атомарному типу также хорошо определено, чтоочевидно, это не так, поскольку для этого у нас есть специальная общая функция atomic_store.

Ответы [ 2 ]

8 голосов
/ 06 апреля 2019

6.2.5p27

Далее есть спецификатор _Atomic. Наличие _Atomic классификатор обозначает атомарный тип. Размер, представление и выравнивание атомного типа не должно быть таким же, как у соответствующий неквалифицированный тип. Таким образом, этот стандарт явно использует фразу «атомарный, квалифицированный или неквалифицированный тип» всякий раз, когда атомарная версия типа допускается вместе с другими квалифицированными версии типа. Фраза «квалифицированный или неквалифицированный тип», без конкретного упоминания атомного, не включает атомные типы.

Я думаю, это должно прояснить, что атомарные типы не считаются совместимыми с квалифицированными или неквалифицированными версиями типов, на которых они основаны.

6 голосов
/ 06 апреля 2019

C11 позволяет _Atomic T иметь другой размер и расположение, чем T, например если это не без блокировки. (См. Ответ @ PSkocik).

Например, реализация может выбрать мьютекс внутри каждого атомарного объекта и поставить его первым. (Большинство реализаций вместо этого используют адрес в качестве индекса в таблице блокировок: Где находится блокировка для std :: atomic? вместо того, чтобы раздуть каждый экземпляр объекта _Atomic или std::atomic<T>, который не является не гарантируется блокировка во время компиляции).

Следовательно, _Atomic T* несовместим с T* даже в однопоточной программе.

Простое назначение указателя может быть не UB (извините, я не надевал свою языковую шапку адвоката), , но разыменование, безусловно, может быть .

Я не уверен, строго ли это UB в реализациях, где _Atomic T и T имеют одинаковую компоновку и выравнивание. Вероятно, это нарушает строгий псевдоним, если _Atomic T и T считаются разными типами, независимо от того, имеют ли они одинаковый макет.


alignof(T) может отличаться от alignof(_Atomic T), но, кроме преднамеренно извращенной реализации (Deathstation 9000), _Atomic T будет, по крайней мере, выровнен как обычный T, так что это не так проблема приведения указателей на объекты, которые уже существуют. Объект, который выровнен больше, чем нужно, является , а не проблемой, просто возможной пропущенной оптимизацией, если он не позволяет компилятору использовать одну более широкую загрузку.

Забавный факт: создание подчеркнутого указателя - это UB в ISO C, даже без разыменования. (Большинство реализаций не жалуются, а для встроенного в Intel _mm_loadu_si128 даже требуются компиляторы.)


На практике в реальных реализациях _Atomic T* и T* используют одинаковое представление макета / объекта и alignof(_Atomic T) >= alignof(T). Однопоточная или защищенная мьютексами часть программы может делать неатомарный доступ к объекту _Atomic, если вы можете работать с UB со строгим псевдонимом. Может быть с memcpy.

В реальных реализациях _Atomic может увеличить требование выравнивания, например, * struct {int a,b;} на большинстве ABI для большинства 64-битных ISA обычно имеет только 4-байтовое выравнивание (максимум членов), но _Atomic даст ему естественное выравнивание = 8, чтобы позволить загружать / хранить его с одним выровненным 64 загрузка / хранение. Это, конечно, не меняет расположение или выравнивание элементов относительно начала объекта, а только выравнивание объекта в целом.


Проблема со всем тем, что, применяя приведенные выше правила, мы также можем заключить, что простое присвоение неатомарного типа атомарному типу также хорошо определено, что, очевидно, неверно, поскольку для этого у нас есть специальная универсальная функция atomic_store.

Нет, это рассуждение ошибочно.

atomic_store(&my_atomic, 1) эквивалентно my_atomic=1;. В абстрактной машине C они оба делают атомарный склад с memory_order_seq_cst.

Вы также можете увидеть это, взглянув на код поколения для реальных компиляторов на любом ISA; например Компиляторы x86 будут использовать инструкцию xchg или mov + mfence. Точно так же shared_var++ компилируется в атомарный RMW (с mo_seq_cst).

IDK, почему есть универсальная функция atomic_store. Может быть, просто для контраста / согласованности с atomic_store_explicit, что позволяет вам сделать atomic_store_explicit(&shared_var, 1, memory_order_release) или memory_order_relaxed, чтобы сделать релиз или расслабленное хранилище вместо последовательного выпуска. (На x86, просто хранилище. Или на слабо упорядоченных ISA, некоторые ограждения, но не полный барьер.)


Для типов без блокировок, где объектные представления _Atomic T и T идентичны, на практике нет проблем с доступом к атомарному объекту через неатомарный указатель в однопоточной программе . Я подозреваю, что это все еще UB.

C ++ 20 планирует ввести std::atomic_ref<T>, который позволит вам выполнять атомарные операции с неатомарными переменными.(Без UB, если нет потоков, которые потенциально делают неатомарный доступ к нему в течение временного окна записи.) Это, в основном, оболочка для встроенных __atomic_* встроенных в GCC, например, что std::atomic<T> реализовано наtop of.

(Это создает некоторые проблемы, например, если atomic<T> требуется больше выравнивания, чем T, например, для long long или double в i386 System V. Или структура 2x intна большинстве 64-битных ISA. Вы должны использовать alignas(_Atomic T) T foo при объявлении неатомарных объектов, над которыми вы хотите выполнять атомарные операции.)

В любом случае, я не знаю ни одного совместимого со стандартами способачтобы делать подобные вещи в portable ISO C11, но стоит упомянуть, что настоящие компиляторы C в значительной степени поддерживают выполнение атомарных операций над объектами, объявленными без _Atomic. , но только с использованиемнапример, атомарные встроенные функции GNU C. :

См. Приведение указателей к _Атомным указателям и _Атомным размерам : очевидно, приведение T* к _Atomic T* не рекомендуется даже в GNU CАльхотя у нас нет однозначного ответа, что это на самом деле UB.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...