【发布时间】:2021-12-30 18:18:13
【问题描述】:
我有一个大的内存数组作为指针uint64_t * arr(加上大小),它代表普通位。我需要非常有效地(最高性能/最快)将这些位从 0 向右移动到 63。
通过移动整个数组,我的意思是不移动每个元素(如a[i] <<= Shift),而是将其作为单个大位向量进行移动。换句话说,对于每个中间位置i(第一个和最后一个元素除外),我可以在循环中执行以下操作:
dst[i] = w | (src[i] << Shift);
w = src[i] >> (64 - Shift);
其中w 是一些临时变量,保存前一个数组元素的右移值。
上面的这个解决方案简单明了。但我需要更高效的东西,因为我有千兆字节的数据。
理想情况下会为此使用一些 SIMD 指令,因此我正在寻找专家的 SIMD 建议。我需要为所有四种流行指令集 - SSE-SSE4.2 / AVX / AVX-2 / AVX-512 实现移位代码。
但据我所知,例如对于 SSE2,仅存在 _mm_slli_si128() 内在/指令,它仅移动 8 的倍数(换句话说,字节移动)。而且我需要按任意位大小进行移位,而不仅仅是字节移位。
如果没有 SIMD,我也可以通过使用 shld reg, reg, reg 指令一次移位 128 位,该指令允许进行 128 位移位。它在 MSVC 中实现为固有的__shiftleft128(),并生成可以是seen here 的汇编代码。
顺便说一句,我需要所有 MSVC/GCC/CLang 的解决方案。
同样在单循环迭代中,我可以在顺序操作中移动 4 或 8 个字,这将使用 CPU 流水线来加速多条指令的并行乱序执行。
如果需要,我的位向量可以与内存中任意数量的字节对齐,如果这有助于例如通过对齐读/写来提高 SIMD 速度。源和目标位向量内存也不同(不重叠)。
换句话说,我正在寻找有关如何在不同的 Intel CPU 上最有效(最高效地)解决我的任务的所有建议。
注意,澄清一下,我实际上必须做几个班次,而不仅仅是一次班次。我有大位向量X,和数百个移位大小s0, s1, ..., sN,其中每个移位大小不同并且也可能很大(例如移位100K位),然后我想计算得到的大位向量@987654335 @。我只是将我对 StackOverflow 的问题简化为移动单个向量。但可能这个关于原始任务的细节非常重要。
应@Jake'Alquimista'LEE 的要求,我决定实现一个现成的玩具最小可重复示例,以计算输入位向量src 的移位或生成最终的@ 987654337@ 位向量。这个例子根本没有优化,只是我的任务如何解决的一个简单的变体。为简单起见,这个例子的输入向量很小,不像我的例子那样是千兆字节。这是一个玩具示例,我没有检查它是否正确解决了任务,它可能包含一些小错误:
#include <cstdint>
#include <vector>
#include <random>
#define bit_sizeof(x) (sizeof(x) * 8)
using u64 = uint64_t;
using T = u64;
int main() {
std::mt19937_64 rng{123};
// Random generate source bit vector
std::vector<T> src(100'000);
for (size_t i = 0; i < src.size(); ++i)
src[i] = rng();
size_t const src_bitsize = src.size() * bit_sizeof(T);
// Destination bit vector, for example twice bigger in size
std::vector<T> dst(src.size() * 2);
// Random generate shifts
std::vector<u64> shifts(200);
for (size_t i = 0; i < shifts.size(); ++i)
shifts[i] = rng() % src_bitsize;
// Right-shift that handles overflow
auto Shr = [](auto x, size_t s) {
return s >= bit_sizeof(x) ? 0 : (x >> s);
};
// Do actual Shift-Ors
for (auto orig_shift: shifts) {
size_t const
word_off = orig_shift / bit_sizeof(T),
bit_off = orig_shift % bit_sizeof(T);
if (word_off >= dst.size())
continue;
size_t const
lim = std::min(src.size(), dst.size() - word_off);
T w = 0;
for (size_t i = 0; i < lim; ++i) {
dst[word_off + i] |= w | (src[i] << bit_off);
w = Shr(src[i], bit_sizeof(T) - bit_off);
}
// Special case of handling for last word
if (word_off + lim < dst.size())
dst[word_off + lim] |= w;
}
}
我真实项目的当前代码与上面的玩具示例不同。该项目已经正确解决了现实世界的任务。我只需要做额外的优化。我已经做了一些优化,比如使用OpenMP 在所有内核上并行化移位或操作。同样如 cmets 中所说,我为每个班次大小创建了专门的模板函数,总共 64 个函数,并选择 64 个函数中的一个来执行实际的班次或。每个 C++ 函数都有移位大小的编译时间值,因此编译器会根据编译时间值进行额外的优化。
【问题讨论】:
-
如果你有千兆字节的数据,你不是内存受限吗?您是否应该考虑将此操作与上一个或下一个操作合并,除非这两者都受到硬件或其他方面的限制?
-
看看 GMP 的低级
mpnlshift / rshift 函数:gmplib.org/repo/gmp/file/tip/mpn/x86_64/fastsse/… - 你可以使用 GMP(GPL 许可证)或者将它们的循环转换为 intrinsincs。 -
好的,是的,当您的读取输入可能以 4 到 8 个为一组时,您需要即时移动,而不是将每个位对齐的临时数组存储回内存(除非您是很快就会重复使用相同的班次计数)。
-
GCC 自动向量化这个以编译时移位计数没有问题:godbolt.org/z/PKbhqjdM4
-
@Arty 你可以使用
__builtin_assume_aligned用于 GCC
标签: c++ performance simd sse avx