Должен ли я объявить двойной массив с номером блока графического процессора во внутреннем или внешнем измерении?
например, должен ли я сделать
int payload[LEN][BLOCKS];
или
int payload[BLOCKS][LEN];
где LEN — очень большое число.
Я планирую, чтобы каждый блок проходил по двойному массиву, сохраняя постоянную размерность блока и повторяя размерность LEN.
Предполагая, что вы собираетесь получать доступ к данным блочно-ориентированным способом, вы хотите сделать последнее. Предположительно, это связано с тем, что когда вы загружаете первый элемент измерения "len", вы уже заплатили за отсутствие в кеше последующих семи элементов. В первом варианте возможно совместное использование строк кэша между блоками графического процессора, но совместное использование относительно ограничено и не на таком низком уровне.
Действительно, приведенный ниже код сообщает, что для выполнения второго варианта требуется 0,481 секунды, а для первого варианта — 0,979 секунды. Расположение данных с помощью блока во внешнем измерении примерно в два раза эффективнее.
#include <cuda_runtime_api.h>
#include <cuda.h>
#include <string>
#include <chrono>
#include <iostream>
#define BLOCKS 80
#define LEN (1 << 20)
void CheckCudaErrorAux (const char *file, unsigned line, const char *statement, cudaError_t err) {
if (err == cudaSuccess)
return;
std::cerr << statement<<" returned " << cudaGetErrorString(err) << "("<<err<< ") at "<<file<<":"<<line << std::endl;
exit (1);
}
#define CUDA_CHECK_RETURN(value) CheckCudaErrorAux(__FILE__,__LINE__, #value, value)
struct Data1 {
int payload[LEN][BLOCKS];
};
struct Data2 {
int payload[BLOCKS][LEN];
};
__global__ void f1(Data1 * data1) {
int sum = 0;
for (int i = 0; i < LEN; ++i) {
sum += data1->payload[i][blockIdx.x];
}
printf("block %i has f1 sum %i\n", blockIdx.x, sum);
}
__global__ void f2(Data2 * data2) {
int sum = 0;
for (int i = 0; i < LEN; ++i) {
sum += data2->payload[blockIdx.x][i];
}
printf("block %i has f2 sum %i\n", blockIdx.x, sum);
}
int main() {
Data1 * data1 = (Data1 *) malloc(sizeof(Data1));
Data2 * data2 = (Data2 *) malloc(sizeof(Data2));;
for (int i = 0; i < LEN; ++i) {
for (int b = 0; b < BLOCKS; ++b) {
data1->payload[i][b] = i * b;
data2->payload[b][i] = i * b;
}
}
Data1 * data1_on_gpu;
CUDA_CHECK_RETURN(cudaMalloc(&data1_on_gpu, sizeof(Data1)));
Data2 * data2_on_gpu;
cudaMalloc(&data2_on_gpu, sizeof(Data2));
CUDA_CHECK_RETURN(cudaDeviceSynchronize());
cudaMemcpy(data1_on_gpu, data1, sizeof(Data1), cudaMemcpyHostToDevice);
cudaMemcpy(data2_on_gpu, data2, sizeof(Data1), cudaMemcpyHostToDevice);
CUDA_CHECK_RETURN(cudaDeviceSynchronize());
std::chrono::time_point<std::chrono::system_clock> t1 = std::chrono::system_clock::now();
f1<<<80,1>>>(data1_on_gpu);
CUDA_CHECK_RETURN(cudaDeviceSynchronize());
std::chrono::time_point<std::chrono::system_clock> t2 = std::chrono::system_clock::now();
f2<<<80,1>>>(data2_on_gpu);
CUDA_CHECK_RETURN(cudaDeviceSynchronize());
std::chrono::time_point<std::chrono::system_clock> t3 = std::chrono::system_clock::now();
std::chrono::duration<double> duration_1_to_2 = t2 - t1;
std::chrono::duration<double> duration_2_to_3 = t3 - t2;
duration_1_to_2.count();
printf("timer for 1st took %.3lf\n", duration_1_to_2.count());
printf("timer for 2nd took %.3lf\n", duration_2_to_3.count());
}
ни один из них не является эффективным шаблоном доступа, и вы никогда не захотите запускать блоки, состоящие из одного потока на блок, если вас интересует производительность на GPU. Кроме того, неясно, что вы понимаете, что такое параметры конфигурации запуска ядра. Если вы не верите, что вашему ядру f2
требуется ровно 1 байт динамически выделяемой разделяемой памяти (это не так), конфигурация <<<80,1,1>>>
не имеет особого смысла. В любом случае, <<<80,1>>>
или <<<80,1,1>>>
плохо влияют на производительность.
<<<80,1,1>>> была опечаткой — исправлено, спасибо. Полуочевидно, что мое реальное приложение имеет более одного потока на блок. Эти вопросы и ответы должны были помочь мне понять, как разместить одну из моих структур данных. В реальном приложении все потоки в блоке считываются из одной и той же записи в data[1/2]_on_gpu
, поэтому я предполагаю, что дополнительные потоки не будут иметь значения для ответа на этот вопрос.
В каком-то смысле я рад, что разница в производительности составляет Только в 2 раза. Для меня это означает, что аппаратное обеспечение прошло долгий путь, компенсируя «плохое» программирование. Если бы разница была значительно больше, то «плохой» выбор здесь мог бы повлиять на производительность моей программы. Как бы то ни было, это, вероятно, не имеет большого значения.
Конечно. Но лучше всего использовать 1D-массив вместо 2D. Двумерный массив фрагментирован, каждый из указателей будет непрерывным блоком памяти, но весь массив не будет непрерывным. Если вы используете 1D-массивы, возможно, вы можете еще больше ускорить его, используя постоянную или текстурную память (в зависимости от вашего приложения).