计算的硬件加速思路
在硬件层面,计算通常可以分为几个阶段:
取指令
指令解码
取数
计算
写回
为了加速计算,整体有两个大的思路:
思路一:缩短指令的执行时间
思路二:提升单位时间内指令的吞吐量。
思路1: 缩短指令的执行时间
常见的方法包括:
指令流水线,以及对应的各种tricky技术,比如
指令预取
内存预取
分支预测
更大的core,更快的主频等
思路2: 提升单位时间内指令的吞吐量
常见的方法包括:
多核心
硬件复用后的虚拟多核心:只将部分关键硬件复制几分,底层的存储等仍然复用,这样能做到有效控制成本的同时提升单位时间内的指令吞吐。
现代处理器,无论是CPU/GPU,通常会同时使用以上两种加速思路。对于 **提升单位时间内指令的吞吐量,**在现代CPU,有 SIMD 指令。
对于cpu scalar 实现
void mandelbrot_cpu_scalar(uint32_t img_size, uint32_t max_iters, uint32_t *out) {
for (uint64_t i = 0; i < img_size; ++i) {
for (uint64_t j = 0; j < img_size; ++j) {
// Get the plane coordinate X for the image pixel.
float cx = (float(j) / float(img_size)) * 2.5f - 2.0f;
float cy = (float(i) / float(img_size)) * 2.5f - 1.25f;
// Innermost loop: start the recursion from z = 0.
float x2 = 0.0f;
float y2 = 0.0f;
float w = 0.0f;
uint32_t iters = 0;
while (x2 + y2 <= 4.0f && iters < max_iters) {
float x = x2 - y2 + cx;
float y = w - x2 - y2 + cy;
x2 = x * x;
y2 = y * y;
float z = x + y;
w = z * z;
++iters;
}
// Write result.
out[i * img_size + j] = iters;
}
}
}
有:
g++ -march=native -O3 -Wall -Wextra -o mandelbrot mandelbrot_cpu.cc
Testing with image size 320x320 and 320 max iterations.
Running mandelbrot_cpu_scalar ...
Runtime: 57.518 ms
CPU 使用 SIMD 实现 CPU 的向量化加速:
void mandelbrot_cpu_vector(uint32_t img_size, uint32_t max_iters, uint32_t *out) {
const __m512 v_img_size = _mm512_set1_ps((float)img_size);
const __m512 v_2_5 = _mm512_set1_ps(2.5f);
const __m512 v_4_0 = _mm512_set1_ps(4.0f);
const __m512i v_max_iters = _mm512_set1_epi32(max_iters);
const __m512i v_step = _mm512_set_epi32(15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0);
for (int i = 0; i < img_size; ++i) {
float cy_val = (float(i) / img_size) * 2.5f - 1.25f;
__m512 v_cy = _mm512_set1_ps(cy_val);
for (int j = 0; j < img_size; j += 16) {
__m512i v_j = _mm512_add_epi32(_mm512_set1_epi32(j), v_step);
__m512 v_cx = _mm512_sub_ps(_mm512_mul_ps(_mm512_div_ps(_mm512_cvtepi32_ps(v_j), v_img_size), v_2_5), _mm512_set1_ps(2.0f));
__m512 v_x = _mm512_set1_ps(0.0f);
__m512 v_y = _mm512_set1_ps(0.0f);
__m512i v_iters = _mm512_set1_epi32(0);
for (int n = 0; n < max_iters; ++n) {
__m512 x2 = _mm512_mul_ps(v_x, v_x);
__m512 y2 = _mm512_mul_ps(v_y, v_y);
__mmask16 mask = _mm512_cmp_ps_mask(_mm512_add_ps(x2, y2), v_4_0, _CMP_LE_OS);
if (!mask) break;
v_iters = _mm512_mask_add_epi32(v_iters, mask, v_iters, _mm512_set1_epi32(1));
__m512 xy = _mm512_mul_ps(v_x, v_y);
v_y = _mm512_add_ps(_mm512_add_ps(xy, xy), v_cy);
v_x = _mm512_add_ps(_mm512_sub_ps(x2, y2), v_cx);
}
_mm512_storeu_si512((__m512i *)(out + i * img_size + j), v_iters);
}
}
}
性能提升19倍,约等于 avx 并行数。但可以发现 cpu SIMD 编程相比下面的cuda 编程,心智负担需要更高,因为需要自行计算和使用mask,确保vector中不同位置(lane)执行(可能)不同的分支。
g++ -O3 -mavx512f -mavx512vl -o mandelbrot_avx512 mandelbrot_cpu.cpp
./mandelbrot_avx512 -r 320 -b 320 -i vector
Testing with image size 320x320 and 320 max iterations.
Running mandelbrot_cpu_vector ...
Runtime: 3.09283 ms
Correctness: average output difference from reference = 0.000207642
Cuda SPMD
__global__ void mandelbrot_gpu_vector(
uint32_t img_size,
uint32_t max_iters,
uint32_t *out /* pointer to GPU memory */
) {
for (uint64_t i = 0; i < img_size; ++i) {
for (uint64_t j = 0; j < img_size; j+=blockDim.x) {
// Get the plane coordinate X for the image pixel.
int jx = j + threadIdx.x;
float cx = (float(jx) / float(img_size)) * 2.5f - 2.0f;
float cy = (float(i) / float(img_size)) * 2.5f - 1.25f;
// Innermost loop: start the recursion from z = 0.
float x2 = 0.0f;
float y2 = 0.0f;
float w = 0.0f;
uint32_t iters = 0;
while (x2 + y2 <= 4.0f && iters < max_iters) {
float x = x2 - y2 + cx;
float y = w - x2 - y2 + cy;
x2 = x * x;
y2 = y * y;
float z = x + y;
w = z * z;
++iters;
}
// Write result.
out[i * img_size + jx] = iters;
}
}
}
void launch_mandelbrot_gpu_vector(
uint32_t img_size,
uint32_t max_iters,
uint32_t *out /* pointer to GPU memory */
) {
mandelbrot_gpu_vector<<<1,32>>>(img_size, max_iters, out);
}
Gpu vector parallel
nvcc -O3 -o mandelbrot mandelbrot_gpu.cu
./mandelbrot -r 320 -b 320 -i vector
Testing with image size 320x320 and 320 max iterations.
Running launch_mandelbrot_gpu_vector ...
Runtime: 10.7224 ms
Correctness: average output difference from reference 0.000191223
可见从单核心性能上,gpu 单核通常慢于cpu 单核