将指针转换为 _Atomic 指针和 _Atomic 大小

Casting pointers to _Atomic pointers and _Atomic sizes

根据我对标准的阅读, *(_Atomic TYPE*)&(TYPE){0}(换句话说,将指向非原子的指针转换为指向相应原子的指针并取消引用)不受支持。

如果 TYPE is/isn 不是无锁的,gcc and/or clang 是否将其识别为扩展? (问题 1)

第二个相关问题:我的印象是,如果 TYPE 不能实现为无锁原子,则需要在相应的 _Atomic TYPE 中嵌入一个锁。但是如果我使 TYPE 成为一个较大的结构,那么在 clanggcc 上它的大小与 _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*)(&x));
}

#include <stdio.h>
int main()
{
    printf("%zu %zu\n", sizeof(TYPE), sizeof(_Atomic TYPE));
}

现在,如果我用 -DSTRUCT 编译上面的代码片段,gcc 和 clang 都会将结构及其原子变体保持在相同的大小,并且它们会生成对名为 [=20 的函数的调用=] 用于商店(通过与 -latomic 链接解决)。

如果 _Atomic 版本的结构中没有嵌入锁,这将如何工作? (问题 2)

_Atomic 更改了 Clang 上某些极端情况下的对齐方式,并且 GCC 将来也可能会得到修复 (PR 65146)。在这些情况下,通过强制转换添加 _Atomic 不起作用(从 C 标准的角度来看这很好,因为它是未定义的行为,正如您所指出的)。

如果对齐正确,使用 __atomic 内置函数更合适,它正是为这个用例设计的:

如上所述,如果 ABI 为普通(非原子)类型提供的对齐方式不足,并且 _Atomic 会更改对齐方式(目前仅使用 Clang),这将不起作用。

这些内置函数也适用于非原子类型,因为它们使用外联锁。这也是为什么 _Atomic 类型不需要额外存储的原因,它们使用相同的机制。这意味着由于无意中共享锁而导致一些不必要的争用。这些锁是如何实现的是一个实现细节,可能会在 libatomic.

的未来版本中发生变化

一般来说,对于具有涉及锁定的原子内置函数的类型,将它们与共享或别名内存映射一起使用是行不通的。这些内置函数也不是异步信号安全的。 (无论如何,所有这些功能在技术上都超出了 C 标准。)

此方法在 C11 中不合法,但我设法欺骗了我的编译器(Intel 2019),使其在原子和非原子 "simple" 类型之间进行转换,如下所示。

首先,我在我的系统 (x86_64) 上查看了 stdatomic.h,以了解各种原子类型的实际定义到底是什么。据我所知,对于简单的整数类型和指针,原子类型与普通类型相同,而且它们是明确的 "lock free".

下一步是使用 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 *)&a;

a.ptr = <address>
/* then inspection in the debugger shows that b->ptr is also <address> */

它很乐意让我在这两种结构类型之间进行转换,如上所示,当我在 IS_ATOMIC 指针变体上使用原子函数(例如 atomic_exchange())时,我的调试器向我展示了非原子结构地址的内容更改为预期值。

此时您可能会问 "why do this?" 答案是我有一个多线程应用程序,我想在短时间内锁定数据库记录,以便单个线程可以更新它而无需来自其他线程的争用,然后在我完成后释放锁。从历史上看,我用一个关键部分来保护这个操作,但这是非常悲观的,因为我可能有 - 比如说 - 10,000,000 条记录并随机更新它们,所以两个线程实际尝试更新同一条记录的机会非常小,但是临界区无条件阻塞所有线程。每条记录都由一个指针引用,所以过程:

  1. 原子地获取想要的记录指针并将其替换为静态定义的"busy"一个
  2. 检查它是否已经 "busy",如果是,旋转并重试直到我们得到 "non-busy"。
  3. 我们现在可以唯一访问此记录,因此请更新它。
  4. 将 "busy" 指针替换为原来的指针。

所以步骤 (1) 锁定和步骤 (4) 解锁,与临界区方法不同,如果两个线程试图访问相同的,访问只需要等待地址。它似乎有效,并且在我的 6 核系统(超线程,所以 12 个线程)上,它比处理真实数据集时使用单个关键部分快 5 倍左右。

那么为什么不首先将指向记录的指针定义为原​​子的呢?。答案是这个特定的代码可能会在其他地方对该信息进行非线程访问,它也可能以一种已知无竞争的方式进行线程访问;事实上,在大多数情况下,我 不想 考虑到锁定机制的成本。计时测试表明,典型的原子 lock/unlock 操作在我的系统上似乎需要大约 5 到 10 纳秒,我想在不需要时避免这种开销,所以在那些情况下我只使用原始指针.

我提供这个作为我解决这个特定问题的方式。我知道它不是正确的 C11,我知道它可能只适用于 x86 类型的架构——或者至少只适用于整数和指针类型是无锁和 "intrinsically atomic" 的架构——而且我也接受可能有如果您知道如何用汇编程序编写(我不会),则锁定给定地址的更好方法。我很高兴听到更好的解决方案。

顺便说一下,我还尝试了事务内存(即 _xbegin() .. _xend())作为解决此问题的方法。我发现它可以解决小的测试问题,但是一旦我将它扩展到真实数据,我就经常遇到 _xbegin() 失败,我认为这是因为当您访问的地址不在缓存内存中时,它往往会退出,迫使您采用后备代码路径。 Intel 不太清楚它是如何工作的细节,所以这个解释可能是错误的。

我也看过 Hardware Lock Elision 作为加速临界区方法的一种方法,但据我所知,由于容易受到黑客攻击,它已被​​弃用。无论如何,我太笨了,无法理解使用方法!