我正在编写一个库函数,比如说,count_char(const char *str, int len, char ch)
检测它正在运行的 CPU 支持的 SIMD 扩展并将调用分派(dispatch)给,比如说,一个 AVX2 或 SSE4.2 优化版本。因为我想避免每次调用都执行几个 cpuid
指令的惩罚,所以我尝试在第一次调用函数时只执行一次(可能由不同的调用)同时线程)。
在 C++ 领域我会做类似的事情
int count_char(const char *str, int len, char ch) {
static const auto fun_ptr = select_simd_function();
return (*fun_ptr)(str, len, ch);
}
并依靠 static
的 C++ 语义来保证在没有任何竞争条件的情况下只调用一次。但是在纯 C 中执行此操作的最佳方法是什么?
这是我想出的:
- 使用原子变量(也存在于 C 语言中)——相当容易出错并且更难维护。
- 使用
pthread_once
— 不确定它有什么开销,而且它在 Windows 上可能会令人头疼。 - 强制库用户调用另一个库函数来初始化指针——简而言之,它在我的情况下不起作用,因为这实际上是另一种语言的库的 C 位。
- 将指针对齐 8 个字节并依赖于原子性的 x86 字大小访问 — 不可移植到其他架构(我稍后会实现一些 PowerPC 或特定于 ARM 的 SIMD 版本,比如说),技术上 UB(至少在 C++ 中)。
- 使用线程本地存储并将
fun_ptr
标记为thread_local
然后执行类似的操作
static thread_local fun_ptr_t fun_ptr = NULL;
if (!fun_ptr) {
fun_ptr = select_simd_function();
}
return (*fun_ptr)(str, len, ch);
好处是代码非常清晰而且显然是正确的,但我不确定 TLS 的性能影响,而且每个线程都必须调用一次 select_simd_function()
(但那是可能没什么大不了的)。
就我个人而言,到目前为止,(5) 是赢家,紧随其后的是 (1)(如果不是其他人的非常基础的库,我什至可能会选择 (1),而且我不想这样做可能因为错误的实现而让自己难堪)。
那么,最好的选择是什么?我还错过了什么吗?
最佳答案
如果您可以使用 C11,这将有效(假设您的实现支持线程 - it's an optional feature):
#include <threads.h>
static fun_ptr_t fun_ptr = NULL;
static void init_fun_ptr( void )
{
fun_ptr = select_simd_function();
}
fun_ptr_t get_simd_function( void )
{
static once_flag flag = ONCE_FLAG_INIT;
call_once( &flag, init_fun_ptr);
return ( fun_ptr );
}
当然,您提到了 Windows。我怀疑 MSVC 是否支持这一点。
关于c - 线程安全地初始化一个指针一次,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60215029/