首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >CUDA编程:Histogram 直方图统计与 Atomic 优化

CUDA编程:Histogram 直方图统计与 Atomic 优化

作者头像
Michael阿明
发布2026-07-22 10:26:18
发布2026-07-22 10:26:18
620
举报

文章目录

  • 1. 本课目标
  • 2. 什么是 Histogram?
  • 3. 为什么 Histogram 在 GPU 上有难点?
  • 4. 两种实现方式
    • Global Atomic Histogram
    • Shared Memory 局部 Histogram
  • 5. 实验设计
  • 6. CUDA C++ 代码
  • 7. 编译与运行
  • 8. 推荐实验组合
    • 8.1 均匀分布
    • 8.2 热点分布
    • 8.3 数据规模变化

CUDA 第 12 课:Histogram 直方图统计与 Atomic 优化

第 10 课讲了 Reduction:很多数归约成一个数

第 11 课讲了 Prefix Scan:很多数生成前缀位置

第 12 课进入另一个非常常见的并行模式:

❝Histogram:把大量数据按照类别/bin 统计频次。

它在图像处理、推荐系统、日志统计、token 分布统计、排序预处理、采样分析中都非常常见。

1. 本课目标

本节课重点掌握:

代码语言:javascript
复制
1. 什么是 histogram 直方图
2. 为什么 histogram 容易产生 atomic 冲突
3. 掌握 global atomic histogram 的基础写法
4. 掌握 shared memory 局部 histogram 优化
5. 理解“先局部统计,再全局合并”的优化思想
6. 对比不同 bin 数量、数据分布下的性能差异

2. 什么是 Histogram?

假设输入数组是:

代码语言:javascript
复制
input = [1, 2, 1, 3, 2, 1, 0, 3]

我们希望统计每个数字出现了多少次。

如果 bin 数量是 4,也就是统计 0,1,2,3 四类:

代码语言:javascript
复制
hist[0] = 1
hist[1] = 3
hist[2] = 2
hist[3] = 2

最终结果:

代码语言:javascript
复制
hist = [1, 3, 2, 2]
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

3. 为什么 Histogram 在 GPU 上有难点?

如果每个线程处理一个元素:

代码语言:javascript
复制
int bin = input[idx];
atomicAdd(&hist[bin], 1);

问题是:很多线程可能同时更新同一个 bin。

例如输入里大量元素都是 1

代码语言:javascript
复制
input = [1, 1, 1, 1, 1, 1, ...]

那么所有线程都执行:

代码语言:javascript
复制
atomicAdd(&hist[1], 1);

这会造成严重竞争。

所以 histogram 的核心难点是:

代码语言:javascript
复制
多个线程可能同时写同一个地址
必须用 atomic 保证正确性
但 atomic 冲突会影响性能

4. 两种实现方式

Global Atomic Histogram

最简单写法:

代码语言:javascript
复制
atomicAdd(&global_hist[bin], 1);

优点:

代码语言:javascript
复制
代码简单
结果正确
适合 bin 多、冲突不严重的情况

缺点:

代码语言:javascript
复制
所有 block 都直接竞争 global memory
当数据集中在少数 bin 时会很慢

Shared Memory 局部 Histogram

优化思路:

代码语言:javascript
复制
每个 block 先在 shared memory 中统计一个局部 hist
然后每个 block 再把局部 hist 合并到 global hist

流程:

代码语言:javascript
复制
Global input
    ↓
每个 block 建立 shared_hist
    ↓
block 内线程 atomicAdd(shared_hist[bin], 1)
    ↓
block 内统计完成
    ↓
把 shared_hist 合并到 global_hist

这样可以把大量 global atomic 竞争变成:

代码语言:javascript
复制
block 内 shared atomic
+
少量 global atomic

通常会更快,尤其是 bin 数量不大时。

5. 实验设计

我们实现并比较:

