C++ operator[] 访问 SIMD(例如 AVX)变量的元素

标签 c++ simd intrinsics avx

我正在寻找一种重载 operator[](在更广泛的 SIMD 类中)的方法,以促进在 SIMD 字(例如 __m512i)中读取和写入单个元素。几个限制:

  • 符合 C++11(或更高版本)
  • 与其他基于内在函数的代码兼容
  • 不是 OpenCL/SYCL(我可以,但我不能*叹*)
  • 主要可移植到 g++、icpc、clang++
  • 最好适用于 Intel 以外的其他 SIMD(ARM、IBM 等...)
  • (编辑)性能并不是真正的问题(通常不用于性能很重要的地方)

  • (这排除了诸如通过指针转换进行类型双关和 GCC vector 类型之类的事情。)
    主要基于 Scott Meyers 的“更有效的 C++”(第 30 项)和其他代码,我提出了以下看起来“正确”的 MVC 代码,这似乎有效,但也似乎过于复杂。 (“代理”方法旨在处理左/右手运算符 [] 的用法,而“memcpy”旨在处理类型双关/C++ 标准问题。)
    我想知道是否有人有更好的解决方案(并且可以解释它以便我学到一些东西;^))
    #include <iostream>
    #include <cstring>
    #include "immintrin.h"
    
    using T = __m256i;           // SIMD type
    using Te = unsigned int;     // SIMD element type
    
    class SIMD {
    
        class SIMDProxy;
    
      public :
    
        const SIMDProxy operator[](int index) const {
          std::cout << "SIMD::operator[] const" << std::endl;
          return SIMDProxy(const_cast<SIMD&>(*this), index);
        }
        SIMDProxy operator[](int index){
          std::cout << "SIMD::operator[]" << std::endl;
          return SIMDProxy(*this, index);
        }
        Te get(int index) {
          std::cout << "SIMD::get" << std::endl;
          alignas(T) Te tmp[8];
          std::memcpy(tmp, &value, sizeof(T));  // _mm256_store_si256(reinterpret_cast<__m256i *>(tmp), c.value);
          return tmp[index];
        }
        void set(int index, Te x) {
          std::cout << "SIMD::set" << std::endl;
          alignas(T) Te tmp[8];
          std::memcpy(tmp, &value, sizeof(T));  // _mm256_store_si256(reinterpret_cast<__m256i *>(tmp), c.value);
          tmp[index] = x;
          std::memcpy(&value, tmp, sizeof(T));  // c.value = _mm256_load_si256(reinterpret_cast<__m256i const *>(tmp));
        }
    
        void splat(Te x) {
          alignas(T) Te tmp[8];
          std::memcpy(tmp, &value, sizeof(T));
          for (int i=0; i<8; i++) tmp[i] = x;
          std::memcpy(&value, tmp, sizeof(T));
        }
        void print() {
          alignas(T) Te tmp[8];
          std::memcpy(tmp, &value, sizeof(T));
          for (int i=0; i<8; i++) std::cout << tmp[i] << " ";
          std::cout << std::endl;
        }
    
      protected :
    
      private :
    
        T value;
    
        class SIMDProxy {
          public :
            SIMDProxy(SIMD & c_, int index_) : c(c_), index(index_) {};
            // lvalue access
            SIMDProxy& operator=(const SIMDProxy& rhs) {
              std::cout << "SIMDProxy::=SIMDProxy" << std::endl;
              c.set(rhs.index, rhs.c.get(rhs.index));
              return *this;
            }
            SIMDProxy& operator=(Te x) {
              std::cout << "SIMDProxy::=T" << std::endl;
              c.set(index,x);
              return *this;
            }
            // rvalue access
            operator Te() const {
              std::cout << "SIMDProxy::()" << std::endl;
              return c.get(index);
            }
          private:
            SIMD& c;       // SIMD this proxy refers to
            int index;      // index of element we want
        };
        friend class SIMDProxy;   // give SIMDProxy access into SIMD
    
    
    };
    
    /** a little main to exercise things **/
    int
    main(int argc, char *argv[])
    {
    
      SIMD x, y;
      Te a = 3;
    
      x.splat(1);
      x.print();
    
      y.splat(2);
      y.print();
    
      x[0] = a;
      x.print();
    
      y[1] = a;
      y.print();
    
      x[1] = y[1]; 
      x.print();
    }
    

    最佳答案

    你的代码效率很低。通常这些 SIMD 类型不会出现在内存中的任何地方,它们是硬件寄存器,它们没有地址,您不能将它们传递给 memcpy()。编译器非常努力地假装它们是普通变量,这就是为什么您的代码可以编译并且可能工作的原因,但它很慢,您一直在从寄存器到内存并返回。
    下面是我将如何做到这一点,假设 AVX2 和整数 channel 。

    class SimdVector
    {
        __m256i val;
    
        alignas( 64 ) static const std::array<int, 8 + 7> s_blendMaskSource;
    
    public:
    
        int operator[]( size_t lane ) const
        {
            assert( lane < 8 );
            // Move lane index into lowest lane of vector register
            const __m128i shuff = _mm_cvtsi32_si128( (int)lane );
            // Permute the vector so the lane we need is moved to the lowest lane
            // _mm256_castsi128_si256 says "the upper 128 bits of the result are undefined",
            // and we don't care indeed.
            const __m256i tmp = _mm256_permutevar8x32_epi32( val, _mm256_castsi128_si256( shuff ) );
            // Return the lowest lane of the result
            return _mm_cvtsi128_si32( _mm256_castsi256_si128( tmp ) );
        }
    
        void setLane( size_t lane, int value )
        {
            assert( lane < 8 );
            // Load the blending mask
            const int* const maskLoadPointer = s_blendMaskSource.data() + 7 - lane;
            const __m256i mask = _mm256_loadu_si256( ( const __m256i* )maskLoadPointer );
            // Broadcast the source value into all lanes.
            // The compiler will do equivalent of _mm_cvtsi32_si128 + _mm256_broadcastd_epi32
            const __m256i broadcasted = _mm256_set1_epi32( value );
            // Use vector blending instruction to set the desired lane
            val = _mm256_blendv_epi8( val, broadcasted, mask );
        }
    
        template<size_t lane>
        int getLane() const
        {
            static_assert( lane < 8 );
            // That thing is not an instruction;
            // compilers emit different ones based on the index
            return _mm256_extract_epi32( val, (int)lane );
        }
    
        template<size_t lane>
        void setLane( int value )
        {
            static_assert( lane < 8 );
            val = _mm256_insert_epi32( val, value, (int)lane );
        }
    };
    
    // Align by 64 bytes to guarantee it's contained within a cache line
    alignas( 64 ) const std::array<int, 8 + 7> SimdVector::s_blendMaskSource
    {
        0, 0, 0, 0, 0, 0, 0, -1,  0, 0, 0, 0, 0, 0, 0
    };
    
    对于 ARM,情况不同。如果在编译时知道车道索引,请参阅 vgetq_lane_s32vsetq_lane_s32内在因素。
    要在 ARM 上设置 channel ,您可以使用相同的广播 + 混合技巧。广播是vdupq_n_s32 . vector 混合的近似等价物是 vbslq_s32 ,它独立处理每一位,但对于这个用例,它同样适用,因为 -1设置了所有 32 位。
    为了提取或者写一个 switch ,或者将完整的 vector 存储到内存中,不确定这两个中哪个更有效。

    关于C++ operator[] 访问 SIMD(例如 AVX)变量的元素,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64282775/

    相关文章:

    optimization - 创建掩蔽 kreg 值的有效方法

    x86-64 - SIMD 固有和内存总线大小 - CPU 如何在一次内存读取中获取所有 128/256 位?

    c++ - Visual C++ 逗号运算符和 sse 内在函数

    c++ - 从 C++ 中的网站读取文本

    c++ - 获取 FastCGI 中的所有客户端 header (C/C++)

    c++ - char *argv[ ] 和 int argc C++ 的全局范围

    c++ - Release模式下的 Xcode 无法编译 <immintrin.h> - 提示 __builtin_ia32_emms()

    c++ - 更快的二进制加法 C++

    c++ - 使用 SSE 内在函数复制少量数据的问题

    c - 数组乘法与 sse 内在函数乘法的时间?