Вступ до GPU програмування з D3D12: Частина 2 - Навіщо нам взагалі GPU?
Перед тим як занурюватись у GPU програмування, треба спочатку зрозуміти для чого це потрібно. Для чого нам ще один тип процесора, якщо CPU вже і так може виконати будь-які обрахунки? Розглянемо декілька задач.
Задача 1
У вас є масив точок у 3D просторі і матриця трансформації. Вам потрібно перезаписати координати точок на координати після застосуванн заданої трансформації.
Рішення на CPU
Щоб вирішити цю проблему на CPU, ми можемо зробити цикл по масиву точок, і застосувати матрицю трансформації на кожну точку. Це вирішує проблему, але такий цикл займе доволі багато часу у випадку великого масиву.
Спробуємо прискорити наше рішення. типовий CPU має приблизно 6 ядер/12 потоків. Це значить що ми можемо обраховувати 12 потоків паралельно, обраховуючи на кожному рівну частину масиву. Це дає прискорення до 12 разів, що краще, але все ще далеко від того, що нам треба.
Рішення на GPU
На відміну від CPU, ми не можемо просто взяти і запустити код на GPU. Ось що потрібно зробити:
- Створити копію масиву, до якої GPU матиме доступ
- Написати програму яка бере трансформацію і індекс точки, застосовує трансформацію, і переписує координати точки результатом
- Запустити необхідну кількість копій нашої программи на GPU
- Після завершення GPU роботи, скопіювати масив назад
І ось тут велика різниця: типовий GPU (Nvidia RTX 3060) має приблизно 3500 потоків. GPU автоматично розділить роботу між ядрами, так що це буде швидше ніж рішення на CPU.
Задача 2
У вас є блок пам’яті. Вам потрібно порахувати його хеш. Хей це функціє яка приймає хеш усіх попередніх байт у блоці, і наступний байт. Якщо блок пам’яті порожній, хеш повертає 0.
Рішення на CPU
Дуже просто, ми починаємо зі значення 0 як попередній хеш, а потім рахуємо хеш з кожним наступним байтом. У кінці, ми будемо мати хеш для усього блоку пам’яті. Ми не можемо використовувати багатопоточність у цьому прикладі, так як кожен наступний обрахунок вимагає результат попереднього.
Рішення на GPU
Нажаль, ми не можемо зробити нічого краще ніж CPU рішення. Оскільки ми вимушені зробити кожен обрахунок один за одним, то найкраще що ми можемо зробити це:
- Створити копію масиву, до якої GPU матиме доступ
- Написати програму яка порахує хеш усього блоку пам’яті
- Запустити 1 копію нашої программи на GPU
- Після завершення GPU роботи, скопіювати хеш назад
У цього підходу є декілька недоліків:
- Ми марнуємо пам’ять, адже тепер нам треба робити копію
- GPU потоки повільніші за CPU потоки, так що сам по собі алгоритм займе більшу кількість часу
- CPU має чекати, поки GPU завершить роботу, роблячи алгоритм більш складним, та марнуючи ресурси на CPU/GPU синхронізацію
- Нам все ще треба скопіювати пам’ять на CPU, що є додатковою тратою ресурсів
Так що у цьому випадку очевидно краще використовувати CPU.
Задача 3
Вам потрібно прочитати 2 числа з консолі і вивести їх сумму.
CPU Solution
#include <iostream>
int main() {
int a;
int b;
std::cin >> a >> b;
std::cout << (a + b) << std::endl;
}
Ось і все.
GPU Solution
Це неможливо зробити. GPU не розраховані на однопоточні задачі. Тому ви не можете отримати доступ до багатьох речей: консолі, введення з клавіатури/миші/контроллера, файлової системи, мережі, тощо. Логіка в тому, що ці речі краще використовувати на CPU.
Порівняння CPU і GPU
Декілька висновків з цих прикладів:
- Деякі задачі можуть бути вирішені як і на CPU так і на GPU, але GPU вирішує їх значно швидше
- Якщо вам потрібна висока продуктивність, і у вас є така задача, то у вас є привід використовувати GPU
- Деякі задачі можуть бути вирішені як і на CPU так і на GPU, але GPU не може виконати їх так еффективно як CPU
- Те що ви можете це зробити ще не значить що варто це робити
- Деякі задачі можуть бути вирішені тільки на CPU
- Вам завжди доведеться виконувати частину програми на CPU