Oversigt
I denne vejledning undersøger vi styrken ved GPU-programmering med C++. Udviklere kan forvente utrolig ydeevne med C ++, og at få adgang til den fænomenale kraft i GPU'en med et lavt niveau sprog kan give nogle af de hurtigste beregninger, der er tilgængelige i øjeblikket.
Krav
Mens enhver maskine, der er i stand til at køre en moderne version af Linux, kan understøtte en C ++ - kompilator, skal du bruge en NVIDIA-baseret GPU for at følge denne øvelse. Hvis du ikke har en GPU, kan du skrue op for en GPU-drevet forekomst i Amazon Web Services eller en anden cloududbyder efter eget valg.
Hvis du vælger en fysisk maskine, skal du sikre dig, at de NVIDIA-proprietære drivere er installeret. Du kan finde instruktioner til dette her: https: // linuxhint.com / install-nvidia-drivers-linux /
Ud over driveren skal du bruge CUDA-værktøjssættet. I dette eksempel bruger vi Ubuntu 16.04 LTS, men der er downloads tilgængelige for de fleste større distributioner på følgende URL: https: // udvikler.nvidia.com / cuda-downloads
For Ubuntu ville du vælge .deb-baseret download. Den downloadede fil har ikke en .deb-udvidelse som standard, så jeg anbefaler at omdøbe den til at have en .deb i slutningen. Derefter kan du installere med:
sudo dpkg -i pakke-navn.debDu bliver sandsynligvis bedt om at installere en GPG-nøgle, og i så fald skal du følge instruktionerne for at gøre det.
Når du har gjort det, skal du opdatere dine arkiver:
sudo apt-get opdateringsudo apt-get install cuda -y
Når det er gjort, anbefaler jeg genstart for at sikre, at alt er korrekt indlæst.
Fordelene ved GPU-udvikling
CPU'er håndterer mange forskellige input og output og indeholder et stort udvalg af funktioner til ikke kun at håndtere et bredt udvalg af programbehov, men også til styring af forskellige hardwarekonfigurationer. De håndterer også hukommelse, caching, systembus, segmentering og IO-funktionalitet, hvilket gør dem til en jack of all trades.
GPU'er er det modsatte - de indeholder mange individuelle processorer, der er fokuseret på meget enkle matematiske funktioner. På grund af dette behandler de opgaver mange gange hurtigere end CPU'er. Ved at specialisere sig i skalære funktioner (en funktion, der tager en eller flere input, men kun returnerer en enkelt output), opnår de ekstrem ydeevne på bekostning af ekstrem specialisering.
Eksempel kode
I eksempelkoden tilføjer vi vektorer sammen. Jeg har tilføjet en CPU- og GPU-version af koden til sammenligning af hastighed.
gpu-eksempel.cpp indholdet nedenfor:
#omfatte
#omfatte
#omfatte
#omfatte
#omfatte
typedef std :: chrono :: high_resolution_clock Clock;
#definer ITER 65535
// CPU-version af vector add-funktionen
ugyldig vektor_add_cpu (int * a, int * b, int * c, int n)
int i;
// Føj vektorelementerne a og b til vektoren c
for (i = 0; i < n; ++i)
c [i] = a [i] + b [i];
// GPU-version af vector add-funktionen
__global__ ugyldig vektor_add_gpu (int * gpu_a, int * gpu_b, int * gpu_c, int n)
int i = threadIdx.x;
// Nej til løkke nødvendig, fordi CUDA-runtime
// vil tråde dette ITER gange
gpu_c [i] = gpu_a [i] + gpu_b [i];
int main ()
int * a, * b, * c;
int * gpu_a, * gpu_b, * gpu_c;
a = (int *) malloc (ITER * sizeof (int));
b = (int *) malloc (ITER * sizeof (int));
c = (int *) malloc (ITER * størrelse af (int));
// Vi har brug for variabler, der er tilgængelige for GPU'en,
// så cudaMallocManaged leverer disse
cudaMallocManaged (& gpu_a, ITER * sizeof (int));
cudaMallocManaged (& gpu_b, ITER * sizeof (int));
cudaMallocManaged (& gpu_c, ITER * sizeof (int));
for (int i = 0; i < ITER; ++i)
a [i] = i;
b [i] = i;
c [i] = i;
// Ring til CPU-funktionen, og tidsindstil den
auto cpu_start = Ur :: nu ();
vector_add_cpu (a, b, c, ITER);
auto cpu_end = Ur :: nu ();
std :: cout << "vector_add_cpu: "
<< std::chrono::duration_cast
<< " nanoseconds.\n";
// Ring til GPU-funktionen, og tidsindstil den
// Trippelvinkelbremserne er en CUDA-runtime-udvidelse, der tillader det
// parametre for et CUDA-kernekald, der skal sendes.
// I dette eksempel passerer vi en trådblok med ITER-tråde.
auto gpu_start = Ur :: nu ();
vector_add_gpu <<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize ();
auto gpu_end = Ur :: nu ();
std :: cout << "vector_add_gpu: "
<< std::chrono::duration_cast
<< " nanoseconds.\n";
// Frigør GPU-funktionsbaserede hukommelsestildelinger
cudaFree (a);
cudaFree (b);
cudaFree (c);
// Frigør CPU-funktionsbaserede hukommelsesallokeringer
gratis (a);
gratis (b)
gratis (c);
returnere 0;
Makefile indholdet nedenfor:
INC = -I / usr / local / cuda / includeNVCC = / usr / local / cuda / bin / nvcc
NVCC_OPT = -std = c ++ 11
alle:
$ (NVCC) $ (NVCC_OPT) gpu-eksempel.cpp -o gpu-eksempel
ren:
-rm -f gpu-eksempel
For at køre eksemplet skal du kompilere det:
laveKør derefter programmet:
./ gpu-eksempelSom du kan se, kører CPU-versionen (vector_add_cpu) betydeligt langsommere end GPU-versionen (vector_add_gpu).
Hvis ikke, skal du muligvis justere ITER-definitionen i gpu-eksempel.cu til et højere tal. Dette skyldes, at GPU-opsætningstiden er længere end nogle mindre CPU-intensive sløjfer. Jeg fandt 65535 til at fungere godt på min maskine, men din kilometertal kan variere. Når du først har ryddet denne tærskel, er GPU'en dog dramatisk hurtigere end CPU'en.
Konklusion
Jeg håber, du har lært meget af vores introduktion til GPU-programmering med C++. Eksemplet ovenfor opnår ikke meget, men de viste koncepter giver en ramme, som du kan bruge til at inkorporere dine ideer til at frigøre kraften i din GPU.