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

时间:2019-03-22 12:23:04

标签: c gcc clang atomic c11

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

如果TYPE是无锁的,gcc和/或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都将struct及其原子变体的大小保持相同,并且它们会生成对名为__atomic_store的函数的调用商店(通过与-latomic链接解决)。

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

2 个答案:

答案 0 :(得分:4)

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

如果对齐方式正确,则更适合使用__atomic内置函数,这些内置函数正是为此用例而设计的:

如上所述,如果ABI对普通(非原子)类型的对齐方式不足,并且_Atomic会更改对齐方式(目前仅使用Clang),则此方法将无效。

这些内建函数在非原子类型的情况下也可以使用,因为它们使用行外锁。这也是为什么使用相同机制的_Atomic类型不需要额外存储的原因。这意味着由于无意间共享锁,存在一些不必要的争用。这些锁的实现方式是一个实现细节,在未来的libatomic版本中可能会更改。

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

答案 1 :(得分:0)

此方法不是合法的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)解锁,与关键部分方法不同,访问只有在两个线程试图访问 相同 地址。它似乎可以正常工作,并且在我的6核心系统(超线程,因此有12个线程)上,与在实际数据集上使用单个关键部分相比,它的速度大约快5倍。

那么为什么不首先将指向记录的指针定义为原​​子?答案是,该特定代码可以在其他地方进行对该信息的无线程访问,并且还可以以一种已知的无竞争方式进行线程访问。实际上,在大多数情况下,由于成本原因,我希望拥有锁定机制。时序测试表明,典型的原子锁定/解锁操作在我的系统上似乎要花费5到10纳秒,并且我想避免不需要的开销,因此在那种情况下,我只使用原始指针。 / p>

我将其作为解决此特定问题的方式。我知道这不是正确的C11,我知道它可能仅适用于x86类型的体系结构-或至少仅适用于整数和指针类型为无锁且“本质上是原子的”体系结构-我也接受可能更好的方法如果您知道如何用汇编器编写(我不知道),则可以锁定给定地址。我很高兴听到更好的解决方案。

偶然地,我也尝试了事务性内存(即_xbegin().. _xend())作为解决此问题的一种方法。我发现它可以解决一些小的测试问题,但是一旦将其扩展到实际数据时,我就会经常遇到_xbegin()失败,并且我认为这是因为当您访问的地址不在高速缓存中时,它往往会退出紧急状态,强迫您采用后备代码路径。英特尔不太了解其工作方式的详细信息,因此这种解释可能是错误的。

我还查看了硬件锁消除技术,作为加快关键部分方法的一种方法,但是据我所知,由于易受黑客攻击,它已被​​弃用..无论如何,我实在太厚了,无法理解如何使用它!