GPU编程2: 并行策略

并行策略

无论是 CPU 还是 GPU 从并行策略上都分为以下几类。理解不同并行方式有助于理解如何实现高性能计算。

指令并行 ILP (Instuction Level Parallel)

指令并行是指同一线程(同一程序流)中,在一个周期内同时发射多条无相互依赖的指令到cpu进行执行。

现代 CPU 通常包含多套独立ALU/FPU等硬件设备,加上流水线和乱序执行等技术,支持了指令并行的实现。

CPU 指令并行示例

java 复制代码
void mandelbrot_cpu_vector_ilp(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_zoom = _mm512_set1_ps(window_zoom);
    const __m512 v_4 = _mm512_set1_ps(4.0f);
    const __m512 v_wx = _mm512_set1_ps(window_x);
    const __m512i v_one = _mm512_set1_epi32(1);

    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 = (float(i) / img_size) * window_zoom + window_y;
        __m512 v_cy = _mm512_set1_ps(cy);

        for (int j = 0; j < img_size; j += 32) {
            __m512i v_j0 = _mm512_add_epi32(_mm512_set1_epi32(j), v_step);
            __m512i v_j1 = _mm512_add_epi32(_mm512_set1_epi32(j + 16), v_step);

            __m512 v_cx0 = _mm512_add_ps(
                _mm512_mul_ps(_mm512_div_ps(_mm512_cvtepi32_ps(v_j0), v_img_size), v_zoom),
                v_wx
            );

            __m512 v_cx1 = _mm512_add_ps(
                _mm512_mul_ps(_mm512_div_ps(_mm512_cvtepi32_ps(v_j1), v_img_size), v_zoom),
                v_wx
            );

            __m512 v_x0 = _mm512_setzero_ps();
            __m512 v_y0 = _mm512_setzero_ps();
            __m512i v_it0 = _mm512_setzero_si512();

            __m512 v_x1 = _mm512_setzero_ps();
            __m512 v_y1 = _mm512_setzero_ps();
            __m512i v_it1 = _mm512_setzero_si512();

            for (int n = 0; n < max_iters; ++n) {
                __m512 x2_0 = _mm512_mul_ps(v_x0, v_x0);
                __m512 y2_0 = _mm512_mul_ps(v_y0, v_y0);
                __mmask16 m0 =
                    _mm512_cmp_ps_mask(_mm512_add_ps(x2_0, y2_0), v_4, _CMP_LE_OS);

                __m512 x2_1 = _mm512_mul_ps(v_x1, v_x1);
                __m512 y2_1 = _mm512_mul_ps(v_y1, v_y1);
                __mmask16 m1 =
                    _mm512_cmp_ps_mask(_mm512_add_ps(x2_1, y2_1), v_4, _CMP_LE_OS);

                if (!(m0 | m1)) break;

                v_it0 = _mm512_mask_add_epi32(v_it0, m0, v_it0, v_one);
                v_it1 = _mm512_mask_add_epi32(v_it1, m1, v_it1, v_one);

                __m512 xy0 = _mm512_mul_ps(v_x0, v_y0);
                __m512 xy1 = _mm512_mul_ps(v_x1, v_y1);

                v_y0 = _mm512_add_ps(_mm512_add_ps(xy0, xy0), v_cy);
                v_y1 = _mm512_add_ps(_mm512_add_ps(xy1, xy1), v_cy);

                v_x0 = _mm512_add_ps(_mm512_sub_ps(x2_0, y2_0), v_cx0);
                v_x1 = _mm512_add_ps(_mm512_sub_ps(x2_1, y2_1), v_cx1);
            }

            _mm512_storeu_si512((__m512i *)(out + i * img_size + j), v_it0);
            _mm512_storeu_si512((__m512i *)(out + i * img_size + j + 16), v_it1);
        }
    }
}

这里实现了两路并行的指令并行,在循环中,每次计算两个不相关的像素点。整体性能能够带来约两倍的提升。不足两倍的主要原因应该是每次循环中迭代次数相差较大,导致负载不均,需要更好的负载均衡策略,才能更逼近理论两倍提升值。

bash 复制代码
g++ -march=native -O3 -Wall -Wextra -o mandelbrot mandelbrot_cpu_2.cpp
bash 复制代码
./mandelbrot -r 320 -b 320 -i scalar
Testing with image size 320x320 and 320 max iterations.

Running mandelbrot_cpu_scalar ...
  Runtime:  70.93 ms
  
  
