CPU 矩阵乘法优化

1. 结论

核心发现:通过逐步优化,矩阵乘法性能可提升 17倍以上

优化方法 峰值性能 相对基准 关键技术
单线程循环重排 (ikj) 31.66 GFLOPs 1x (基准) 缓存局部性
单线程分块 31.45 GFLOPs 1x 分块 + 缓存优化
OpenMP 简单并行 5.31 GFLOPs 0.17x (更慢!) 缓存竞争导致性能下降
OpenMP + 分块 77.01 GFLOPs 2.4x 多线程 + 缓存优化
OpenBLAS 538.35 GFLOPs 17x AVX-512 + 专业优化
关键结论:

2. 实现原理

2.1 矩阵乘法基础

矩阵乘法 C = A × B,其中 A[M×K], B[K×N], C[M×N]:

核心公式
C[i][j] = Σk=0..K-1 A[i][k] × B[k][j]
总浮点运算量
FLOPs = 2 × M × K × N
每个元素需要 K 次乘法 + K 次加法

2.2 为什么循环顺序影响性能?

CPU 缓存是按行(连续内存)加载的。不同循环顺序导致不同的内存访问模式:

循环顺序 B矩阵访问 缓存效率
ijk (朴素) 按列跳跃访问 差 - 每次访问都 cache miss
ikj (优化) 按行连续访问 好 - 利用缓存行
朴素 ijk — B矩阵按列跳跃访问 ↓
1
2
3
4
Cache Miss 频繁
优化 ikj — B矩阵按行连续访问 →
1
2
3
4
缓存行命中

2.3 分块优化原理

将大矩阵分成小块,使每个块能完全放入 L1/L2 缓存:

