Как стать автором
Обновить

Сравнение времени выполнения алгоритма на CPU и GPU

Время на прочтение 12 мин
Количество просмотров 7.6K

Использование CUDA Runtime API для вычислений. Сравнение CPU и GPU вычислений


В данной статье я решил провести сравнение выполнения алгоритма написанного на C++ на центральном и графическом процессоре(выполнение вычислений с помощью Nvidia CUDA Runtime API на поддерживаемом GPU Nvidia). CUDA API позволяет выполнение некоторых вычислений на графическом процессоре. Файл c++ использующий cuda, будет иметь расширение .cu.
Схема работы алгоритма приведена ниже.



Задача алгоритма состоит в том, что найти возможные числа X, при возведении которых в степень degree_of, будет получатся исходное число max_number. Сразу отмечу, что все числа которые будут передаваться GPU, будут хранится в массивах. Алгоритм, выполняемый каждым потоком, имеет приблизительно следующий вид:

int degree_of=2;
int degree_of_max=Number_degree_of_max[0];//Массив хранящий значение максимальной степени числа
int x=thread;//номер выполняемого потока
int max_number=INPUT[0];//Массив хранящий число, которое необходимо получить
int Number=1;
int Degree;
bool BREAK=false;//Переменная для завершения while
while(degree_of<=degree_of_max&&!BREAK){
    Number=1;
    for(int i=0;i<degree_of;i++){
        Number*=x;
        Degree=degree_of;
    }
    if(Number==max_number){
        OUT_NUMBER[thread]=X;//OUT_NUMBER Массив хранящий числа которые необходимо возвести в степень Degree для получения исходного числа
        OUT_DEGREE[thread]=Degree;// OUT_DEGREE Массив хранящий степень в которую нужно возвести число X для получения исходного числа
    }
   degree_of++;
   //В случае выхода за предел :
if(degree_of>degree_of_max||Number>max_number){
    BREAK=true;
}
}


Код для выполнения на CPU C++.cpp
#include <iostream>
#include<vector>
#include<string>//необходимо для getline
#include<thread>
#include<fstream>
using namespace std;
int Running_thread_counter = 0;
void Upload_to_CPU(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int THREAD);
void Upload_to_CPU(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int THREAD) {
	int thread = THREAD;
	Running_thread_counter++;
	unsigned long long  MAX_DEGREE_OF = max[0];
	int X = thread;
	unsigned long long  Calculated_number = 1;
	unsigned long long  DEGREE_OF = 2;
	unsigned long long   INP = INPUT[0];
	Stop[thread] = false;
	bool BREAK = false;
	if (X != 0 && X != 1) {
		while (!BREAK) {
			if (DEGREE_OF <= MAX_DEGREE_OF) {
				Calculated_number = 1;
				for (int counter = 0; counter < DEGREE_OF; counter++) {
					Calculated_number *= X;
				}
				if (Calculated_number == INP) {
					Stepn[thread] = DEGREE_OF;
					Number[thread] = X;
					Stop[thread] = true;
					BREAK = true;
				}
				DEGREE_OF++;
			}
			else { BREAK = true; }
		}
	}
}
void Parallelize_to_threads(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int size);
int main()
{
	int size = 1000;
	unsigned long long  *Number = new unsigned long long[size], *Degree_of = new unsigned long long[size];
	unsigned long long  *Max_Degree_of = new unsigned long long[1];
	unsigned long long  *INPUT_NUMBER = new unsigned long long[1];
	Max_Degree_of[0] = 7900;

	INPUT_NUMBER[0] = 216 * 216 * 216;
	ifstream inp("input.txt");
	if (inp.is_open()) {
		string t;
		vector<unsigned long long>IN;
		while (getline(inp, t)) {
			IN.push_back(stol(t));

		}
		INPUT_NUMBER[0] = IN[0];//исходное число
		Max_Degree_of[0] = IN[1];//значение максимальной степени
	}
	else {
		ofstream error("error.txt");
		if (error.is_open()) {
			error << "No file " << '"' << "input.txt" << '"' << endl;
			error << "Please , create a file" << '"' << "input.txt" << '"' << endl;
			error << "One read:input number" << endl;
			error << "Two read:input max stepen" << endl;
			error << "." << endl;
			error.close();
			INPUT_NUMBER[0] = 1;
			Max_Degree_of[0] = 1;
		}
	}
	// раскомментируйте следующий код , если хотите видеть исходные значения в окне консоли 
	//cout << INPUT[0] << endl;
	bool *Elements_that_need_to_stop = new bool[size];
	Parallelize_to_threads(Number, Degree_of, Elements_that_need_to_stop, INPUT_NUMBER, Max_Degree_of, size);
	vector<unsigned long long>NUMBER, DEGREEOF;
	for (int i = 0; i < size; i++) {
		if (Elements_that_need_to_stop[i]) {
			if (Degree_of[i] < INPUT_NUMBER[0] && Number[i] < INPUT_NUMBER[0]) {//проверка на ошибки 
				NUMBER.push_back(Number[i]);
				DEGREEOF.push_back(Degree_of[i]);
			}
		}
	}
	// раскомментируйте следующий код , если хотите вывести результаты в консоль
	//это может замедлить программу 
	/*
	for (int f = 0; f < NUMBER.size(); f++) {
			cout << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;
		}
		*/
	ofstream out("out.txt");
	if (out.is_open()) {
		for (int f = 0; f < NUMBER.size(); f++) {
			out << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;
		}
		out.close();
	}
}
void Parallelize_to_threads(unsigned long long  *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max, int size) {
	thread *T = new thread[size];
	Running_thread_counter = 0;
	for (int i = 0; i < size; i++) {
		T[i] = thread(Upload_to_CPU, Number, Stepn, Stop, INPUT, max, i);
		T[i].detach();
	}
	while (Running_thread_counter < size - 1);//дождаться завершения выполнения всех потоков 
}