./mandelbrot -r 320 -b 320 -i vector
Testing with image size 320x320 and 320 max iterations.

Running mandelbrot_cpu_vector ...
  Runtime:   7.45 ms
  
  
/mandelbrot -r 320 -b 320 -i vector_ilp
Testing with image size 320x320 and 320 max iterations.

Running mandelbrot_cpu_vector_ilp ...
  Runtime:   4.73 ms

GPU 指令并行示例

cpp 复制代码
__global__ void mandelbrot_gpu_vector_ilp(
    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 += 2 * blockDim.x) {

            int jx0 = j + threadIdx.x;
            int jx1 = j + threadIdx.x + blockDim.x;

            float cx0 = (float(jx0) / img_size) * 2.5f - 2.0f;
            float cx1 = (float(jx1) / img_size) * 2.5f - 2.0f;
            float cy  = (float(i) / img_size) * 2.5f - 1.25f;

            float x2_0 = 0.0f, y2_0 = 0.0f, w0 = 0.0f;
            float x2_1 = 0.0f, y2_1 = 0.0f, w1 = 0.0f;

            uint32_t iters0 = 0, iters1 = 0;

            while (true) {
                bool active0 = (x2_0 + y2_0 <= 4.0f) && (iters0 < max_iters);
                bool active1 = (x2_1 + y2_1 <= 4.0f) && (iters1 < max_iters);

                if (!active0 && !active1) break;

                if (active0) {
                    float x = x2_0 - y2_0 + cx0;
                    float y = w0 - x2_0 - y2_0 + cy;
                    x2_0 = x * x;
                    y2_0 = y * y;
                    float z = x + y;
                    w0 = z * z;
                    iters0++;
                }

                if (active1) {
                    float x = x2_1 - y2_1 + cx1;
                    float y = w1 - x2_1 - y2_1 + cy;
                    x2_1 = x * x;
                    y2_1 = y * y;
                    float z = x + y;
                    w1 = z * z;
                    iters1++;
                }
            }

            out[i * img_size + jx0] = iters0;
            out[i * img_size + jx1] = iters1;
        }
    }
}

void launch_mandelbrot_gpu_vector_ilp(
    uint32_t img_size,
    uint32_t max_iters,
    uint32_t *out /* pointer to GPU memory */
) {
    return mandelbrot_gpu_vector_ilp<<<1, 32>>>(img_size, max_iters, out);
}

GPU同样可以使用指令并行的方式来提升执行速度,且有与CPU相似的结论

bash 复制代码
nvcc -O3  -o mandelbrot_gpu mandelbrot_gpu_2.cu
bash 复制代码
./mandelbrot_gpu -r 320 -b 320 -i vector
Testing with image size 320x320 and 320 max iterations.

Running launch_mandelbrot_gpu_vector ...
  Runtime:  52.10 ms
  
  
./mandelbrot_gpu -r 320 -b 320 -i vector_ilp
Testing with image size 320x320 and 320 max iterations.

Running launch_mandelbrot_gpu_vector_ilp ...
  Runtime:  17.59 ms

多核并行 Multiple core parallelism

多核并行将任务分发到不同核心来实现并行计算。

CPU 多核并行示例

使用与CPU 逻辑核相同核心数目

cpp 复制代码
void mandelbrot_cpu_vector_thread(int n_threads, int id, 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_window_zoom = _mm512_set1_ps(window_zoom);
    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);

    const int chunk_size = img_size / n_threads;
    const int start_i = id * chunk_size, end_i = min((id + 1) * chunk_size, img_size);

    for (int i = start_i; i < end_i; ++i) {
        float cy_val = (float(i) / img_size) * window_zoom + window_y;
        __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_add_ps(_mm512_mul_ps(_mm512_div_ps(_mm512_cvtepi32_ps(v_j), v_img_size), v_window_zoom), _mm512_set1_ps(window_x));

            __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);
        }
    }
}

void mandelbrot_cpu_vector_multicore(
    uint32_t img_size,
    uint32_t max_iters,
    uint32_t *out) {
    const int n_threads = 8;
    std::vector<std::thread> threads;
    for (int i=0; i<n_threads; ++i) {
        threads.emplace_back(std::thread([=] {
            mandelbrot_cpu_vector_thread(n_threads, i, img_size, max_iters, out);
        }));
    }
    for (auto &t: threads) {
        t.join();
    }
}
bash 复制代码
g++ -march=native -O3 -Wall -Wextra -o mandelbrot mandelbrot_cpu_2.cpp -lpthread
bash 复制代码
./mandelbrot -r 320 -b 320 -i vector_multicore