大矩阵 (无法放入缓存)
N × N 连续内存
BLOCK=64
分块后 (每块放入缓存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

2.4 OpenMP 并行化

使用 #pragma omp parallel for 将工作分配给多个线程:

单线程
线程 0 处理全部
时间: 100%
多线程 (20核)
T0
T1
T2
T3
...
T19
5%
5%
5%
5%
5%
时间: ~5% (理想情况)

2.5 OpenBLAS 为什么这么快?

🔧
手写汇编
针对特定 CPU 架构
逐指令手工优化
SIMD 指令
AVX-512 一条指令
处理 16 个 float
🧠
Goto BLAS 算法
精确匹配 L1/L2/L3
缓存层级大小
💾
寄存器分块
最大化利用
32 个 AVX 寄存器
🚀
内存预取
提前加载下一批数据
隐藏内存延迟

3. 编译与运行

3.1 编译命令

# 基础版本
gcc -O3 -march=native -o matrix matrix.c -lm

# OpenMP 多线程版本
gcc -O3 -fopenmp -march=native -o matrix matrix.c -lm

# OpenBLAS 版本 (需要先安装: sudo apt install libopenblas-dev)
gcc -O3 -fopenmp -march=native -DUSE_OPENBLAS -o matrix matrix.c -lopenblas -lm

3.2 运行方式

# 默认 1024x1024
./matrix

# 指定维度
./matrix 2048

# 指定 M K N (非方阵)
./matrix 1024 512 2048

4. 核心源代码

4.1 朴素版本 (ijk)

void matmul_naive(const float* A, const float* B, float* C, int M, int K, int N) {
    for (int i = 0; i < M; i++) {
        for (int j = 0; j < N; j++) {
            float sum = 0.0f;
            for (int k = 0; k < K; k++) {
                sum += A[i * K + k] * B[k * N + j];  // B 按列访问,缓存不友好
            }
            C[i * N + j] = sum;
        }
    }
}

4.2 循环重排版本 (ikj)

void matmul_ikj(const float* A, const float* B, float* C, int M, int K, int N) {
    memset(C, 0, M * N * sizeof(float));
    for (int i = 0; i < M; i++) {
        for (int k = 0; k < K; k++) {
            float a_ik = A[i * K + k];  // 提取到寄存器
            for (int j = 0; j < N; j++) {
                C[i * N + j] += a_ik * B[k * N + j];  // B 按行访问,缓存友好
            }
        }
    }
}

4.3 分块版本

#define BLOCK_SIZE 64  // 块大小,通常选择 L1 缓存大小的 1/3

void matmul_blocked(const float* A, const float* B, float* C, int M, int K, int N) {
    memset(C, 0, M * N * sizeof(float));

    for (int i0 = 0; i0 < M; i0 += BLOCK_SIZE) {
        for (int k0 = 0; k0 < K; k0 += BLOCK_SIZE) {
            for (int j0 = 0; j0 < N; j0 += BLOCK_SIZE) {
                // 块内乘法
                int i_end = (i0 + BLOCK_SIZE < M) ? i0 + BLOCK_SIZE : M;
                int k_end = (k0 + BLOCK_SIZE < K) ? k0 + BLOCK_SIZE : K;
                int j_end = (j0 + BLOCK_SIZE < N) ? j0 + BLOCK_SIZE : N;

                for (int i = i0; i < i_end; i++) {
                    for (int k = k0; k < k_end; k++) {
                        float a_ik = A[i * K + k];
                        for (int j = j0; j < j_end; j++) {
                            C[i * N + j] += a_ik * B[k * N + j];
                        }
                    }
                }
            }
        }
    }
}

4.4 OpenMP 并行 + 分块

#include <omp.h>

void matmul_omp(const float* A, const float* B, float* C, int M, int K, int N) {
    memset(C, 0, M * N * sizeof(float));

    #pragma omp parallel for schedule(dynamic)  // 动态调度,负载均衡
    for (int i0 = 0; i0 < M; i0 += BLOCK_SIZE) {
        for (int k0 = 0; k0 < K; k0 += BLOCK_SIZE) {
            for (int j0 = 0; j0 < N; j0 += BLOCK_SIZE) {
                // ... 块内计算同上
            }
        }
    }
}

4.5 OpenBLAS 版本

#include <cblas.h>

void matmul_openblas(const float* A, const float* B, float* C, int M, int K, int N) {
    // C = alpha * A * B + beta * C
    cblas_sgemm(CblasRowMajor,   // 行优先存储
                CblasNoTrans,    // A 不转置
                CblasNoTrans,    // B 不转置
                M, N, K,         // 矩阵维度
                1.0f,            // alpha = 1
                A, K,            // A 矩阵
                B, N,            // B 矩阵
                0.0f,            // beta = 0
                C, N);           // C 矩阵
}

5. 运行结果与分析

5.1 测试环境

项目配置
CPU20 核心 (OpenMP 线程数: 20)
矩阵大小1024 × 1024 (单精度 float)
总浮点运算2.15 GFLOPs
内存占用12.00 MB

5.2 性能测试结果

================================================================================ 矩阵乘法性能测试 (FLOPs) ================================================================================ 矩阵维度: A[1024 x 1024] * B[1024 x 1024] = C[1024 x 1024] 总浮点运算: 2.15 GFLOPs 内存占用: 12.00 MB 预热次数: 2, 测试次数: 5 ================================================================================ 开始测试... 算法 | 时间 | 最快时间 | 平均性能 | 峰值性能 -------------------------------------------------------------------------------- 朴素 (ijk) | 跳过 (矩阵过大) 循环重排 (ikj) | 平均: 0.0680 s | 最快: 0.0678 s | 平均: 31.57 GFLOPs | 峰值: 31.66 GFLOPs 分块 (blocked) | 平均: 0.0685 s | 最快: 0.0683 s | 平均: 31.36 GFLOPs | 峰值: 31.45 GFLOPs └─ 结果验证: 通过 -------------------------------------------------------------------------------- OpenMP 线程数: 20 -------------------------------------------------------------------------------- OpenMP 简单并行 | 平均: 0.4149 s | 最快: 0.4045 s | 平均: 5.18 GFLOPs | 峰值: 5.31 GFLOPs └─ 结果验证: 通过 OpenMP + 分块 | 平均: 0.0313 s | 最快: 0.0279 s | 平均: 68.62 GFLOPs | 峰值: 77.01 GFLOPs └─ 结果验证: 通过 -------------------------------------------------------------------------------- OpenBLAS 测试 -------------------------------------------------------------------------------- OpenBLAS (sgemm) | 平均: 0.0057 s | 最快: 0.0040 s | 平均: 376.16 GFLOPs | 峰值: 538.35 GFLOPs └─ 结果验证: 通过 ================================================================================

5.3 结果分析

单线程优化效果:

OpenMP 简单并行为什么更慢?

OpenMP + 分块为什么有效?

OpenBLAS 为什么快这么多?

6. 更多加速技术

技术 预期性能 说明
Intel MKL ~600 GFLOPs Intel CPU 上比 OpenBLAS 更快 10-30%
GPU (cuBLAS) 30,000-80,000 GFLOPs RTX 4090 可达 ~80 TFLOPs (FP32)
FP16 半精度 2x FP32 精度降低换取速度,深度学习常用
Tensor Core 160,000+ GFLOPs NVIDIA GPU 专用矩阵加速单元
分布式计算 线性扩展 MPI + 多机协同

6.1 GPU 加速示例 (CUDA cuBLAS)

#include <cublas_v2.h>

// GPU 矩阵乘法
void matmul_gpu(const float* A, const float* B, float* C, int M, int K, int N) {
    cublasHandle_t handle;
    cublasCreate(&handle);

    float *d_A, *d_B, *d_C;
    cudaMalloc(&d_A, M * K * sizeof(float));
    cudaMalloc(&d_B, K * N * sizeof(float));
    cudaMalloc(&d_C, M * N * sizeof(float));

    cudaMemcpy(d_A, A, M * K * sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_B, B, K * N * sizeof(float), cudaMemcpyHostToDevice);

    float alpha = 1.0f, beta = 0.0f;
    cublasSgemm(handle, CUBLAS_OP_N, CUBLAS_OP_N,
                N, M, K, &alpha, d_B, N, d_A, K, &beta, d_C, N);

    cudaMemcpy(C, d_C, M * N * sizeof(float), cudaMemcpyDeviceToHost);

    cudaFree(d_A); cudaFree(d_B); cudaFree(d_C);
    cublasDestroy(handle);
}

6.2 性能层级总结

TPU / NPU
专用硬件
1000x+
GPU
cuBLAS / Tensor Core
500 - 1000x
BLAS 库
OpenBLAS / Intel MKL
15 - 20x
多线程 + SIMD
OpenMP 并行
2 - 10x
算法优化
循环重排 / 分块
1 - 2x
朴素实现
三重循环 ijk
1x (基准)

实际工程建议:

⬇ 点击下载 matrix_mul_benchmark.cu

GPU 矩阵乘法基准测试完整源码 (matrix_mul_benchmark.cu)

7. 各硬件平台编程生态

不同加速硬件有各自独立的编程接口,CUDA 仅适用于 NVIDIA GPU,不能用于 TPU 或 NPU:

💠
NVIDIA GPU
编程接口
CUDA / cuBLAS / cuDNN
生态最成熟,深度学习事实标准
Google TPU
编程接口
XLA / JAX / TensorFlow
Google Cloud 专属,大模型训练
🔥
AMD GPU
编程接口
ROCm / HIP / rocBLAS
HIP 可从 CUDA 代码移植
🧠
NPU (各厂商)
编程接口
厂商私有 SDK
华为 CANN / Apple CoreML / 高通 SNPE
关键区别:
硬件 编程接口 厂商 是否支持 CUDA
NVIDIA GPU CUDA / cuBLAS NVIDIA 支持 (原生)
AMD GPU ROCm / HIP AMD 不支持 (HIP 可移植)
Google TPU XLA / JAX Google 不支持
NPU CANN / CoreML / SNPE 华为 / Apple / 高通 不支持
Intel GPU oneAPI / SYCL Intel 不支持

测试环境: Ubuntu 22 / GCC / 20核 CPU

生成时间: 2026-01-29