用 C++ 原子替换 #pragma omp atomic
Replacing #pragma omp atomic with c++ atomics
我正在用标准 C++11/C++14 atomics/thread 支持替换一些 OpenMP 代码。这是 OpenMP 最小代码示例:
#include <vector>
#include <cstdint>
void omp_atomic_add(std::vector<std::int64_t> const& rows,
std::vector<std::int64_t> const& cols,
std::vector<double>& values,
std::size_t const row,
std::size_t const col,
double const value)
{
for (auto i = rows[row]; i < rows[row+1]; ++i)
{
if (cols[i] == col)
{
#pragma omp atomic
values[i] += value;
return;
}
}
}
该代码更新了 CSR 矩阵格式,并出现在科学计算的热路径中。从技术上讲,使用 std::mutex
是可行的,但是 values
向量可以包含数百万个元素,并且访问次数比它多很多次,因此 std::mutex
太重了。
检查程序集 https://godbolt.org/g/nPE9Dt,它似乎使用了 CAS(免责声明,我的原子和程序集知识非常有限,所以我的评论可能不正确):
mov rax, qword ptr [rdi]
mov rdi, qword ptr [rax + 8*rcx]
mov rax, qword ptr [rax + 8*rcx + 8]
cmp rdi, rax
jge .LBB0_6
mov rcx, qword ptr [rsi]
.LBB0_2: # =>This Inner Loop Header: Depth=1
cmp qword ptr [rcx + 8*rdi], r8
je .LBB0_3
inc rdi
cmp rdi, rax
jl .LBB0_2
jmp .LBB0_6
#### Interesting stuff happens from here onwards
.LBB0_3:
mov rcx, qword ptr [rdx] # Load values pointer into register
mov rax, qword ptr [rcx + 8*rdi] # Offset to value[i]
.LBB0_4: # =>This Inner Loop Header: Depth=1
movq xmm1, rax # Move value into floating point register
addsd xmm1, xmm0 # Add function arg to the value from the vector<double>
movq rdx, xmm1 # Move result to register
lock # x86 lock
cmpxchg qword ptr [rcx + 8*rdi], rdx # Compare exchange on the value in the vector
jne .LBB0_4 # If failed, go back to the top and try again
.LBB0_6:
ret
使用 C++ 原子能做到这一点吗?我看到的示例仅使用 std::atomic<double> value{}
而在通过指针访问值的上下文中没有任何内容。
您可以创建 std::vector<std::atomic<double>>
但不能更改其大小。
我要做的第一件事是获取 gsl::span
或编写我自己的变体。那么 gsl::span<std::atomic<double>>
是 values
比 std::vector<std::atomic<double>>
更好的模型。
完成后,只需删除 #pragma omp atomic
并且您的代码在 c++20. In c++17 中是原子的,然后您必须手动实施 +=
.
double old = values[i];
while(!values[i].compare_exchange_weak(old, old+value))
{}
Clang 5 生成:
omp_atomic_add(std::vector<long, std::allocator<long> > const&, std::vector<long, std::allocator<long> > const&, std::vector<std::atomic<double>, std::allocator<std::atomic<double> > >&, unsigned long, unsigned long, double): # @omp_atomic_add(std::vector<long, std::allocator<long> > const&, std::vector<long, std::allocator<long> > const&, std::vector<std::atomic<double>, std::allocator<std::atomic<double> > >&, unsigned long, unsigned long, double)
mov rax, qword ptr [rdi]
mov rdi, qword ptr [rax + 8*rcx]
mov rax, qword ptr [rax + 8*rcx + 8]
cmp rdi, rax
jge .LBB0_6
mov rcx, qword ptr [rsi]
.LBB0_2: # =>This Inner Loop Header: Depth=1
cmp qword ptr [rcx + 8*rdi], r8
je .LBB0_3
inc rdi
cmp rdi, rax
jl .LBB0_2
jmp .LBB0_6
.LBB0_3:
mov rax, qword ptr [rdx]
mov rax, qword ptr [rax + 8*rdi]
.LBB0_4: # =>This Inner Loop Header: Depth=1
mov rcx, qword ptr [rdx]
movq xmm1, rax
addsd xmm1, xmm0
movq rsi, xmm1
lock
cmpxchg qword ptr [rcx + 8*rdi], rsi
jne .LBB0_4
.LBB0_6:
ret
这和我不经意的一瞥一模一样。
atomic_view
有一项提案允许您通过原子视图操作非原子值。通常,C++ 只允许您对原子数据进行原子操作。
我正在用标准 C++11/C++14 atomics/thread 支持替换一些 OpenMP 代码。这是 OpenMP 最小代码示例:
#include <vector>
#include <cstdint>
void omp_atomic_add(std::vector<std::int64_t> const& rows,
std::vector<std::int64_t> const& cols,
std::vector<double>& values,
std::size_t const row,
std::size_t const col,
double const value)
{
for (auto i = rows[row]; i < rows[row+1]; ++i)
{
if (cols[i] == col)
{
#pragma omp atomic
values[i] += value;
return;
}
}
}
该代码更新了 CSR 矩阵格式,并出现在科学计算的热路径中。从技术上讲,使用 std::mutex
是可行的,但是 values
向量可以包含数百万个元素,并且访问次数比它多很多次,因此 std::mutex
太重了。
检查程序集 https://godbolt.org/g/nPE9Dt,它似乎使用了 CAS(免责声明,我的原子和程序集知识非常有限,所以我的评论可能不正确):
mov rax, qword ptr [rdi]
mov rdi, qword ptr [rax + 8*rcx]
mov rax, qword ptr [rax + 8*rcx + 8]
cmp rdi, rax
jge .LBB0_6
mov rcx, qword ptr [rsi]
.LBB0_2: # =>This Inner Loop Header: Depth=1
cmp qword ptr [rcx + 8*rdi], r8
je .LBB0_3
inc rdi
cmp rdi, rax
jl .LBB0_2
jmp .LBB0_6
#### Interesting stuff happens from here onwards
.LBB0_3:
mov rcx, qword ptr [rdx] # Load values pointer into register
mov rax, qword ptr [rcx + 8*rdi] # Offset to value[i]
.LBB0_4: # =>This Inner Loop Header: Depth=1
movq xmm1, rax # Move value into floating point register
addsd xmm1, xmm0 # Add function arg to the value from the vector<double>
movq rdx, xmm1 # Move result to register
lock # x86 lock
cmpxchg qword ptr [rcx + 8*rdi], rdx # Compare exchange on the value in the vector
jne .LBB0_4 # If failed, go back to the top and try again
.LBB0_6:
ret
使用 C++ 原子能做到这一点吗?我看到的示例仅使用 std::atomic<double> value{}
而在通过指针访问值的上下文中没有任何内容。
您可以创建 std::vector<std::atomic<double>>
但不能更改其大小。
我要做的第一件事是获取 gsl::span
或编写我自己的变体。那么 gsl::span<std::atomic<double>>
是 values
比 std::vector<std::atomic<double>>
更好的模型。
完成后,只需删除 #pragma omp atomic
并且您的代码在 c++20. In c++17 中是原子的,然后您必须手动实施 +=
.
double old = values[i];
while(!values[i].compare_exchange_weak(old, old+value))
{}
Clang 5 生成:
omp_atomic_add(std::vector<long, std::allocator<long> > const&, std::vector<long, std::allocator<long> > const&, std::vector<std::atomic<double>, std::allocator<std::atomic<double> > >&, unsigned long, unsigned long, double): # @omp_atomic_add(std::vector<long, std::allocator<long> > const&, std::vector<long, std::allocator<long> > const&, std::vector<std::atomic<double>, std::allocator<std::atomic<double> > >&, unsigned long, unsigned long, double)
mov rax, qword ptr [rdi]
mov rdi, qword ptr [rax + 8*rcx]
mov rax, qword ptr [rax + 8*rcx + 8]
cmp rdi, rax
jge .LBB0_6
mov rcx, qword ptr [rsi]
.LBB0_2: # =>This Inner Loop Header: Depth=1
cmp qword ptr [rcx + 8*rdi], r8
je .LBB0_3
inc rdi
cmp rdi, rax
jl .LBB0_2
jmp .LBB0_6
.LBB0_3:
mov rax, qword ptr [rdx]
mov rax, qword ptr [rax + 8*rdi]
.LBB0_4: # =>This Inner Loop Header: Depth=1
mov rcx, qword ptr [rdx]
movq xmm1, rax
addsd xmm1, xmm0
movq rsi, xmm1
lock
cmpxchg qword ptr [rcx + 8*rdi], rsi
jne .LBB0_4
.LBB0_6:
ret
这和我不经意的一瞥一模一样。
atomic_view
有一项提案允许您通过原子视图操作非原子值。通常,C++ 只允许您对原子数据进行原子操作。