Для работы алгоритма необходим текстовый файл с исходным числом и максимальной степенью.

Код для выполнения вычислений на GPU C++.cu
//библиотеки cuda_runtime.h и device_launch_parameters.h
//для работы с cyda
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include<vector>
#include<string>//для getline
#include <stdio.h>
#include<fstream>
using namespace std;
__global__ void Upload_to_GPU(unsigned long long  *Number,unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT,unsigned long long  *max) {
	int thread = threadIdx.x;
	unsigned long long  MAX_DEGREE_OF = max[0];
    int X = thread;
	unsigned long long  Calculated_number = 1;
	unsigned long long  Current_degree_of_number = 2;
    unsigned long long   Original_numberP = INPUT[0];
	Stop[thread] = false;
	bool BREAK = false;
	if (X!=0&&X!=1) {
		while (!BREAK) {
			if (Current_degree_of_number <= MAX_DEGREE_OF) {
				Calculated_number = 1;
				for (int counter = 0; counter < Current_degree_of_number; counter++) {
				 Calculated_number	*=X;
				}
				if (Calculated_number == Original_numberP) {
					Stepn[thread] = Current_degree_of_number;
					Number[thread] = X;
					Stop[thread] = true;
					BREAK = true;
				}
				Current_degree_of_number++;
			}
			else { BREAK = true; }
		}
	}
}
cudaError_t Configure_cuda(unsigned long long *Number, unsigned long long  *Stepn, bool *Stop,unsigned long long  *INPUT, unsigned long long  *max,unsigned int size);
int main()
{
	int size = 1000;
    unsigned long long  *Number=new unsigned long long [size], *Degree_of=new unsigned long long [size];
	unsigned long long  *Max_degree_of = new unsigned long long [1];
	unsigned long long  *INPUT_NUMBER = new unsigned long long [1];
   Max_degree_of[0] = 7900;
	
	
	ifstream inp("input.txt");
	if (inp.is_open()) {
		string text;
		vector<unsigned long long>IN;
		while (getline(inp, text)) {
			IN.push_back( stol(text));
				
		}
		INPUT_NUMBER[0] = IN[0];
		Max_degree_of[0] = IN[1];
	}
	else {
		ofstream error("error.txt");
		if (error.is_open()) {
			error<<"No file "<<'"'<<"input.txt"<<'"'<<endl;
			error<<"Please , create a file" << '"' << "input.txt" << '"' << endl;
			error << "One read:input number" << endl;
			error << "Two read:input max stepen" << endl;
			error << "." << endl;
			error.close();
			INPUT_NUMBER[0] = 1;
			Max_degree_of[0] = 1;
		}
	}
	
	
	bool *Elements_that_need_to_stop = new bool[size];
    // Загрузка массивов в cuda
	cudaError_t cudaStatus =  Configure_cuda(Number, Degree_of, Elements_that_need_to_stop, INPUT_NUMBER, Max_degree_of, size);
    if (cudaStatus != cudaSuccess) {
        fprintf(stderr, "addWithCuda failed!");
        return 1;
    }
	vector<unsigned long long>NUMBER, DEGREEOF;
	for (int i = 0; i < size; i++) {
		if (Elements_that_need_to_stop[i]) {
			NUMBER.push_back(Number[i]);//занести в вектор число
			DEGREEOF.push_back(Degree_of[i]);//занести в вектор степень числа
		}
	}
	//раскоментируйте следующий код , чтобы вывести результаты в консоль
	/*
	for (int f = 0; f < NUMBER.size(); f++) {
		cout << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;
	}*/
	ofstream out("out.txt");
	if (out.is_open()) {
		for (int f = 0; f < NUMBER.size(); f++) {
			out << NUMBER[f] << "^" << DEGREEOF[f] << "=" << INPUT_NUMBER[0] << endl;
		}
		out.close();
	}
	
    //Очистить ресурсы связанные с устройством
    cudaStatus = cudaDeviceReset();
    if (cudaStatus != cudaSuccess) {
        fprintf(stderr, "cudaDeviceReset failed!");
        return 1;
    }

    return 0;
}