代码语言:javascript
复制
1. CPU histogram
2. GPU global atomic histogram
3. GPU shared memory histogram

测试维度:

代码语言:javascript
复制
1. bin 数量:16、64、256
2. 数据规模:1M、16M、64M
3. 数据分布:
   - 均匀分布:冲突较平均
   - 热点分布:大量数据集中在少数 bin

6. CUDA C++ 代码

保存为:

代码语言:javascript
复制
lesson12_histogram.cu

代码如下:

代码语言:javascript
复制
#include <cuda_runtime.h>

#include <algorithm>
#include <chrono>
#include <cstdlib>
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>

#define CUDA_CHECK(call)                                                        \
    do {                                                                        \
        cudaError_t err = call;                                                 \
        if (err != cudaSuccess) {                                               \
            std::cerr << "CUDA Error: " << cudaGetErrorString(err)              \
                      << " at " << __FILE__ << ":" << __LINE__ << std::endl;    \
            std::exit(EXIT_FAILURE);                                            \
        }                                                                       \
    } while (0)

/*
 * CPU histogram reference.
 */
void histogram_cpu(const int* input, int* hist, size_t n, int bins) {
    std::fill(hist, hist + bins, 0);

    for (size_t i = 0; i < n; ++i) {
        int bin = input[i];
        hist[bin]++;
    }
}

/*
 * 方法 1:Global atomic histogram。
 *
 * 每个线程读取一个 input,然后直接 atomicAdd 到 global hist。
 */
__global__ void histogram_global_atomic_kernel(const int* input,
                                               int* global_hist,
                                               size_t n) {
    size_t idx = static_cast<size_t>(blockIdx.x) * blockDim.x + threadIdx.x;

    if (idx < n) {
        int bin = input[idx];
        atomicAdd(&global_hist[bin], 1);
    }
}

/*
 * 方法 2:Shared memory 局部 histogram。
 *
 * 每个 block 先在 shared memory 中统计局部 hist,
 * 然后再合并到 global hist。
 */
__global__ void histogram_shared_kernel(const int* input,
                                        int* global_hist,
                                        size_t n,
                                        int bins) {
    extern __shared__ int shared_hist[];

    unsigned int tid = threadIdx.x;

    /*
     * 1. 初始化 shared_hist。
     *
     * bins 可能大于 blockDim.x,所以用 stride 循环。
     */
    for (int i = tid; i < bins; i += blockDim.x) {
        shared_hist[i] = 0;
    }

    __syncthreads();

    /*
     * 2. 每个线程处理多个 input 元素。
     *
     * grid-stride loop:
     * 让总线程数不足以覆盖 n 时,也可以处理完整数组。
     */
    size_t global_tid = static_cast<size_t>(blockIdx.x) * blockDim.x + threadIdx.x;
    size_t stride = static_cast<size_t>(blockDim.x) * gridDim.x;

    for (size_t i = global_tid; i < n; i += stride) {
        int bin = input[i];
        atomicAdd(&shared_hist[bin], 1);
    }

    __syncthreads();

    /*
     * 3. 把 shared_hist 合并到 global_hist。
     *
     * 每个 block 对每个 bin 最多做一次 global atomic。
     */
    for (int i = tid; i < bins; i += blockDim.x) {
        if (shared_hist[i] > 0) {
            atomicAdd(&global_hist[i], shared_hist[i]);
        }
    }
}

/*
 * 用 cudaEvent 计时一个 kernel。
 */
template <typename LaunchFunc>
float time_kernel(LaunchFunc launch, int repeat) {
    /*
     * warmup
     */
    launch();
    CUDA_CHECK(cudaDeviceSynchronize());

    cudaEvent_t start, stop;
    CUDA_CHECK(cudaEventCreate(&start));
    CUDA_CHECK(cudaEventCreate(&stop));

    float total_ms = 0.0f;

    for (int r = 0; r < repeat; ++r) {
        CUDA_CHECK(cudaEventRecord(start));

        launch();

        CUDA_CHECK(cudaEventRecord(stop));
        CUDA_CHECK(cudaEventSynchronize(stop));

        float ms = 0.0f;
        CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop));
        total_ms += ms;
    }

    CUDA_CHECK(cudaEventDestroy(start));
    CUDA_CHECK(cudaEventDestroy(stop));

    return total_ms / repeat;
}

