Overzicht
In deze gids onderzoeken we de kracht van GPU-programmering met C++. Ontwikkelaars kunnen ongelooflijke prestaties verwachten met C++, en toegang tot de fenomenale kracht van de GPU met een taal op laag niveau kan een van de snelste berekeningen opleveren die momenteel beschikbaar zijn.
Vereisten
Hoewel elke machine die een moderne versie van Linux kan draaien een C++-compiler kan ondersteunen, heb je een op NVIDIA gebaseerde GPU nodig om deze oefening te volgen. Als je geen GPU hebt, kun je een GPU-aangedreven instantie starten in Amazon Web Services of een andere cloudprovider naar keuze.
Als u een fysieke machine kiest, zorg er dan voor dat u de eigen NVIDIA-stuurprogramma's hebt geïnstalleerd. Instructies hiervoor vind je hier: https://linuxhint.com/install-nvidia-drivers-linux/
Naast de driver heb je de CUDA-toolkit nodig. In dit voorbeeld gebruiken we Ubuntu 16.04 LTS, maar er zijn downloads beschikbaar voor de meeste grote distributies op de volgende URL: https://developer.nvidia.com/cuda-downloads
Voor Ubuntu kiest u de .op deb gebaseerde download. Het gedownloade bestand heeft geen .deb-extensie standaard, dus ik raad aan deze te hernoemen naar a .deb aan het einde. Vervolgens kunt u installeren met:
sudo dpkg -i pakketnaam.debU wordt waarschijnlijk gevraagd om een GPG-sleutel te installeren, en als dat het geval is, volgt u de instructies om dit te doen.
Zodra je dat hebt gedaan, werk je je repositories bij:
sudo apt-get updatesudo apt-get install cuda -y
Als je klaar bent, raad ik aan om opnieuw op te starten om ervoor te zorgen dat alles correct is geladen.
De voordelen van GPU-ontwikkeling
CPU's verwerken veel verschillende inputs en outputs en bevatten een groot assortiment aan functies, niet alleen voor het omgaan met een breed assortiment aan programmabehoeften, maar ook voor het beheren van verschillende hardwareconfiguraties. Ze verwerken ook geheugen, caching, de systeembus, segmentering en IO-functionaliteit, waardoor ze een manusje van alles zijn.
GPU's zijn het tegenovergestelde - ze bevatten veel individuele processors die zijn gericht op zeer eenvoudige wiskundige functies. Hierdoor verwerken ze taken vele malen sneller dan CPU's. Door zich te specialiseren in scalaire functies (een functie die een of meer invoer nodig heeft maar slechts één uitvoer retourneert), bereiken ze extreme prestaties ten koste van extreme specialisatie.
Voorbeeldcode:
In de voorbeeldcode tellen we vectoren bij elkaar op. Ik heb een CPU- en GPU-versie van de code toegevoegd voor snelheidsvergelijking.
gpu-voorbeeld.cpp inhoud hieronder:
#include
#include
#include
#include
#include
typedef std::chrono::high_resolution_clock Klok;
#define ITER 65535
// CPU-versie van de vector-toevoegfunctie
void vector_add_cpu(int *a, int *b, int *c, int n)
int ik;
// Voeg de vectorelementen a en b toe aan de vector c
voor (i = 0; i < n; ++i)
c[i] = a[i] + b[i];
// GPU-versie van de vector-toevoegfunctie
__global__ void vector_add_gpu(int *gpu_a, int *gpu_b, int *gpu_c, int n)
int i = threadIdx.X;
// Geen for-lus nodig omdat de CUDA-runtime
// zal deze ITER-tijden inrijgen
gpu_c[i] = gpu_a[i] + gpu_b[i];
int hoofd()
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 * sizeof (int));
// We hebben variabelen nodig die toegankelijk zijn voor de GPU,
// dus cudaMallocManaged biedt deze
cudaMallocManaged(&gpu_a, ITER * sizeof(int));
cudaMallocManaged(&gpu_b, ITER * sizeof(int));
cudaMallocManaged(&gpu_c, ITER * sizeof(int));
voor (int i = 0; i < ITER; ++i)
een[i] = ik;
b[i] = ik;
c[i] = ik;
// Roep de CPU-functie aan en time it
auto cpu_start = Klok::nu();
vector_add_cpu(a, b, c, ITER);
auto cpu_end = Klok::nu();
standaard::cout << "vector_add_cpu: "
<< std::chrono::duration_cast
<< " nanoseconds.\n";
// Roep de GPU-functie aan en time it
// De triple angle brakets is een CUDA runtime-extensie die het mogelijk maakt:
// parameters van een CUDA-kernelaanroep die moet worden doorgegeven.
// In dit voorbeeld passeren we één threadblok met ITER-threads.
auto gpu_start = Klok::nu();
vector_add_gpu <<<1, ITER>>> (gpu_a, gpu_b, gpu_c, ITER);
cudaDeviceSynchronize();
auto gpu_end = Klok::nu();
standaard::cout << "vector_add_gpu: "
<< std::chrono::duration_cast
<< " nanoseconds.\n";
// Bevrijd de op GPU-functie gebaseerde geheugentoewijzingen
cudaFree(a);
cudaFree(b);
cudaFree(c);
// Bevrijd de op CPU-functie gebaseerde geheugentoewijzingen
gratis (een);
gratis (b);
gratis (c);
retourneer 0;
Makefile inhoud hieronder:
INC=-I/usr/local/cuda/includeNVCC=/usr/local/cuda/bin/nvcc
NVCC_OPT=-std=c++11
alle:
$(NVCC) $(NVCC_OPT) gpu-voorbeeld.cpp -o gpu-voorbeeld
schoon:
-rm -f gpu-voorbeeld
Om het voorbeeld uit te voeren, compileert u het:
makenVoer vervolgens het programma uit:
./gpu-voorbeeldZoals u kunt zien, werkt de CPU-versie (vector_add_cpu) aanzienlijk langzamer dan de GPU-versie (vector_add_gpu).
Als dit niet het geval is, moet u mogelijk de ITER-definitie in gpu-voorbeeld aanpassen.cu naar een hoger nummer. Dit komt doordat de GPU-insteltijd langer is dan bij sommige kleinere CPU-intensieve lussen. Ik vond 65535 goed werken op mijn machine, maar uw kilometerstand kan variëren may. Als u deze drempel eenmaal hebt overschreden, is de GPU echter aanzienlijk sneller dan de CPU.
Conclusie
Ik hoop dat je veel hebt geleerd van onze introductie in GPU-programmering met C++. Het bovenstaande voorbeeld brengt niet veel tot stand, maar de gedemonstreerde concepten bieden een raamwerk dat u kunt gebruiken om uw ideeën op te nemen om de kracht van uw GPU te ontketenen.