【问题标题】:Generate random numbers in a given range with AVX2, faster than SVML _mm256_rem_epu32 remainder?使用 AVX2 生成给定范围内的随机数,比 SVML _mm256_rem_epu32 余数更快?
【发布时间】:2022-01-02 17:49:49
【问题描述】:

我目前正在尝试使用 AVX2 实现 XOR_SHIFT 随机数生成器,它实际上非常简单且非常快速。但是我需要能够指定一个范围。这通常需要模数。

这对我来说是一个主要问题,原因有两个:

  • 将 _mm256_rem_epu32() / _mm256_rem_epi32() SVML 函数添加到我的代码中,我的循环运行时间从大约 270 毫秒减少到 1.8 秒。哎哟!
  • SVML 仅适用于 MSVC 和 Intel 编译器

是否有任何使用 AVX2 进行模运算显着更快的方法?

非向量代码:

 std::srand(std::time(nullptr));
 std::mt19937_64 e(std::rand());

 uint32_t seed = static_cast<uint32_t>(e());

 for (; i != end; ++i)
 {
      seed ^= (seed << 13u);
      seed ^= (seed >> 7u);
      seed ^= (seed << 17u);

      arr[i] = static_cast<T>(low + (seed % ((up + 1u) - low)));
 }//End for

矢量化:

  constexpr uint32_t thirteen = 13u;
  constexpr uint32_t seven = 7u;
  constexpr uint32_t seventeen = 17u;

  const __m256i _one = _mm256_set1_epi32(1);
  const __m256i _lower = _mm256_set1_epi32(static_cast<uint32_t>(low));
  const __m256i _upper = _mm256_set1_epi32(static_cast<uint32_t>(up));
                           
  __m256i _temp = _mm256_setzero_si256();
  __m256i _res = _mm256_setzero_si256();
                                                            
  __m256i _seed = _mm256_set_epi32(
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e()),
       static_cast<uint32_t>(e())
  );

  for (; (i + 8uz) < end; ++i)
  {
       //Generate Random Numbers
       _temp = _mm256_slli_epi32(_seed, thirteen);
       _seed = _mm256_xor_si256(_seed, _temp);

       _temp = _mm256_srai_epi32(_seed, seven);
       _seed = _mm256_xor_si256(_seed, _temp);

       _temp = _mm256_slli_epi32(_seed, seventeen);
       _seed = _mm256_xor_si256(_seed, _temp);

       //Narrow
       _temp = _mm256_add_epi32(_upper, _one);
       _temp = _mm256_sub_epi32(_temp, _lower);
       _temp = _mm256_rem_epu32(_seed, _temp); //Comment this line out for a massive speed up but incorrect results
       _res = _mm256_add_epi32(_lower, _temp);                                        

       _mm256_store_si256((__m256i*) &arr[i], _res);
  }//End for

【问题讨论】:

  • 对于编译时间常数范围,使用乘法逆进行除法。例如,就像我在What's the fastest way to generate a 1 GB text file containing random digits? 中所做的那样,将 AVX2 xorshift+ 输出中的熵切碎为 base-10 数字,使用 GNU C 向量扩展让编译器生成实际的乘法常数和立即移位。 (当然,您可以将该 asm 移植回内部函数以与其他编译器一起使用。)在我的情况下,16 位块很有用,这是 x86 的元素大小具有 SIMD 乘法。
  • 几乎是 Vectorized Ranged Random number generation across all types 的副本,其中提到了 libdivide。对于您的原始标题,只是在不讨论随机数的情况下进行模运算,它将是 SSE integer division? 的副本,其中提到了 libdivide 和 VCL(github.com/vectorclass/version2/blob/… - VCL 具有乘法逆运算,其方式适用于常量传播和提升没有循环)。
  • stackoverflow.com/questions/30790184/… 由 njuffa 评论的 Montgomery 的论文显示了通用算法(或少数算法,因为某些模不需要所有后校正步骤)除以倒数乘法。因此,最好制作一个能返回最优序列的生成器...
  • 另外,你知道你可以做到__m256i range = _mm256_set1_epi32(arg2 - arg1 + 1),对吧?不需要鼓励愚蠢的编译器(cough MSVC)在循环内部或根本在 SIMD 寄存器中实际执行那些循环不变操作。
  • 而不是rand() % range,您通常会使用(rand() * range) / RAND_MAX 获得类似分布良好的数字(这在很大程度上取决于您的RNG)。对于 32 位整数,这对于 AVX2 来说也不是很简单,但它应该比计算余数要快得多。

标签: c++ random simd modulo avx


【解决方案1】:

如果您的范围小于约 1670 万,并且您不需要加密级别的分布质量,那么缩小这些随机数的一种简单且相对快速的方法是 FP32 数学。

这是一个未经测试的示例。 下面的函数采用带有随机位的整数向量,并将这些位转换为 [ 0 .. range - 1 ] 区间内的整数。

// Ideally, make sure this function is inlined,
// by applying __forceinline for vc++ or __attribute__((always_inline)) for gcc/clang
inline __m256i narrowRandom( __m256i bits, int range )
{
    assert( range > 1 );

    // Convert random bits into FP32 number in [ 1 .. 2 ) interval
    const __m256i mantissaMask = _mm256_set1_epi32( 0x7FFFFF );
    const __m256i mantissa = _mm256_and_si256( bits, mantissaMask );
    const __m256 one = _mm256_set1_ps( 1 );
    __m256 val = _mm256_or_ps( _mm256_castsi256_ps( mantissa ), one );

    // Scale the number from [ 1 .. 2 ) into [ 0 .. range ),
    // the formula is ( val * range ) - range
    const __m256 rf = _mm256_set1_ps( (float)range );
    val = _mm256_fmsub_ps( val, rf, rf );

    // Convert to integers
    // The instruction below always truncates towards 0 regardless on MXCSR register.
    // If you want ranges like [ -10 .. +10 ], use _mm256_add_epi32 afterwards
    return _mm256_cvttps_epi32( val );
}

内联时,它应该编译成 4 条指令,vpandvorpsvfmsub132psvcvttps2dq 在您的示例中可能比 _mm256_rem_epu32 快一个数量级。

【讨论】:

  • 我查看了其他一些解决方案,您的解决方案是迄今为止最简单和最快的。我向上帝、Lisa Su 和 Pat Gelsinger 祈祷,有一天我们会在硬件中获得实际的向量整数除法和模数指令。因为在我看来,这确实是一种荒谬的能力。 (矢量 fmod 也会很不错...)
  • 如何为 64 位整数实现这一点?只是将所有内容更改为使用双精度(包括设置 mantissaMask = 0x7FFFFFFFFFFFFFFFF)会导致一些数字溢出到或接近 UINT64_T::MAX。即我想要范围 1-100 并得到如下结果:4,377,922,996,578,428,928
  • @dave_thenerd 不知道你有什么改变,但如果你想要在 [1 .. 100] 范围内的随机 64 位数字,使用相同的 FP32 代码两倍窄(即在 16 字节上运行SSE 向量),最后加上_mm256_cvtepi32_epi64
猜你喜欢
  • 2014-02-25
  • 1970-01-01
  • 1970-01-01
  • 2016-05-04
  • 2017-06-27
  • 1970-01-01
  • 2011-05-09
相关资源
最近更新 更多