/*
 * 校验两个 histogram 是否一致。
 */
bool check_hist(const std::vector<int>& a,
                const std::vector<int>& b,
                int bins) {
    for (int i = 0; i < bins; ++i) {
        if (a[i] != b[i]) {
            std::cerr << "Mismatch at bin " << i
                      << ": CPU=" << a[i]
                      << ", GPU=" << b[i] << std::endl;
            return false;
        }
    }

    return true;
}

/*
 * 生成输入数据。
 *
 * mode = uniform:
 *   每个 bin 概率基本相同。
 *
 * mode = hot:
 *   90% 的数据集中在 bin 0,剩下 10% 随机分布。
 */
void generate_input(std::vector<int>& input,
                    int bins,
                    const std::string& mode) {
    std::mt19937 rng(12345);
    std::uniform_int_distribution<int> uniform_dist(0, bins - 1);
    std::uniform_real_distribution<float> prob(0.0f, 1.0f);

    for (size_t i = 0; i < input.size(); ++i) {
        if (mode == "hot") {
            if (prob(rng) < 0.90f) {
                input[i] = 0;
            } else {
                input[i] = uniform_dist(rng);
            }
        } else {
            input[i] = uniform_dist(rng);
        }
    }
}

int main(int argc, char** argv) {
    size_t n = 1ULL << 24;     // 16,777,216 elements
    int bins = 256;
    int block_size = 256;
    int repeat = 10;
    std::string mode = "uniform";

    if (argc >= 2) {
        n = static_cast<size_t>(std::atoll(argv[1]));
    }

    if (argc >= 3) {
        bins = std::atoi(argv[2]);
    }

    if (argc >= 4) {
        block_size = std::atoi(argv[3]);
    }

    if (argc >= 5) {
        repeat = std::atoi(argv[4]);
    }

    if (argc >= 6) {
        mode = argv[5];
    }

    if (bins <= 0 || bins > 4096) {
        std::cerr << "bins must be in (0, 4096].\n";
        return 1;
    }

    if (block_size <= 0 || block_size > 1024) {
        std::cerr << "block_size must be in (0, 1024].\n";
        return 1;
    }

    size_t input_bytes = n * sizeof(int);
    size_t hist_bytes = static_cast<size_t>(bins) * sizeof(int);

    std::cout << "CUDA Lesson 12: Histogram and Atomic Optimization\n";
    std::cout << "Elements   : " << n << "\n";
    std::cout << "Input size : " << input_bytes / 1024.0 / 1024.0 << " MB\n";
    std::cout << "Bins       : " << bins << "\n";
    std::cout << "Block size : " << block_size << "\n";
    std::cout << "Repeat     : " << repeat << "\n";
    std::cout << "Mode       : " << mode << "\n";

    std::vector<int> h_input(n);
    std::vector<int> h_cpu_hist(bins, 0);
    std::vector<int> h_global_hist(bins, 0);
    std::vector<int> h_shared_hist(bins, 0);

    generate_input(h_input, bins, mode);

    /*
     * CPU timing.
     */
    auto cpu_start = std::chrono::high_resolution_clock::now();

    histogram_cpu(h_input.data(),
                  h_cpu_hist.data(),
                  n,
                  bins);

    auto cpu_end = std::chrono::high_resolution_clock::now();

    double cpu_ms = std::chrono::duration<double, std::milli>(
        cpu_end - cpu_start
    ).count();

    int* d_input = nullptr;
    int* d_global_hist = nullptr;
    int* d_shared_hist = nullptr;

    CUDA_CHECK(cudaMalloc(&d_input, input_bytes));
    CUDA_CHECK(cudaMalloc(&d_global_hist, hist_bytes));
    CUDA_CHECK(cudaMalloc(&d_shared_hist, hist_bytes));

    CUDA_CHECK(cudaMemcpy(d_input,
                          h_input.data(),
                          input_bytes,
                          cudaMemcpyHostToDevice));

    /*
     * Global atomic kernel 配置。
     */
    int grid_global = static_cast<int>((n + block_size - 1) / block_size);

    auto launch_global = [&]() {
        CUDA_CHECK(cudaMemset(d_global_hist, 0, hist_bytes));

        histogram_global_atomic_kernel<<<grid_global, block_size>>>(
            d_input,
            d_global_hist,
            n
        );
        CUDA_CHECK(cudaGetLastError());
    };

    /*
     * Shared histogram kernel 配置。
     *
     * 使用固定 block 数量,配合 grid-stride loop。
     * 不要让 block 数过大,否则每个 block 都要合并一次 hist,global atomic 次数也会上升。
     */
    int sm_count = 0;
    CUDA_CHECK(cudaDeviceGetAttribute(&sm_count,
                                      cudaDevAttrMultiProcessorCount,
                                      0));

    int grid_shared = sm_count * 4;
    size_t shared_bytes = hist_bytes;

    auto launch_shared = [&]() {
        CUDA_CHECK(cudaMemset(d_shared_hist, 0, hist_bytes));

        histogram_shared_kernel<<<grid_shared,
                                  block_size,
                                  shared_bytes>>>(
            d_input,
            d_shared_hist,
            n,
            bins
        );
        CUDA_CHECK(cudaGetLastError());
    };

    float global_ms = time_kernel(launch_global, repeat);
    float shared_ms = time_kernel(launch_shared, repeat);

    CUDA_CHECK(cudaMemcpy(h_global_hist.data(),
                          d_global_hist,
                          hist_bytes,
                          cudaMemcpyDeviceToHost));

    CUDA_CHECK(cudaMemcpy(h_shared_hist.data(),
                          d_shared_hist,
                          hist_bytes,
                          cudaMemcpyDeviceToHost));

    bool global_ok = check_hist(h_cpu_hist, h_global_hist, bins);
    bool shared_ok = check_hist(h_cpu_hist, h_shared_hist, bins);

    std::cout << std::fixed << std::setprecision(4);

    std::cout << "\n[Timing]\n";
    std::cout << "CPU histogram time          : " << cpu_ms << " ms\n";
    std::cout << "GPU global atomic time      : " << global_ms << " ms\n";
    std::cout << "GPU shared histogram time   : " << shared_ms << " ms\n";

    std::cout << "\n[Speedup]\n";
    std::cout << "Global atomic vs CPU        : " << cpu_ms / global_ms << "x\n";
    std::cout << "Shared histogram vs CPU     : " << cpu_ms / shared_ms << "x\n";
    std::cout << "Shared vs Global atomic     : " << global_ms / shared_ms << "x\n";

    std::cout << "\n[Check]\n";
    std::cout << "Global atomic result        : " << (global_ok ? "PASS" : "FAIL") << "\n";
    std::cout << "Shared histogram result     : " << (shared_ok ? "PASS" : "FAIL") << "\n";

    std::cout << "\n[Sample hist bins]\n";
    int show_bins = std::min(bins, 16);
    for (int i = 0; i < show_bins; ++i) {
        std::cout << "bin[" << i << "] = " << h_cpu_hist[i] << "\n";
    }

    CUDA_CHECK(cudaFree(d_input));
    CUDA_CHECK(cudaFree(d_global_hist));
    CUDA_CHECK(cudaFree(d_shared_hist));

    return 0;
}

