将指针转换为 _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
成为一个较大的结构,那么在 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*)(&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 条记录并随机更新它们,所以两个线程实际尝试更新同一条记录的机会非常小,但是临界区无条件阻塞所有线程。每条记录都由一个指针引用,所以过程:
- 原子地获取想要的记录指针并将其替换为静态定义的"busy"一个
- 检查它是否已经 "busy",如果是,旋转并重试直到我们得到 "non-busy"。
- 我们现在可以唯一访问此记录,因此请更新它。
- 将 "busy" 指针替换为原来的指针。
所以步骤 (1) 锁定和步骤 (4) 解锁,与临界区方法不同,如果两个线程试图访问相同的,访问只需要等待地址。它似乎有效,并且在我的 6 核系统(超线程,所以 12 个线程)上,它比处理真实数据集时使用单个关键部分快 5 倍左右。
那么为什么不首先将指向记录的指针定义为原子的呢?。答案是这个特定的代码可能会在其他地方对该信息进行非线程访问,它也可能以一种已知无竞争的方式进行线程访问;事实上,在大多数情况下,我 不想 考虑到锁定机制的成本。计时测试表明,典型的原子 lock/unlock 操作在我的系统上似乎需要大约 5 到 10 纳秒,我想在不需要时避免这种开销,所以在那些情况下我只使用原始指针.
我提供这个作为我解决这个特定问题的方式。我知道它不是正确的 C11,我知道它可能只适用于 x86 类型的架构——或者至少只适用于整数和指针类型是无锁和 "intrinsically atomic" 的架构——而且我也接受可能有如果您知道如何用汇编程序编写(我不会),则锁定给定地址的更好方法。我很高兴听到更好的解决方案。
顺便说一下,我还尝试了事务内存(即 _xbegin() .. _xend())作为解决此问题的方法。我发现它可以解决小的测试问题,但是一旦我将它扩展到真实数据,我就经常遇到 _xbegin() 失败,我认为这是因为当您访问的地址不在缓存内存中时,它往往会退出,迫使您采用后备代码路径。 Intel 不太清楚它是如何工作的细节,所以这个解释可能是错误的。
我也看过 Hardware Lock Elision 作为加速临界区方法的一种方法,但据我所知,由于容易受到黑客攻击,它已被弃用。无论如何,我太笨了,无法理解使用方法!
根据我对标准的阅读,
*(_Atomic TYPE*)&(TYPE){0}
(换句话说,将指向非原子的指针转换为指向相应原子的指针并取消引用)不受支持。
如果 TYPE
is/isn 不是无锁的,gcc and/or clang 是否将其识别为扩展? (问题 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*)(&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 条记录并随机更新它们,所以两个线程实际尝试更新同一条记录的机会非常小,但是临界区无条件阻塞所有线程。每条记录都由一个指针引用,所以过程:
- 原子地获取想要的记录指针并将其替换为静态定义的"busy"一个
- 检查它是否已经 "busy",如果是,旋转并重试直到我们得到 "non-busy"。
- 我们现在可以唯一访问此记录,因此请更新它。
- 将 "busy" 指针替换为原来的指针。
所以步骤 (1) 锁定和步骤 (4) 解锁,与临界区方法不同,如果两个线程试图访问相同的,访问只需要等待地址。它似乎有效,并且在我的 6 核系统(超线程,所以 12 个线程)上,它比处理真实数据集时使用单个关键部分快 5 倍左右。
那么为什么不首先将指向记录的指针定义为原子的呢?。答案是这个特定的代码可能会在其他地方对该信息进行非线程访问,它也可能以一种已知无竞争的方式进行线程访问;事实上,在大多数情况下,我 不想 考虑到锁定机制的成本。计时测试表明,典型的原子 lock/unlock 操作在我的系统上似乎需要大约 5 到 10 纳秒,我想在不需要时避免这种开销,所以在那些情况下我只使用原始指针.
我提供这个作为我解决这个特定问题的方式。我知道它不是正确的 C11,我知道它可能只适用于 x86 类型的架构——或者至少只适用于整数和指针类型是无锁和 "intrinsically atomic" 的架构——而且我也接受可能有如果您知道如何用汇编程序编写(我不会),则锁定给定地址的更好方法。我很高兴听到更好的解决方案。
顺便说一下,我还尝试了事务内存(即 _xbegin() .. _xend())作为解决此问题的方法。我发现它可以解决小的测试问题,但是一旦我将它扩展到真实数据,我就经常遇到 _xbegin() 失败,我认为这是因为当您访问的地址不在缓存内存中时,它往往会退出,迫使您采用后备代码路径。 Intel 不太清楚它是如何工作的细节,所以这个解释可能是错误的。
我也看过 Hardware Lock Elision 作为加速临界区方法的一种方法,但据我所知,由于容易受到黑客攻击,它已被弃用。无论如何,我太笨了,无法理解使用方法!