Заполнение вектора несколькими потоками

мне нужно заполнить огромный (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 секунды: как вы думаете, есть способ сделать его более эффективным?


Edit: давая каждому потоку свой собственный генератор

я изменил процедуру как следует

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.


Edit: давая каждому потоку свой собственный вектор, а затем объединяя

я изменил код следующим образом:

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;
}

время выполнения немного хуже, чем раньше, когда я использовал только один вектор, разделяемый между всеми потоками. Я упускаешь что-то или это просто случается?


Edit: использование различных 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 ответов


генератор std::mt19937_64 generator{rd()}; разделяется между потоками. Там будет некоторое общее состояние, которое требует обновления в нем, следовательно, раздор; есть гонка данных. Вы также должны позволить каждому потоку использовать свой собственный генератор - вам просто нужно убедиться, что они генерируют отдельные последовательности.

у вас, возможно, есть проблема с кешем вокруг std::vector<unsigned int> v;, он объявляется вне потоков, а затем попадает с каждой итерацией цикла for в каждом потоке. Пусть каждый поток имейте свой собственный вектор для заполнения, как только все потоки будут сделаны, сопоставьте их результаты в векторе v. Возможно, через std::future будет быстрее. точный размер спора зависит от размеров строки кэша и размера используемого вектора (и сегментированного).

в этом случае вы заполняете большое количество элементов (7734500) сравнительно небольшим количеством потоков (4), соотношение, возможно, приведет к меньшему количеству споров.

У. Р.Т. в количество потоков, вы могли бы использовать, вы должны рассмотреть возможность привязки NUM_THREADS к аппаратному параллелизму, доступному на цели; т. е. std::thread::hardware_concurrency().

при работе с этим большим количеством элементов вы также можете избежать ненужных инициализаций и перемещения результатов (хотя и учитывая int введите, движение здесь менее не заметно). Сам контейнер тоже нужно знать; vector требует непрерывной памяти, поэтому любые дополнительные элементы (во время коалиционная фаза) может привести к выделению и копированию памяти.

скорость генератора случайных чисел также может оказать влияние, другие реализации и / или алгоритмы могут повлиять на конечное время выполнения достаточно значительно, чтобы считаться.

как всегда со всеми вопросами на основе производительности - окончательное решение требует измерения. Реализуйте возможные решения, измеряйте целевые процессоры и среды и адаптируйтесь до соответствующее представление найдено.


генератор вихря Мерсенна (std::mt19937_64) не слишком быстро. Вы можете рассмотреть другие генераторы, такие как Xorshift+. См., например, этот вопрос:каков наилучший способ генерации случайных булов с точки зрения производительности? (обсуждение там выходит за рамки просто bools).

и вы должны избавиться от гонки данных в коде. Использовать один генератор на поток.


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

к сожалению, std::vector::resize value-intialize примитивы, а также, что делает вашу программу один раз записать нули над векторной памяти, а затем переопределение этого значения со случайными числами.

попробовать std::vector::reserve + std::vector::push_back.
это означает, что потоки больше не могут делиться вектором без блокировки, но вы можете дать каждому свой собственный вектор, используйте reserve+push_back затем объединить все результаты в больший вектор.

если этого недостаточно, и я ненавижу это говорить, используйте std::unique_ptr С malloc (С делетер костюм). да, это C, да это противно, да, у нас new[] , а malloc не будет нулевой инициализации памяти (в отличие от new[] и контейнеры stl), затем вы можете распространять сегменты памяти на каждый поток и позволять ему генерировать случайное число на нем. вы сэкономите сочетание векторов в один вектор.