cudaError_t  Configure_cuda(unsigned long long  *Number, unsigned long long *Degree_of, bool *Stop,unsigned long long *INPUT, unsigned long long *max,unsigned int size) {
	unsigned long long *dev_Number = 0;
	unsigned long long *dev_Degree_of = 0;
	unsigned long long *dev_INPUT = 0;
	unsigned long long *dev_Max = 0;
	bool *dev_Elements_that_need_to_stop;

	cudaError_t cudaStatus;

	// УСТАНОВКА ИСПОЛЬЗУЕМОГО GPU 
	cudaStatus = cudaSetDevice(0);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaSetDevice failed!  Do you have a CUDA-capable GPU installed?");
		goto Error;
	}

	// РЕЗЕРВИРОВАНИЕ МЕСТА В ПАМЯТИ ПОД ДАННЫЕ
	cudaStatus = cudaMalloc((void**)&dev_Number, size * sizeof(unsigned long long));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!dev_Number");
		goto Error;
	}
	cudaStatus = cudaMalloc((void**)&dev_Degree_of, size * sizeof(unsigned long long));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!dev_Degree_of");
		goto Error;
	}
	cudaStatus = cudaMalloc((void**)&dev_Max, size * sizeof(unsigned long long int));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!dev_Max");
		goto Error;
	}
	cudaStatus = cudaMalloc((void**)&dev_INPUT, size * sizeof(unsigned long long));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!dev_INPUT");
		goto Error;
	}
	cudaStatus = cudaMalloc((void**)&dev_Elements_that_need_to_stop, size * sizeof(bool));
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMalloc failed!dev_Stop");
		goto Error;
	}
	// ПЕРЕМЕЩЕНИЕ ДАННЫХ В ПАМЯТЬ GPU
	cudaStatus = cudaMemcpy(dev_Max, max, size * sizeof(unsigned long long), cudaMemcpyHostToDevice);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}
	cudaStatus = cudaMemcpy(dev_INPUT, INPUT, size * sizeof(unsigned long long), cudaMemcpyHostToDevice);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}
	Upload_to_GPU<<<1, size>>>(dev_Number, dev_Degree_of, dev_Elements_that_need_to_stop, dev_INPUT, dev_Max);
	// Проверка сбоев ядра
	cudaStatus = cudaGetLastError();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "addKernel launch failed: %s\n", cudaGetErrorString(cudaStatus));
		goto Error;
	}

	// Ожидание завершения операций , выполняемых ядром

	cudaStatus = cudaDeviceSynchronize();
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaDeviceSynchronize returned error code %d after launching addKernel!\n", cudaStatus);
		goto Error;
	}

	// Перемещение данных из памяти GPU в системную память
	cudaStatus = cudaMemcpy(Number, dev_Number, size * sizeof(unsigned long long), cudaMemcpyDeviceToHost);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}
	cudaStatus = cudaMemcpy(Degree_of, dev_Degree_of, size * sizeof(unsigned long long), cudaMemcpyDeviceToHost);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}
	cudaStatus = cudaMemcpy(Stop, dev_Elements_that_need_to_stop, size * sizeof(bool), cudaMemcpyDeviceToHost);
	if (cudaStatus != cudaSuccess) {
		fprintf(stderr, "cudaMemcpy failed!");
		goto Error;
	}
