- PVSM.RU - https://www.pvsm.ru -
В публикации «Сказка о потерянном времени» [1] пользователь crea7or [2] рассказал, как он опровергал Гипотезу Эйлера [3] на современном CPU.
Мне же было интересно узнать как покажет себя GPU, и я сравнил однопоточный код с многопоточным для CPU и совсем многопоточным для GPU, с помощью архитектуры параллельных вычислений CUDA [4].
Для начала я распараллелил алгоритм [8] для CPU. Для этого в функцию передается диапазон для перебора значений внешнего цикла a. Затем весь диапазон 1..N разбивается на кусочки и скармливается ядрам процессора.
void Algorithm_1(const uint32_t N, const std::vector<uint64_t>& powers, const uint32_t from, const uint32_t to) {
for (uint32_t a = from; a < to; ++a) {
for (uint32_t b = 1; b < a; ++b) {
for (uint32_t c = 1; c < b; ++c) {
for (uint32_t d = 1; d < c; ++d) {
const uint64_t sum = powers[a] + powers[b] + powers[c] + powers[d];
if (std::binary_search(std::begin(powers), std::end(powers), sum)) {
const auto it = std::lower_bound(std::begin(powers), std::end(powers), sum);
uint32_t e = static_cast<uint32_t>(std::distance(std::begin(powers), it));
std::cout << a << " " << b << " " << c << " " << d << " " << e << "n";
}
}
}
}
}
}
В моем случае было 4 ядра, и вычисления я выполнял через чуть-чуть доработанный пул потоков, который взял за основу тут [9]. Так как никаких зависимостей между данными нет, скорость возрастает практически пропорционально количеству ядер.
Для GPU получается немного сложнее. Внешние два цикла перебора (a и b) будут развернуты, а в функции останется только перебор двух внутренних циклов (c и d). Можно себе представить что программа будет выполняться параллельно для всех значений а = 1..N и b = 1..N. На самом деле конечно это не так, все исполняться параллельно они все-таки не смогут, но это уже забота CUDA максимально распараллелить выполнение, этому можно помочь, если правильно настроить конфигурацию блоков и потоков.
int blocks_x = N;
int threads = std::min(1024, static_cast<int>(N));
int blocks_y = (N + threads - 1) / threads;
dim3 grid(blocks_x, blocks_y);
NaiveGPUKernel<<<grid, threads>>>(N);
blocks_x — это диапазон перебора первого цикла a.
А вот перебор второго цикла b пришлось сделать сложнее из-за ограничения видеокарты на количество потоков (1024), и поместить счетчик одновременно в threads и blocks_y:
a и b вычисляется так:
const int a = blockIdx.x + 1;
const int b = threadIdx.x + blockIdx.y * blockDim.x + 1;
Это точно не оптимальная конфигурация, так как получается много холостых циклов. Но дальше подстраивать значения я не стал.
Код для GPU получается вполне прямолинейный и очень похожий на CPU, только бинарный поиск приходится сделать руками.
__constant__ uint64_t gpu_powers[8192];
inline __device__ int gpu_binary_search(const uint32_t N, const uint64_t search) {
uint32_t l = 0;
uint32_t r = elements_count - 1;
uint32_t m;
while (l <= r) {
m = (l + r) / 2;
if (search < gpu_powers[m])
r = m - 1;
else if (search > gpu_powers[m])
l = m + 1;
else
return l;
}
return -1;
}
__global__ void NaiveGPUKernel(const uint32_t N) {
const int a = blockIdx.x + 1;
const int b = threadIdx.x + blockIdx.y * blockDim.x + 1;
if (b >= a)
return;
for (uint32_t c = 1; c < b; ++c) {
for (uint32_t d = 1; d < c; ++d) {
const uint64_t sum = gpu_powers[a] + gpu_powers[b] + gpu_powers[c] + gpu_powers[d];
const auto s = gpu_binary_search(N, sum);
if (s > 0) {
printf("%d %d %d %d %dn", a, b, c, d, s);
}
}
}
}
Так же я реализовал оптимизации, подсказанные тут [8].
Замеры для CPU делались по два раза, так как они оказались намного стабильнее GPU, которых я делал уже по семь и выбирал лучшее время. Разброс для GPU мог быть двукратным, это я объясняю тем что для CUDA-расчетов использовался единственный в системе видеоадаптер.
N | CPU время, мс | CPU (4 потока) время, мс | GPU время, мс | |
---|---|---|---|---|
@antonkrechetov [10] | 100 | 58.6 | 16.7 | 14.8 |
Базовый [8] | 100 | 45.3 | 13.0 | 10.7 |
Базовый оптимизация #1 [8] | 100 | 6.3 | 2.1 | 12.7 |
Базовый оптимизация #2 [8] | 100 | 1.4 | 0.7 | 0.8 |
@antonkrechetov [10] | 250 | 2999.7 | 782.9 | 119.0 |
Базовый [8] | 250 | 2055.6 | 550.1 | 90.9 |
Базовый оптимизация #1 [8] | 250 | 227.2 | 60.3 | 109.2 |
Базовый оптимизация #2 [8] | 250 | 42.9 | 11.9 | 6.0 |
@antonkrechetov [10] | 500 | 72034.2 | 19344.1 | 1725.83 |
Базовый [8] | 500 | 38219.7 | 10200.8 | 976.7 |
Базовый оптимизация #1 [8] | 500 | 3725.1 | 926.5 | 1140.36 |
Базовый оптимизация #2 [8] | 500 | 630.7 | 170.2 | 48.7 |
@antonkrechetov [10] | 750 | 396566.0 | 105003.0 | 11521.2 |
Базовый [8] | 750 | 218615.0 | 57639.2 | 5742.5 |
Базовый оптимизация #1 [8] | 750 | 19082.7 | 4736.8 | 6402.1 |
Базовый оптимизация #2 [8] | 750 | 3272.0 | 846.9 | 222.9 |
Базовый оптимизация #2 [8] | 1000 | 10204.4 | 2730.3 | 1041.9 |
Базовый оптимизация #2 [8] | 1250 | 25133.1 | 6515.3 | 2445.5 |
Базовый оптимизация #2 [8] | 1500 | 51940.1 | 14005.0 | 4895.2 |
И этим адреналином будет Profile-guided optimization [11].
Для PGO я привожу только время CPU, потому что GPU мало изменилось, и это ожидаемо.
N | CPU время, мс |
CPU (4 потока) время, мс |
CPU+PGO время, мс |
CPU+PGO (4 потока) время, мс |
|
---|---|---|---|---|---|
@antonkrechetov [10] | 100 | 58.6 | 16.7 | 55.3 | 15.0 |
Базовый [8] | 100 | 45.3 | 13.0 | 42.2 | 12.1 |
Базовый оптимизация #1 [8] | 100 | 6.3 | 2.1 | 5.2 | 1.9 |
Базовый оптимизация #2 [8] | 100 | 1.4 | 0.7 | 1.3 | 0.8 |
@antonkrechetov [10] | 250 | 2999.7 | 782.9 | 2966.1 | 774.1 |
Базовый [8] | 250 | 2055.6 | 550.1 | 2050.2 | 544.6 |
Базовый оптимизация #1 [8] | 250 | 227.2 | 60.3 | 200.0 | 53.2 |
Базовый оптимизация #2 [8] | 250 | 42.9 | 11.9 | 40.4 | 11.4 |
@antonkrechetov [10] | 500 | 72034.2 | 19344.1 | 68662.8 | 17959.0 |
Базовый [8] | 500 | 38219.7 | 10200.8 | 38077.7 | 10034.0 |
Базовый оптимизация #1 [8] | 500 | 3725.1 | 926.5 | 3132.9 | 822.2 |
Базовый оптимизация #2 [8] | 500 | 630.7 | 170.2 | 618.1 | 160.6 |
@antonkrechetov [10] | 750 | 396566.0 | 105003.0 | 404692.0 | 103602.0 |
Базовый [8] | 750 | 218615.0 | 57639.2 | 207975.0 | 54868.2 |
Базовый оптимизация #1 [8] | 750 | 19082.7 | 4736.8 | 15496.4 | 4082.3 |
Базовый оптимизация #2 [8] | 750 | 3272.0 | 846.9 | 3093.8 | 812.7 |
Базовый оптимизация #2 [8] | 1000 | 10204.4 | 2730.3 | 9781.6 | 2565.9 |
Базовый оптимизация #2 [8] | 1250 | 25133.1 | 6515.3 | 23704.3 | 6244.1 |
Базовый оптимизация #2 [8] | 1500 | 51940.1 | 14005.0 | 48717.5 | 12793.5 |
Видно что PGO смогло ускорить оптимизацию #1 на целых 18%, для остальных прирост оказался скромнее.
Для CPU попробовать развернуть циклы, считать по два числа за раз, использовать векторные команды процессора.
Для GPU поиграть с порядком вычислений, попробовать по-экономить на регистрах, лучше подобрать параметры блоков и потоков.
1. Писать на CUDA просто.
2. Без ухищрений, прямолинейный код на CUDA оказался быстрее CPU реализации в десятки раз.
3. В числодробильных задачах GPU сильно быстрее CPU за те же деньги.
4. GPU требуется много времени на «раскачку» и для совсем коротких задач ускорения может не быть.
5. Более сложный алгоритм может оказаться быстрее для CPU, но медленнее для GPU.
6. Профильная оптимизация хорошо ускоряет код и уменьшает размер файла одновременно.
Традиционно можно потрогать код на GitHub [12]
Автор: drBasic
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/algoritmy/223828
Ссылки в тексте:
[1] «Сказка о потерянном времени»: https://habrahabr.ru/post/317588/
[2] crea7or: https://habrahabr.ru/users/crea7or/
[3] Гипотезу Эйлера: https://ru.wikipedia.org/wiki/Гипотеза_Эйлера
[4] CUDA: http://www.nvidia.ru/object/cuda-parallel-computing-ru.html
[5] Core i5 4440: http://ark.intel.com/products/75038/Intel-Core-i5-4440-Processor-6M-Cache-up-to-3_30-GHz
[6] GeForce GTX 770: http://www.nvidia.ru/object/geforce-gtx-770-ru
[7] MSVS 2015 update 3: https://www.microsoft.com/ru-ru/download/details.aspx?id=48146
[8] алгоритм: http://rosettacode.org/wiki/Euler%27s_sum_of_powers_conjecture#C.2B.2B
[9] тут: https://github.com/progschj/ThreadPool/blob/master/ThreadPool.h
[10] @antonkrechetov: https://habrahabr.ru/post/317588/#comment_9965062
[11] Profile-guided optimization: https://ru.wikipedia.org/wiki/Profile-guided_optimization
[12] GitHub: https://github.com/drbasic/EulerSum
[13] Источник: https://habrahabr.ru/post/318066/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.