是否需要将一种类型的数组位转换为另一种类型以避免 UB? 例如,我有一个函数
void func(std::vector<int32_t>& dest, std::vector<std::byte>& src, const long stride){
const auto ptr = reinterpret_cast<std::byte *>(dest.data());
for (std::size_t i = 0; i < src.size(); ++i) {
const auto t = ptr + 4 * i;
t[0] = src[i];
t[1] = src[i + stride];
t[2] = src[i + 2 * stride];
t[3] = src[i + 3 * stride];
}
}
我需要改用bit_cast
吗?
void func2(std::vector<int32_t>& dest, std::vector<std::byte>& src, const long stride){
for (std::size_t i = 0; i < src.size(); ++i) {
alignas(std::int32_t) std::array<std::byte, 4> t;
t[0] = src[i];
t[1] = src[i + stride];
t[2] = src[i + 2 * stride];
t[3] = src[i + 3 * stride];
dest[i] = std::bit_cast<std::int32_t>(t);
}
}
或者使用memcpy
?
void func3(std::vector<int32_t>& dest, std::vector<std::byte>& src, const long stride){
for (std::size_t i = 0; i < src.size(); ++i) {
alignas(std::int32_t) std::byte t[4];
t[0] = src[i];
t[1] = src[i + stride];
t[2] = src[i + 2 * stride];
t[3] = src[i + 3 * stride];
std::memcpy(&dest[i], t, sizeof t);
}
}
根据我的测试,bit_cast
和 memcpy
似乎有一些开销,并且生成的 asm 代码不同,我们期望标量类型相同
https://godbolt.org/z/Y1W585EWY
最佳答案
我不知道它的UB是否在那里,但如果你可以使用未签名的版本,你可以转换这部分:
t[0] = src[i];
t[1] = src[i + stride];
t[2] = src[i + 2 * stride];
t[3] = src[i + 3 * stride];
对此:
dest[i] = src[i] +
src[i+stride] * (uint32_t) 256 +
src[i+stride*2] * (uint32_t) 65536 +
src[i+stride*3] * (uint32_t) 16777216;
如果您需要加速,您可以向量化操作:
// for avx512
vector1 = src[i] to src[i+16]
vector2 = src[i+stride] to src[i+stride+16]
vector3 = src[i+stride*2] to src[i+stride*2+16]
vector4 = src[i+stride*3] to src[i+stride*3+16]
然后以相同的方式将它们连接起来,但是以矢量化的形式。
// either a 16-element vector extension
destVect[i] = vector1 + vector2*256 + ....
// or just run over 16-elements at a time like tiled-computing
for(j from 0 to 15)
destVect[i+j] = ...
也许您甚至不需要显式使用内在函数。只需尝试使用简单的循环处理 simd 宽度数量的元素的数组( vector ),但通常封装会增加代码膨胀,因此您可能需要在堆栈上的裸数组上执行此操作。
某些编译器有一个默认的最小循环迭代次数来进行矢量化,因此您应该使用不同的图 block 宽度来测试它,或者应用一个编译器标志,让它甚至可以对小循环进行矢量化。
以下是来自玩具自动矢量化 SIMD 库的示例解决方案:https://godbolt.org/z/qMWsbsrG8
输出:
1000 operations took 5518 nanoseconds
这是所有堆栈数组分配+内核启动开销。
对于 10000 次操作 ( https://godbolt.org/z/Mz1K75Kj1 ),每次操作需要 2.4 纳秒。
这里有 1000 个操作,但仅使用 16 个工作项(AVX512 上的单个 ZMM 寄存器):https://godbolt.org/z/r9GTfffG8
simd*1000 operations took 20551 nanoseconds
每个操作需要 1.25 纳秒(至少在 godbolt.org 服务器上)。对于 FX8150 和更窄的 simd 值,每次操作大约需要 6.9 纳秒。如果您编写非封装版本,它应该会减少代码膨胀并且速度更快。
最后,使用多次迭代进行基准测试:https://godbolt.org/z/dn9vj9seP
simd*1000 operations took 12367 nanoseconds
simd*1000 operations took 12420 nanoseconds
simd*1000 operations took 12118 nanoseconds
simd*1000 operations took 2753 nanoseconds
simd*1000 operations took 2694 nanoseconds
simd*1000 operations took 2691 nanoseconds
simd*1000 operations took 2839 nanoseconds
simd*1000 operations took 2698 nanoseconds
simd*1000 operations took 2702 nanoseconds
simd*1000 operations took 2711 nanoseconds
simd*1000 operations took 2718 nanoseconds
simd*1000 operations took 2710 nanoseconds
每次操作0.17纳秒。对于客户端共享的服务器 RAM + 简单循环来说,23GB/s 还算不错。显式使用 AVX 内在函数并且不进行封装应该可以获得 L1/L2/L3 缓存或 RAM 的最大带宽(取决于数据集大小)。但要注意,如果您在具有客户端共享的实际工作服务器上执行此操作,那么您的邻居将在 AVX512 加速计算期间感受到涡轮降频(除非整数不算作 AVX512 管道的重负载)。
编译器的行为会有所不同。例如,clang 会生成以下内容:
.LBB0_4:
vpslld zmm1, zmm1, 8 // evil integer bit level hacking?
vpslld zmm2, zmm2, 16
vpslld zmm3, zmm3, 24
vpord zmm0, zmm1, zmm0
vpternlogd zmm3, zmm0, zmm2, 254 // what the code?
vmovdqu64 zmmword ptr [r14 + 4*rax], zmm3
add rax, 16
cmp rax, 16000
jne .LBB0_4
而 gcc 会产生更多的代码膨胀。我不知道为什么。
(通过迭代到最后一个元素并添加步幅,您还会在 src vector 中出现溢出)
关于c++ - `bit_cast` 数组到数组,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/72070483/