Error://Освобождение памяти GPU от данных
	cudaFree(dev_INPUT);
	cudaFree(dev_Degree_of);
	cudaFree(dev_Max);
	cudaFree(dev_Elements_that_need_to_stop);
	cudaFree(dev_Number);

	return cudaStatus;
}


Идентификатор
__global__  
в .cu файле указывает, что функция выполняется на уровне ядра GPU.

Для работы с CUDA API, перед вызовом функции, нужно зарезервировать память под массив и перенести элементы в память GPU. Это увеличивает объем кода, но позволяет разгрузить CPU, так как вычисления производятся на GPU.Поэтому ,cuda, дает как минимум возможность разгрузить процессор для выполнения других нагрузок, не использующих cuda.

В случае примера на cuda, задача процессора заключается лишь в загрузке инструкций на GPU и обработке результатов пришедших с GPU; В то время как в коде для CPU, процессор обрабатывает каждый поток. Стоит отметить, что cyda имеет ограничения по количеству запускаемых потоков, поэтому в обоих алгоритмах я взял одинаковое количество потоков, равное 1000. Также, в случае с CPU я использовал переменную

int Running_thread_counter = 0;

чтобы считать количество уже выполненных потоков и дожидаться, пока все потоки не выполнятся.

Тестируемая конфигурация
  • CPU :amd ryzen 5 1400(4core,8thread)
  • ОЗУ:8гбDDR4 2666
  • GPU:Nvidia rtx 2060

  • OS:windows 10 version 2004
  • Cuda:
    • Compute Capability 7.5
    • Threads per Multiprocessor 1024
    • CUDA 11.1.70

  • GPU-Z:version 2.35.0
  • Visual Studio 2017

Сведения о CUDA были взяты из GPU-Z

Для тестирования алгоритма я использовал

