OpenMP реализация PCG PRNG

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

#include <chrono>
#include <iostream>
#include <omp.h>

#include "../include_random/pcg_basic.hpp"

int main()
{
    

    // /*Bloc 1*/
    // omp_set_num_threads (threadSNumber);
    // startingTime = std::chrono::system_clock::now();
    // #pragma omp parallel
    // {
    //     int threadID = omp_get_thread_num();
    //     pcg32_random_t rng;
    //     pcg32_srandom_r(&rng, time(NULL) ^ (intptr_t)&printf,(intptr_t)&threadID);
    //     // uint32_t bound =1;
    //     #pragma omp for reduction (+:sum)
    //     for (int step = 0; step < N; step++)
    //     {
    //         // sum += 0.5 - (double)pcg32_boundedrand_r(&rng,bound);
    //         sum += 0.5 -((double)pcg32_random_r(&rng)/(double)UINT32_MAX);
    //     }
    // }
    /**Bloc 2**/
    omp_set_num_threads (threadSNumber);
    pcg32_random_t *rng;
    rng = new pcg32_random_t[threadSNumber];
    #pragma omp parallel
    {
        int threadID = omp_get_thread_num();
        pcg32_srandom_r(&rng[threadID], time(NULL) ^ (intptr_t)&printf,(intptr_t)&threadID);
    }
    startingTime = std::chrono::system_clock::now();
    #pragma omp parallel
    {
        int threadID = omp_get_thread_num();
        #pragma omp for reduction (+:sum)
        for (int step = 0; step < N; step++)
        {
            sum += 0.5 -((double)pcg32_random_r(&rng[threadID])/(double)UINT32_MAX);
        }
    }
    delete[] rng;
    /****/
    auto end = std::chrono::system_clock::now();
    auto diff = end - startingTime;
    double total_time = chrono::duration <double, std::ratio<1>> (diff).count();
    cout << "The result of the sum is "<< sum/N << "\n" << endl;
    cout << "# Total time:  "<<  (int)total_time/3600<<"h    "<< ((int)total_time%3600)/60<<"m    "<< (int)total_time%60 << "s        (" << total_time << " s)" << endl;
    return 0;
}

Блок 1 масштабируется, как и ожидалось, с номером потока, а блок 2 — нет.

# thread    1     2    3    4
block1(s)  3.27 1.64 1.12 0.83
block2(s)  4.60 13.7 8.28 10.9

Эти примеры являются минимальными примерами для воспроизведения проблемы. Это часть большей функции, которая находится в большем коде.

Я хочу инициализировать начальное число только один раз, и каждый раз я вычисляю набор случайных чисел, которые используются в другой функции (не выполняя такую ​​сумму, которая здесь делается только для записи чего-либо). Можно использовать блок 1, но это означает, что я инициализирую начальное значение на каждом временном шаге, а не один раз. Более того, я не понимаю масштабирование блока2.

Что не так во 2 блоке? Почему я получаю это масштабирование? Там не используется то же самое rng, поэтому я должен избегать гонки данных, или я что-то неправильно понимаю.

Я действительно не читал ваш код, но вижу, что в массиве rng есть, вероятно, ложное совместное использование (более одного rng ​​в одной строке кэша). См. Понимание std::hardware_destructive_interference_size и std::hardware_constructive_interference_size

paleonix 08.01.2023 00:58

Я предполагаю, что функция time не является потокобезопасной. Поскольку вы хотите писать на C++, почему бы не использовать потокобезопасный chrono. (Кроме того, не используйте new.)

Victor Eijkhout 08.01.2023 02:40

* Используйте RAII вместо new, то есть std::vector или хотя бы std::make_unique.

paleonix 08.01.2023 02:49

Если я заменю указатель на vector<pcg32_random_t>, я получу меньше времени, но такое же масштабирование с номером потока. (s) 3.40 6.95 4.53 5.27. Я прочитал связанную тему, но я не понимаю, как решить проблему со строкой кэша в моем случае. Должен ли я изменить структуру pcg32_random_t?

Hunken 08.01.2023 21:08

Использование RAII связано с безопасностью памяти, а не с производительностью. Связанная функция стандартной библиотеки дает вам информацию о расстоянии, которое должно быть между двумя объектами в памяти, чтобы избежать ложного совместного использования (нахождения в одной строке кэша). См. использование alignas вместо cache_int в блоке кода первого ответа.

paleonix 08.01.2023 23:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
59
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

@paleonix Действительно, проблема возникает из-за ложного обмена. Этот код решает странное масштабирование с номером потока. Тем не менее, первый блок по-прежнему является самым быстрым.

#ifdef __cpp_lib_hardware_interference_size
    using std::hardware_constructive_interference_size;
    using std::hardware_destructive_interference_size;
#else
    // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ...
    constexpr std::size_t hardware_constructive_interference_size = 64;
    constexpr std::size_t hardware_destructive_interference_size = 64;
#endif

struct cache_pcg32 {
    alignas(hardware_destructive_interference_size) pcg32_random_t rng;
};

inline uint64_t timeForSeed()
{
    return std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
int main()
{
    

    // /*Block 1*/
    // omp_set_num_threads (threadSNumber);
    // startingTime = std::chrono::system_clock::now();
    // for (int i = 0; i < 200000; i++)
    // {
    //     #pragma omp parallel
    //     {
    //         int threadID = omp_get_thread_num();
    //         // int threadID = 1;
    //         pcg32_random_t rng;
    //         pcg32_srandom_r(&rng, timeForSeed() ^ (intptr_t)&printf,(intptr_t)&threadID);
    //         #pragma omp for reduction (+:sum)
    //         for (int step = 0; step < N; step++)
    //         {
    //             sum += 0.5 -((double)pcg32_random_r(&rng)/(double)UINT32_MAX);
    //         }
    //     }
    // } 
    
    /**Block 2**/
    startingTime = std::chrono::system_clock::now();
    omp_set_num_threads (threadSNumber);
    vector<cache_pcg32> rngVector;
    rngVector.resize(threadSNumber);

    #pragma omp parallel
    {
        int threadID = omp_get_thread_num();
        pcg32_srandom_r(&rngVector[threadID].rng, timeForSeed() ^ (intptr_t)&printf,(intptr_t)&threadID);
    }
    for (int i = 0; i < 200000; i++)
    {
        #pragma omp parallel
        {
            int threadID = omp_get_thread_num();
            #pragma omp for reduction (+:sum)
            for (int step = 0; step < N; step++)
            {
                sum += 0.5 -((double)pcg32_random_r(&rngVector[threadID].rng)/(double)UINT32_MAX);
            }
        }
    }

@Виктор Я изменил time(NULL) для функции timeForSeed(), чтобы использовать std::chrono, но, похоже, это ничего не меняет (производительность или результат суммы).

Другие вопросы по теме