7. 编译与运行

Tesla T4:

代码语言:javascript
复制
nvcc -O3 -arch=sm_75 lesson12_histogram.cu -o lesson12_histogram

默认运行:

代码语言:javascript
复制
./lesson12_histogram

指定参数:

代码语言:javascript
复制
./lesson12_histogram 16777216 256 256 10 uniform

参数含义:

代码语言:javascript
复制
第 1 个参数:元素数量 n
第 2 个参数:bins 数量
第 3 个参数:block_size
第 4 个参数:repeat
第 5 个参数:mode,uniform 或 hot

8. 推荐实验组合

8.1 均匀分布

代码语言:javascript
复制
./lesson12_histogram 16777216 16 256 10 uniform
./lesson12_histogram 16777216 64 256 10 uniform
./lesson12_histogram 16777216 256 256 10 uniform

Bins

CPU Histogram (ms)

GPU Global Atomic (ms)

GPU Shared Histogram (ms)

Global vs CPU

Shared vs CPU

Shared vs Global Atomic

16

11.1612

6.7412

0.3134

1.6557x

35.6119x

21.5091x

64

15.2359

10.5726

0.3208

1.4411x

47.4879x

32.9531x

256

13.2023

9.6184

0.3251

1.3726x

40.6109x

29.5868x

