使用多个线程填充向量

时间:2016-02-22 10:28:56

标签: c++ multithreading c++11

我需要用随机值填充一个巨大的(7734500元素)std::vector<unsigned int>,我试图与多个线程并行地实现它以实现更高的效率。这是我到目前为止的代码:

std::random_device rd; // seed generator

std::mt19937_64 generator{rd()}; // generator initialized with seed from rd

static const unsigned int NUM_THREADS = 4;


std::uniform_int_distribution<> initialize(unsigned long long int modulus)
{
    std::uniform_int_distribution<> unifDist{0, (int)(modulus-1)};
    return unifDist;
}


void unifRandVectorThreadRoutine
    (std::vector<unsigned int>& vector, unsigned int start,
    unsigned int end, std::uniform_int_distribution<>& dist)
{
    for(unsigned int i = start ; i < end ; ++i)
    {
        vector[i] = dist(generator);
    }
}


std::vector<unsigned int> uniformRandomVector
    (unsigned int rows, unsigned int columns, unsigned long long int modulus)
{
    std::uniform_int_distribution<> dist = initialize(modulus);

    std::thread threads[NUM_THREADS];

    std::vector<unsigned int> v;
    v.resize(rows*columns);

    // number of entries each thread will take care of
    unsigned int positionsEachThread = rows*columns/NUM_THREADS;

    // all but the last thread
    for(unsigned int i = 0 ; i < NUM_THREADS - 1 ; ++i)
    {
        threads[i] = std::thread(unifRandVectorThreadRoutine, v, i*positionsEachThread,
            (i+1)*positionsEachThread, dist);
        // threads[i].join();
    }

    // last thread
    threads[NUM_THREADS - 1] = std::thread(unifRandVectorThreadRoutine, v,
        (NUM_THREADS-1)*positionsEachThread, rows*columns, dist);
    // threads[NUM_THREADS - 1].join();

    for(unsigned int i = 0 ; i < NUM_THREADS ; ++i)
    {
        threads[i].join();
    }

    return v;
}

目前,大约需要0.3秒:您认为有办法提高效率吗?

编辑:为每个线程提供自己的生成器

我修改了例程如下

void unifRandVectorThreadRoutine
    (std::vector<unsigned int>& vector, unsigned int start,
    unsigned int end, std::uniform_int_distribution<>& dist)
{
    std::mt19937_64 generator{rd()};
    for(unsigned int i = start ; i < end ; ++i)
    {
        vector[i] = dist(generator);
    }
}

并且运行时间减少了一半。所以我仍在分享std::random_device,但每个帖子都有自己的std::mt19937_64

编辑:为每个帖子提供自己的向量,然后连接

我更改了代码如下:

void unifRandVectorThreadRoutine
    (std::vector<unsigned int>& vector, unsigned int length,
    std::uniform_int_distribution<>& dist)
{
    vector.reserve(length);
    std::mt19937_64 generator{rd()};
    for(unsigned int i = 0 ; i < length ; ++i)
    {
        vector.push_back(dist(generator));
    }
}

std::vector<unsigned int> uniformRandomVector
    (unsigned int rows, unsigned int columns, unsigned long long int modulus)
{
    std::uniform_int_distribution<> dist = initialize(modulus);

    std::thread threads[NUM_THREADS];

    std::vector<unsigned int> v[NUM_THREADS];

    unsigned int positionsEachThread = rows*columns/NUM_THREADS;

    // all but the last thread
    for(unsigned int i = 0 ; i < NUM_THREADS - 1 ; ++i)
    {
        threads[i] = std::thread(unifRandVectorThreadRoutine, std::ref(v[i]), positionsEachThread, dist);
    }

    // last thread
    threads[NUM_THREADS - 1] = std::thread(unifRandVectorThreadRoutine, std::ref(v[NUM_THREADS-1]),
        rows*columns - (NUM_THREADS-1)*positionsEachThread, dist);

    for(unsigned int i = 0 ; i < NUM_THREADS ; ++i)
    {
        threads[i].join();
    }

    std::vector<unsigned int> finalVector;
    finalVector.reserve(rows*columns);

    for(unsigned int i = 0 ; i < NUM_THREADS ; ++i)
    {
        finalVector.insert(finalVector.end(), v[i].begin(), v[i].end());
    }

    return finalVector;
}

执行时间比之前略差,当时我只使用了一个在所有线程之间共享的向量。我错过了什么或者它会发生吗?

使用不同的PRNG +基准

编辑

使用不同的PRNG(如某些评论/答案中所建议的)有很多帮助:我尝试使用xorshift+,这是我正在使用的实现:

class xorShift128PlusGenerator
{
public:
    xorShift128PlusGenerator()
    {
        state[0] = rd();
        state[1] = rd();
    };


    unsigned long int next()
    {
        unsigned long int x = state[0];
        unsigned long int const y = state[1];
        state[0] = y;
        x ^= x << 23; // a
        state[1] = x ^ y ^ (x >> 17) ^ (y >> 26); // b, c
        return state[1] + y;
    }


private:
    std::random_device rd; // seed generator
    unsigned long int state[2];

};

然后例程如下

void unifRandVectorThreadRoutine
    (std::vector<unsigned int>& vector, unsigned int start,
    unsigned int end)
{
    xorShift128PlusGenerator prng;
    for(unsigned int i = start ; i < end ; ++i)
    {
        vector[i] = prng.next();
    }
}

由于我现在在家,而且我正在使用另一台(功能更强大的)机器,我重新测试以比较结果。这是我获得的:

  • Mersenne Twister每个线程有一个发生器:0.075秒
  • xorshift128 +所有线程之间共享:0.023秒
  • xorshift128 +每个线程有一个生成器:0.023秒

注意:执行时间在每次重复时都会有所不同。这些只是典型值。

因此,如果共享xorshift生成器似乎没有区别,但是通过所有这些改进,执行时间显着下降。

3 个答案:

答案 0 :(得分:8)

生成器std::mt19937_64 generator{rd()};在线程之间共享。将存在一些需要更新的共享状态,因此存在争用;有数据竞争。您还应该考虑允许每个线程使用自己的生成器 - 您只需要确保它们生成单独的序列。

你可能在std::vector<unsigned int> v;周围有一个缓存争用问题,它在线程之外声明,然后在每个线程中的for循环的每次迭代中命中。让每个线程都有自己的向量来填充,一旦完成所有线程,就将它们的结果整理到向量v中。可能通过std::future将是最快的。 争用的确切大小取决于缓存行大小和正在使用的向量(和分段)的大小

在这种情况下,您使用相对较少的线程(4)填充大量元素(7734500),该比率可能会导致较少的争用。

W.r.t。您可以使用的数字线程,您应该考虑将NUM_THREADS绑定到目标上可用的硬件并发;即std::thread::hardware_concurrency()

在处理大量元素时,您还可以避免不必要的初始化和移动结果(尽管给定int类型,此处移动不太明显)。容器本身也是值得注意的; vector需要连续的内存,因此任何其他元素(在联盟阶段)都可能导致内存分配和复制。

随机数生成器的速度也可能产生影响,其他实现和/或算法可能会大大影响最终执行时间以供考虑。

与所有基于绩效的问题一样 - 最终的解决方案需要衡量。实施可能的解决方案,测量目标处理器和环境,并进行调整,直到找到合适的性能。

答案 1 :(得分:3)

Mersenne Twister发电机(std::mt19937_64)并不太快。你可能会考虑其他生成器,如Xorshift +。参见,例如,这个问题:What is performance-wise the best way to generate random bools?(讨论不仅仅是bool)。

你应该摆脱代码中的数据竞争。每个线程使用一个生成器。

答案 2 :(得分:0)

  std::vector<unsigned int> v;
    v.resize(rows*columns);

不幸的是,std::vector::resize值初始化原语,使你的程序一旦在向量存储器上写零,然后用随机数覆盖这个值。

尝试std::vector::reserve + std::vector::push_back  这意味着线程不能在没有锁定的情况下共享向量,但是您可以为每个向量分配它自己的向量,使用reserve+push_back然后将所有结果组合到更大的向量。

如果这还不够,我不想这么说,请std::unique_ptr使用malloc(使用服装删除器)。是的,这是C,是的,这是令人讨厌的,是的,我们有new[],但是malloc赢了零内存(不像new[]和stl容器),那么你可以将内存段传播到每个线程,并让它在其上生成随机数。您将保存向量与一个组合向量的组合。