c++ - `bit_cast` 数组到数组

标签 c++ c++20 type-punning

是否需要将一种类型的数组位转换为另一种类型以避免 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_castmemcpy 似乎有一些开销,并且生成的 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/

相关文章:

c++ - 如何检查 C++ API 的性能

c++ - 是否可以在C++编写的gRPC异步服务器中使用智能PTR或Boost Intrusive Ptr作为 “void* tag”值

c++ - QMAKE_EXTRA_COMPILERS 变量使 INCLUDEPATH 无效

c++ - 嵌入模板时模板化函数类型丢失

c++ - 如何通过不同类型重新解释数据? (类型双关困惑)

c++ - Qt 录像机

c++ - 析构函数必须对默认初始化的类成员才可用(公共(public))还是完全有效?

c++ - 为什么我不能从迭代器构造 std::span ?

c - 从内联字节重新组装 float

c - C 中的类型双关和 union