如何在C+;中高效地生成排序的均匀分布随机数+;? < >我想生成大量的,C++中的排序和均匀分布随机数的代码< n>代码>,即 n>=1000000000 < /> > >

如何在C+;中高效地生成排序的均匀分布随机数+;? < >我想生成大量的,C++中的排序和均匀分布随机数的代码< n>代码>,即 n>=1000000000 < /> > > ,c++,algorithm,sorting,random,c++17,C++,Algorithm,Sorting,Random,C++17,我考虑的第一个简单方法是 使用std::uniform_real_distribution按顺序生成n均匀分布的数字 然后使用std::sort对它们进行排序 然而,这需要几分钟 第二种也是更复杂的方法是将两个步骤并行化,如下所示: template <typename T> void computeUniformDistribution(std::vector<T>& elements) { #pragma omp parallel {

我考虑的第一个简单方法是

  • 使用
    std::uniform_real_distribution
    按顺序生成
    n
    均匀分布的数字
  • 然后使用
    std::sort
    对它们进行排序
  • 然而,这需要几分钟

    第二种也是更复杂的方法是将两个步骤并行化,如下所示:

    template <typename T>
    void computeUniformDistribution(std::vector<T>& elements)
    {
        #pragma omp parallel
        {
            std::seed_seq seed{distribution_seed, static_cast<size_t>(omp_get_thread_num())};
            std::mt19937 prng = std::mt19937(seed);
            std::uniform_real_distribution<double> uniform_dist(0, std::numeric_limits<T>::max());
    
            #pragma omp for
            for (size_t i = 0; i < elements.size(); ++i)
            {
                elements[i] = static_cast<T>(uniform_dist(prng));
            }
        }
    
        std::sort(std::execution::par_unseq, elements.begin(), elements.end());
    }
    
    模板
    无效计算分布(标准::向量和元素)
    {
    #pragma-omp并行
    {
    std::seed_seq seed{distribution_seed,static_cast(omp_get_thread_num())};
    标准::mt19937 prng=标准::mt19937(种子);
    标准::均匀实分布均匀分布(0,标准::数值极限::max());
    #pragma omp for
    对于(size_t i=0;i
    然而,即使这样也需要大约30秒。考虑到生成均匀分布的数字只需约1.5秒,瓶颈仍然是排序阶段


    因此,我想问以下问题:如何有效地以排序方式生成均匀分布的数据?

    有一些方法可以生成已排序的样本,但我认为生成部分排序的样本可能更好

    将输出范围划分为等宽的k个桶。每个桶中的样本数将具有概率相等的多项式分布。对多项式分布进行采样的慢方法是在[0,k]中生成n个整数。更有效的方法是以n/k的和不超过n为条件绘制k个泊松样本,然后使用慢方法添加另一个n和样本。泊松分布的采样很难做到完美,但当n/k非常大时(如本文所示),泊松分布通过将正态分布与均值和方差n/k四舍五入而得到了很好的近似。如果这是不可接受的,那么慢速方法也可以很好地并行

    给定bucket计数,计算前缀和以找到bucket边界。对于并行的每个bucket,生成bucket范围内给定数量的样本并对其进行排序。如果我们选择n/k,每个bucket几乎肯定适合一级缓存。对于n=1e9,我想我会尝试k=1e5或k=1e6

    这是一个顺序实现。有点不完善,因为我们确实需要避免对桶边界进行2倍的过采样,这些边界是闭合的,但我将留给您。我不熟悉OMP,但我认为您可以通过在
    SortedUniformSamples
    末尾的for循环中添加pragma来获得一个相当好的并行实现>
    #include <algorithm>
    #include <cmath>
    #include <iostream>
    #include <numeric>
    #include <random>
    #include <span>
    #include <vector>
    
    template <typename Dist, typename Gen>
    void SortedSamples(std::span<double> samples, Dist dist, Gen& gen) {
      for (double& sample : samples) {
        sample = dist(gen);
      }
      std::sort(samples.begin(), samples.end());
    }
    
    template <typename Gen>
    void ApproxMultinomialSample(std::span<std::size_t> samples, std::size_t n,
                                 Gen& gen) {
      double lambda = static_cast<double>(n) / samples.size();
      std::normal_distribution<double> approx_poisson{lambda, std::sqrt(lambda)};
      std::size_t sum;
      do {
        for (std::size_t& sample : samples) {
          sample = std::lrint(approx_poisson(gen));
        }
        sum = std::accumulate(samples.begin(), samples.end(), std::size_t{0});
      } while (sum > n);
      std::uniform_int_distribution<std::size_t> uniform{0, samples.size() - 1};
      for (; sum < n; sum++) {
        samples[uniform(gen)]++;
      }
    }
    
    template <typename Gen>
    void SortedUniformSamples(std::span<double> samples, Gen& gen) {
      static constexpr std::size_t kTargetBucketSize = 1024;
      if (samples.size() < kTargetBucketSize) {
        SortedSamples(samples, std::uniform_real_distribution<double>{0, 1}, gen);
        return;
      }
      std::size_t num_buckets = samples.size() / kTargetBucketSize;
      std::vector<std::size_t> bucket_counts(num_buckets);
      ApproxMultinomialSample(bucket_counts, samples.size(), gen);
      std::vector<std::size_t> prefix_sums(num_buckets + 1);
      std::partial_sum(bucket_counts.begin(), bucket_counts.end(),
                       ++prefix_sums.begin());
      for (std::size_t i = 0; i < num_buckets; i++) {
        SortedSamples(std::span<double>{&samples[prefix_sums[i]],
                                        &samples[prefix_sums[i + 1]]},
                      std::uniform_real_distribution<double>{
                          static_cast<double>(i) / num_buckets,
                          static_cast<double>(i + 1) / num_buckets},
                      gen);
      }
    }
    
    int main() {
      std::vector<double> samples(100000000);
      std::default_random_engine gen;
      SortedUniformSamples(samples, gen);
      if (std::is_sorted(samples.begin(), samples.end())) {
        std::cout << "sorted\n";
      }
    }
    

    我运行了一些测试,根据系统的不同,基数排序的速度是std::sort的4到6倍,但它需要第二个向量,对于1GB的元素,每个双精度向量是8GB,总共有16GB的可用内存,因此可能需要32GB的RAM

    如果排序不受内存带宽限制,则多线程基数排序可能会有所帮助

    单线程代码示例:

    #include <algorithm>
    #include <iostream>
    #include <random>
    #include <vector>
    #include <time.h>
    
    clock_t ctTimeStart;            // clock values
    clock_t ctTimeStop;
    
    typedef unsigned long long uint64_t;
    
    //  a is input array, b is working array
    uint64_t * RadixSort(uint64_t * a, uint64_t *b, size_t count)
    {
    uint32_t mIndex[8][256] = {0};          // count / index matrix
    uint32_t i,j,m,n;
    uint64_t u;
        for(i = 0; i < count; i++){         // generate histograms
            u = a[i];
            for(j = 0; j < 8; j++){
                mIndex[j][(size_t)(u & 0xff)]++;
                u >>= 8;
            }
        }
        for(j = 0; j < 8; j++){             // convert to indices
            m = 0;
            for(i = 0; i < 256; i++){
                n = mIndex[j][i];
                mIndex[j][i] = m;
                m += n;
            }
        }
        for(j = 0; j < 8; j++){             // radix sort
            for(i = 0; i < count; i++){     //  sort by current LSB
                u = a[i];
                m = (size_t)(u>>(j<<3))&0xff;
                b[mIndex[j][m]++] = u;
            }
            std::swap(a, b);                //  swap ptrs
        }
        return(a);
    }
    
    #define COUNT (1024*1024*1024)
    
    int main(int argc, char**argv)
    {
        std::vector<double> v(COUNT);       // vctr to be generated
        std::vector<double> t(COUNT);       // temp vector
        std::random_device rd;
        std::mt19937 gen(rd());
    //  std::uniform_real_distribution<> dis(0, std::numeric_limits<double>::max());
        std::uniform_real_distribution<> dis(0, COUNT);
        ctTimeStart = clock();
        for(size_t i = 0; i < v.size(); i++)
            v[i] = dis(gen);
        ctTimeStop = clock();
        std::cout << "# of ticks " << ctTimeStop - ctTimeStart << std::endl;
        ctTimeStart = clock();
    //  std::sort(v.begin(), v.end());
        RadixSort((uint64_t *)&v[0], (uint64_t *)&t[0], COUNT);
        ctTimeStop = clock();
        std::cout << "# of ticks " << ctTimeStop - ctTimeStart << std::endl;
        return(0);
    }
    

    我倾向于依赖这样一个事实,即一组已排序的均匀分布变量的连续元素之间的差异呈指数分布。这可以被利用来在
    O(N)
    时间内运行,而不是
    O(N*logn)

    快速实施将执行以下操作:

    template<typename T> void
    computeSorteUniform2(std::vector<T>& elements)
    {
        std::random_device rd;
        std::mt19937 prng(rd());
    
        std::exponential_distribution<T> dist(static_cast<T>(1));
    
        auto sum = dist(prng);
    
        for (auto& elem : elements) {
            elem = sum += dist(prng);
        }
    
        sum += dist(prng);
    
        for (auto& elem : elements) {
            elem /= sum;
        }
    }
    
    模板无效
    computeSorteUniform2(标准::向量和元素)
    {
    std::随机_装置rd;
    标准:mt19937 prng(rd());
    指数分布区(静态分布(1));
    自动求和=距离(prng);
    用于(自动和元素:元素){
    elem=总和+=距离(prng);
    }
    总和+=距离(prng);
    用于(自动和元素:元素){
    元素/=总和;
    }
    }
    
    假设您想要统一(0,1)中的值,可以简化此示例,但它应该很容易推广。使用OMP进行此工作不是很简单,但也不应该太难

    如果您关心最后50%的性能,有一些数字技巧可能会加快生成随机偏差(例如,PRNG比MT更快更好),并将其转换为
    double
    s(但最近的编译器可能知道这些技巧)。参考文献:和


    我刚刚对此进行了基准测试,发现clang的
    std::uniform\u real\u distribution
    std::exponential\u distribution
    都非常慢。速度快了8倍,这样我就可以在笔记本电脑上用一个线程在约10秒内生成1e9
    double
    (即
    std
    实现大约需要80秒)使用上述算法。我没有在1e9元素上尝试过OP的实现,但在1e8元素上,我的实现速度快了约15倍。

    有一个简单的观察,涉及[0,1]中的排序均匀随机数:

  • 每个均匀[0,1]数都同样可能小于一半或大于一半。因此,小于一半或大于一半的均匀[0,1]数遵循二项(n,1/2)分布
  • 在小于一半的数字中,每个数字都可能小于1/4,因为它可能大于1/4,所以小于1/4的数字与大于1/4的数字遵循相同的分布
  • 等等
  • 因此,每个数字可以一次生成一位,从二进制点后的左到右。下面是如何生成n个排序均匀随机数的示意图:

  • 如果n为0或1,停止。否则,生成b,一个二项(n,1/2)随机数
  • 将0附加到前b个随机数,将1附加到t
    // converting doubles to unsigned long long for radix sort or something similar
    // note -0 converted to 0x7fffffffffffffff, +0 converted to 0x8000000000000000
    // -0 is unlikely to be produced by a float operation
    
    #define SM2ULL(x) ((x)^(((~(x) >> 63)-1) | 0x8000000000000000ull))
    #define ULL2SM(x) ((x)^((( (x) >> 63)-1) | 0x8000000000000000ull))
    
    template<typename T> void
    computeSorteUniform2(std::vector<T>& elements)
    {
        std::random_device rd;
        std::mt19937 prng(rd());
    
        std::exponential_distribution<T> dist(static_cast<T>(1));
    
        auto sum = dist(prng);
    
        for (auto& elem : elements) {
            elem = sum += dist(prng);
        }
    
        sum += dist(prng);
    
        for (auto& elem : elements) {
            elem /= sum;
        }
    }