следующий код на C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.IO;
namespace ConsoleAppTESTSTEPEN_CPU_AND_GPU_
{
    class Program
    {
        static string Upload(Int64 number,Int64 degree_of)
        {
            string OUT = "";
            string[] Chord_values = new string[2];
            Int64 Degree_of = degree_of;
            Int64 Number = number;
            Chord_values[0] = Number.ToString();
            Chord_values[1] = Degree_of.ToString();
            File.WriteAllLines("input.txt", Chord_values);//файл входных данных
            OUT+="input number:" + Number.ToString()+"\n";
            OUT+="input degree of number:" + Degree_of.ToString()+"\n";
            DateTime running_CPU_application = DateTime.Now;//записать время запуска программы
            Process proc=  Process.Start("ConsoleApplication29.exe");//exe реализация алгоритма на c++ x64 использующая CPU для вычислений
            while (!proc.HasExited) ;//дождаться завершения программы
            DateTime stop_CPU_application = DateTime.Now;//записать время остановки программы 
            string[]outs = File.ReadAllLines("out.txt");//получить результаты
            File.Delete("out.txt");
            OUT+="CPU:"+"\n";
            if (outs.Length>0)
            {
                for (int j = 0; j < outs.Length; j++)
                {
                    OUT+=outs[j]+"\n";
                }
            }
            else { OUT+="no values"+"\n"; }
            OUT+="running_CPU_application:" + running_CPU_application.ToString()+"\n";
            OUT+="stop_CPU_application:" + stop_CPU_application.ToString()+"\n";
            OUT+="GPU:"+"\n";
            //альтернативные действия для реализации алгоритма korenXN.exe x64 использующего для вычислений GPU
            DateTime running_GPU_application = DateTime.Now;
            Process procGPU = Process.Start("korenXN.exe");
            while (!procGPU.HasExited) ;
            DateTime stop_GPU_application = DateTime.Now;
            string[] outs2 = File.ReadAllLines("out.txt");
            File.Delete("out.txt");
            if (outs2.Length > 0)
            {
                for (int j = 0; j < outs2.Length; j++)
                {
                    OUT+=outs2[j]+"\n";
                }
            }
            else { OUT+="no values"+"\n"; }
            OUT+="running_GPU_application:" + running_GPU_application.ToString()+"\n";
            OUT+="stop_GPU_application:" + stop_GPU_application.ToString()+"\n";
            return OUT;//возвратить результат
        }
        static void Main()
        {
            Int64 start = 36*36;//начальное значение входного числа
            Int64 degree_of_strat = 500;//начальное значение максимальной степени
            int size = 20-5;//количество элементов в массиве
            Int64[] Number = new Int64[size];//массив входных чисел
            Int64[] Degree_of = new Int64[size];//массив максимальных степеней
            string[]outs= new string[size];//массив результатов
            for (int n = 0; n < size; n++)
            {
                if (n % 2 == 0)
                {
                    Number[n] = start * start;
                }
                else
                {
                    Number[n] = start * degree_of_strat;
                    Number[n] -= n + n;
                }
                start += 36*36;
                Degree_of[n] = degree_of_strat;
                degree_of_strat +=1000;
            }
            for (int n = 0; n < size; n++)
            {
                outs[n] = Upload(Number[n], Degree_of[n]);
                Console.WriteLine(outs[n]);
            }
            System.IO.File.WriteAllLines("result.txt", outs);//записать результаты в файл result.txt
        }
    }
}


, который создавал файл с исходными данными, затем последовательно запускал exe файлы алгоритмов использующих CPU или GPU и замерял время их работы, затем заносил это время и результаты работы алгоритмов в файл result.txt. Для замера загруженности процессора использовался диспетчер задач windows.

Результаты теста приведены в таблице:



Как видно из таблицы, время выполнения алгоритма на GPU немного больше, чем на CPU.
Однако, отмечу, что вовремя работы алгоритма использующего для вычислений GPU, загрузка алгоритмом CPU, отображаемая в Диспетчере задач, не превышала 30%, в то время как алгоритм использующий для вычислений CPU, загружал его на 68-85%, что в свою очередь иногда приводило к замедлению других приложений. Также, ниже приведен график, показывающий различие во
времени выполнения (по оси Y)CPU и GPU в зависимости от входного числа(по оси X).

график


Далее я решил провести тестирование при процессоре нагруженном другими приложениями. Процессор был нагружен так, что запущенный в приложение тест, не занимал больше 55% ресурсов процессора. Результаты теста приведены ниже:



График


Как видно из таблицы, в случае с нагруженным CPU, выполнение вычислений на GPU, дает прирост производительности, так как загруженность процессора в 30% укладывается в лимит 55%, а в случае использования CPU для вычислений, его загрузка составляет 68-85% , что тормозит работу алгоритма, если CPU нагружен другими приложениями.

Причина отставания GPU от CPU, на мой взгляд, может заключатся в том, что у CPU выше производительность ядра(3400 МГц CPU, 1680 МГц GPU). В том случае, когда ядра процессора нагружены другими процессами, быстродействие будет зависть от количества обрабатываемых потоков за определенный интервал времени и в этом случае быстрее будет GPU, так как он способен одновременно обрабатывать больше потоков (1024 GPU,8 CPU).
Поэтому, можно сделать вывод, что использование GPU для вычислений, не обязательно должно давать более быструю работу алгоритма, однако, оно способно разгрузить CPU, что может играть роль, если он нагружен другими приложениями.


Теги:
Хабы:
-5
Комментарии 41
Комментарии Комментарии 41

Публикации

Истории

Работа

QT разработчик
13 вакансий
Программист C++
121 вакансия

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн