【问题标题】:Filling a vector with multiple threads用多个线程填充向量
【发布时间】:2016-06-03 17:43:23
【问题描述】:

我需要用随机值填充一个巨大的(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(如某些 cmets/answers 中所建议的)有很大帮助:我尝试使用 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 生成器是否共享似乎没有区别,但通过所有这些改进,执行时间已显着下降。

【问题讨论】:

  • 为什么你一创建线程就join?这与按顺序执行基本相同。
  • 另外,每个线程使用一个 RNG 可能比共享一个 RNG 更好。
  • 您在generator 上存在竞争,因为从多个线程访问不同步。让它thread_local 吧?
  • PRNG 有状态,因此通常不是线程安全的。
  • 而不是NUM_THREADS = 4; 试试NUM_THREADS = std::thread::hardware_concurrency();,也就是不去猜测核心数(有些核心支持超线程)

标签: c++ multithreading c++11


【解决方案1】:

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

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

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

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

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

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

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

【讨论】:

  • 我不认为有太多的缓存争用,每个线程只从某个起始位置接触v 的连续部分。争论只发生在边界和非常不同的时刻。在整理线程局部向量时也会出现同样的问题,并且使用此解决方案将执行更多的内存操作。
  • 写入将使缓存行无效,因此这在很大程度上取决于缓存行的大小等。为每个线程提供自己的写入向量(然后在最后进行整理)避免争用,无论争用多么激烈。
  • 是的,这就是我的观点,当每个线程只访问它的v 的连续部分时,唯一的共享缓存行就是这部分两端的缓存行。还有什么争议?
  • 正如 Daniel Langr 所写,在这个例子中。缓存争用仅发生在部分末尾的少数元素上。这可以通过将部件与缓存行对齐来优化,但它不太可能产生很大的影响。此外,使用多个向量保证数组位于不同的缓存行上,尽管大型数组通常由分配器放置在它们自己的页面范围内。
  • 而且,当线程局部向量被合并时,同样的争论是,使用局部向量 IMO 绝对没有任何好处。也许大卫海姆建议避免resize(),但我更喜欢基于自定义分配器的解决方案。
【解决方案2】:

Mersenne Twister 生成器 (std::mt19937_64) 并不太快。您可以考虑使用其他生成器,例如 Xorshift+。参见,例如,这个问题:What is performance-wise the best way to generate random bools?(那里的讨论不仅仅是布尔值)。

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

【讨论】:

    【解决方案3】:
      std::vector<unsigned int> v;
        v.resize(rows*columns);
    

    不幸的是,std::vector::resize value-initialize 原语也是如此,使您的程序一旦在向量内存上写入零,然后用随机数覆盖该值。

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

    如果这还不够,我不想这么说,请使用std::unique_ptrmalloc(带有服装删除器)。是的,这是 C,是的,这很讨厌,是的,我们有 new[] ,但是 malloc 不会将内存初始化为零(与 new[] 和 stl 容器不同),然后您可以将内存段分散到每个线程并让它在其上生成随机数。您将保存组合向量到一个组合向量。

    【讨论】:

    • 我也遇到过这个问题; resize() 具有数十亿个元素的向量,由于值初始化,耗时超过 20 秒。在 C++11(N3346 后标准更正)中,可以通过使用带有空 construct() 成员函数的自定义分配器来避免这种情况。
    • @DanielLangr,或者有一个vector&lt;X&gt;,其中Xunsigned int 的简单包装,它不会在其默认ctor 中初始化成员。
    • 当然,我在实践中使用了这个空构造函数解决方案来处理类/结构类型的元素。它也有效。
    猜你喜欢
    • 2018-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-26
    • 1970-01-01
    • 2012-04-12
    相关资源
    最近更新 更多