8.2 热点分布

代码语言:javascript
复制
./lesson12_histogram 16777216 16 256 10 hot
./lesson12_histogram 16777216 64 256 10 hot
./lesson12_histogram 16777216 256 256 10 hot

Bins

CPU Histogram(ms)

GPU Global Atomic(ms)

GPU Shared Histogram(ms)

Global vs CPU

Shared vs CPU

Shared vs Global Atomic

16

59.6011

22.5139

0.5098

2.6469x

116.9147x

44.1636x

64

44.0567

17.2838

0.5066

2.5490x

86.9688x

34.1216x

256

46.7463

17.3613

0.5096

2.6920x

91.7194x

34.0683x

观察:

代码语言:javascript
复制
90% 数据集中在 bin 0
global atomic 明显变慢
shared histogram 通常更有优势

8.3 数据规模变化

代码语言:javascript
复制
./lesson12_histogram 1048576 256 256 10 uniform
./lesson12_histogram 16777216 256 256 10 uniform
./lesson12_histogram 67108864 256 256 10 uniform

Elements

Input Size

CPU Histogram(ms)

GPU Global Atomic(ms)

GPU Shared Histogram(ms)

Global vs CPU

Shared vs CPU

Shared vs Global Atomic

1,048,576

4 MB

2.6045

0.5900

0.0203

4.4144x

128.3005x

29.0640x

16,777,216

64 MB

59.9670

8.7032

0.3137

6.8907x

191.1559x

27.7443x

67,108,864

256 MB

172.6915

25.8392

1.2761

6.6813x

135.3224x

20.2492x

观察:

代码语言:javascript
复制
数据越大,GPU 并行统计优势越明显

结论:

代码语言:javascript
复制
1. Histogram 是典型的 many-to-few 统计问题
2. 多线程更新同一个 bin 时必须处理写冲突
3. global atomic 写法简单,但热点 bin 会造成严重竞争
4. shared memory 局部 histogram 可以减少 global atomic 次数
5. 优化思想是“先 block 内局部统计,再跨 block 合并”
6. bins 数量和数据分布会显著影响性能

一句话总结:

❝Histogram 优化的本质,是把全局范围的大量写冲突,拆成 block 内局部冲突和少量全局合并。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Michael阿明 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章目录
  • 1. 本课目标
  • 2. 什么是 Histogram?
  • 3. 为什么 Histogram 在 GPU 上有难点?
  • 4. 两种实现方式
    • Global Atomic Histogram
    • Shared Memory 局部 Histogram
  • 5. 实验设计
  • 6. CUDA C++ 代码
  • 7. 编译与运行
  • 8. 推荐实验组合
    • 8.1 均匀分布
    • 8.2 热点分布
    • 8.3 数据规模变化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档