Running mandelbrot_cpu_vector_multicore ...
  Runtime:   3.54 ms

GPU 多核并行示例

cpp 复制代码
__global__ void mandelbrot_gpu_vector_multicore(
    uint32_t img_size,
    uint32_t max_iters,
    uint32_t *out /* pointer to GPU memory */
) {
    for (uint64_t i = 0; i < img_size; i+=gridDim.x) {
        int ix = i + blockIdx.x;
        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)) * window_zoom + window_x;
            float cy = (float(ix) / float(img_size)) * window_zoom + window_y;

            // 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[ix * img_size + jx] = iters;
        }
    }
}

void launch_mandelbrot_gpu_vector_multicore(
    uint32_t img_size,
    uint32_t max_iters,
    uint32_t *out /* pointer to GPU memory */
) {
    return mandelbrot_gpu_vector_multicore<<<80, 32 * 4>>>(img_size, max_iters, out);
}

注意使用了80个SM(我手上gpu卡的全部sm),每个sm共4个warp scheduler。类似于cpu的思路,使用了全部可用的硬件核心。

bash 复制代码
nvcc -O3  -o mandelbrot_gpu mandelbrot_gpu_2.cu
bash 复制代码
./mandelbrot_gpu -r 320 -b 320 -i vector
Testing with image size 320x320 and 320 max iterations.

Running launch_mandelbrot_gpu_vector ...
  Runtime:  52.63 ms
  
#<<<80,32*4>>>
./mandelbrot_gpu -r 320 -b 320 -i vector_multicore
Testing with image size 320x320 and 320 max iterations.

Running launch_mandelbrot_gpu_vector_multicore ...
  Runtime:   0.10 ms
  
#<<<160,32*2>>>
./mandelbrot_gpu -r 320 -b 320 -i vector_multicore
Testing with image size 320x320 and 320 max iterations.

Running launch_mandelbrot_gpu_vector_multicore ...
  Runtime:   0.09 ms
  
  
#<<<40,32*8>>> sm未充分使用
./mandelbrot_gpu -r 320 -b 320 -i vector_multicore
Testing with image size 320x320 and 320 max iterations.

Running launch_mandelbrot_gpu_vector_multicore ...
  Runtime:   0.14 ms

Multi-threaded parallelism

整体思路类似Multiple core parallelism,区别是并发数目大于硬件核心数。

示例代码与多核并行相同,只是增加了 thread 数量。可以发现Multi-threaded parallelism中,随着并发数(threaded数目)增加,性能先明显提升,后提升放缓,最后不再提升。通常的原因是一个thread中会因为cache miss等原因,让硬件stall。

  • 而更多的threads可以通过thread的调度来隐藏硬件latency。
  • 通常你的 ILP 做的越好,任务越是compute-bound,这个提升也就越小。
  • 当硬件资源(比如ALU没有闲置,内存带宽没有闲置等)被打满后,再提升 thread 数目就不再能取得正向效果。

最后一次更新时间:2026-08-05 16:12:21 CST

相关推荐
碧海银沙音频科技研究院1 小时前
8基于BES2700IHC数字助听器系统开发
嵌入式硬件·算法
HZZD_HZZD2 小时前
智能电表选CoAP还是MQTT?合众致达万表实测:CoAP省流17.3%、续航提升38%,附报文拆解与选型矩阵
算法
linux-hzh2 小时前
百日算法修炼 · Day 05
java·算法
paopaokaka_luck2 小时前
基于springboot3+vue3的云南本土影视文旅推荐平台(协同过滤算法、Echarts图形化分析)
前端·spring boot·学习·算法·echarts·mybatis
Ivanqhz2 小时前
泰勒展开(Taylor Expansion)
算法·决策树·机器学习·集成学习
zander2584 小时前
34. 在排序数组中查找元素的第一个和最后一个位置:用两个边界定位区间
数据结构·算法·leetcode
郑州光合科技余经理9 小时前
代驾系统架构拆解:订单链路、权限组织与私有化源码交付
开发语言·后端·算法·架构·系统架构·uni-app·php
To_OC11 小时前
LC 35 搜索插入位置:二分查找谁都会,边界条件谁写谁懵
javascript·算法·leetcode
码哥DFS11 小时前
二叉树的直径
开发语言·javascript·算法