Я пытаюсь генерировать случайные числа с помощью метода 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
, поэтому я должен избегать гонки данных, или я что-то неправильно понимаю.
Я предполагаю, что функция time
не является потокобезопасной. Поскольку вы хотите писать на C++, почему бы не использовать потокобезопасный chrono
. (Кроме того, не используйте new
.)
* Используйте RAII вместо new
, то есть std::vector
или хотя бы std::make_unique
.
Если я заменю указатель на vector<pcg32_random_t>
, я получу меньше времени, но такое же масштабирование с номером потока. (s) 3.40 6.95 4.53 5.27
. Я прочитал связанную тему, но я не понимаю, как решить проблему со строкой кэша в моем случае. Должен ли я изменить структуру pcg32_random_t
?
Использование RAII связано с безопасностью памяти, а не с производительностью. Связанная функция стандартной библиотеки дает вам информацию о расстоянии, которое должно быть между двумя объектами в памяти, чтобы избежать ложного совместного использования (нахождения в одной строке кэша). См. использование alignas
вместо cache_int
в блоке кода первого ответа.
@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
, но, похоже, это ничего не меняет (производительность или результат суммы).
Я действительно не читал ваш код, но вижу, что в массиве rng есть, вероятно, ложное совместное использование (более одного rng в одной строке кэша). См. Понимание std::hardware_destructive_interference_size и std::hardware_constructive_interference_size