【问题标题】:Casting pointers to _Atomic pointers and _Atomic sizes将指针转换为 _Atomic 指针和 _Atomic 大小
【发布时间】:2019-03-22 12:23:03
【问题描述】:

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

如果TYPE 是/不是无锁的,gcc 和/或 clang 是否将其识别为扩展? (问题一)

第二个相关问题:我的印象是,如果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 编译上面的sn-p,gcc 和clang 都将结构及其原子变体保持在相同的大小,并且它们会生成对名为__atomic_store 的函数的调用store(通过链接-latomic解决)。

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

【问题讨论】:

    标签: c gcc clang atomic c11


    【解决方案1】:

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

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

    如上所述,这在 ABI 为普通(非原子)类型提供的对齐不足以及 _Atomic 会更改对齐(目前仅使用 Clang)的情况下不起作用。

    这些内置函数也适用于非原子类型,因为它们使用外联锁。这也是_Atomic 类型不需要额外存储的原因,它们使用相同的机制。这意味着由于无意共享锁而存在一些不必要的争用。这些锁的实现方式是一个实现细节,可能会在 libatomic 的未来版本中发生变化。

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

    【讨论】:

      【解决方案2】:

      这种方法不是合法的 C11,但我设法欺骗了我的编译器(英特尔 2019),使其在原子和非原子“简单”类型之间进行转换,如下所示。

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

      下一步是使用 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())时,我的调试器向我显示非原子结构地址的内容更改为预期值。

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

      1. 以原子方式获取所需记录指针并将其替换为静态定义的“忙碌”记录指针
      2. 检查它是否已经“忙碌”,如果是,请旋转并重试,直到我们得到“非忙碌”。
      3. 我们现在拥有对该记录的唯一访问权限,因此请更新它。
      4. 将“忙碌”指针替换为原来的指针。

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

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

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

      顺便说一句,我还尝试了事务性内存(即 _xbegin() .. _xend())作为解决此问题的一种方法。我发现它可以解决一些小的测试问题,但是一旦我将它扩展到真实数据,我经常会遇到 _xbegin() 失败,我认为这是因为当您访问的地址不在高速缓存中时,它往往会退出,强迫您采用您的后备代码路径。英特尔对其工作方式的细节并不十分坦率,因此这种解释可能是错误的。

      我还查看了硬件锁省略作为加速临界区方法的一种方式,但据我所知,由于容易受到黑客攻击,它已被​​弃用......而且无论如何我太厚了,无法理解如何使用它!

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-07-22
        • 2012-10-24
        • 2017-02-12
        • 1970-01-01
        • 1970-01-01
        • 2021-09-20
        • 1970-01-01
        相关资源
        最近更新 更多