性能不如OpenBLAS因内存未对齐、无缓存分块及缺预取;需32字节对齐分配、8×8分块、B转置、NTA预取,并统一处理trans/batch场景。

直接用 _mm256_load_ps 和 _mm256_fmadd_ps 写三层循环,性能大概率不如 OpenBLAS 的 sgemm——瓶颈不在算力,而在内存对齐、缓存分块和预取节奏。
内存必须 32 字节对齐,否则 _mm256_load_ps 会崩溃或静默降级
AVX2 要求 float 数组起始地址能被 32 整除。用 new float[n] 或 std::vector<float></float> 分配的内存几乎从不满足这点,轻则触发 #GP 异常,重则静默降级为慢速路径,性能断崖下跌。
- 改用
_mm_malloc(size, 32)分配,配对用_mm_free(ptr)释放;aligned_alloc(32, size)也可,但要确保size % 32 == 0 - 验证是否对齐:
(uintptr_t)ptr & 31结果必须恒为 0 - 非对齐场景下可退用
_mm256_loadu_ps,但性能下降 10–30%,且易破坏流水线 - 结构体成员或栈数组需显式对齐:
float data[64] __attribute__((aligned(32)))
不做分块(blocking),L1d cache 命中率低于 30%
不分块的手写 SIMD 矩阵乘,每算一个 C[i][j] 都要重新加载整行 A 和整列 B,而 B 的列访问是跨步的,L1d 缓存行反复被踢出。实测 miss rate 超过 70%,CPU 大部分时间在等数据。
- 至少做两级分块:内层用 8×8 tile(适配 AVX2 的 8 float 宽度),外层按 64×64 匹配 L2 容量
- 块内索引用
i*8 + r、j*8 + c(r/c ∈ [0,7]),禁用除法或模运算 - 对 B 预转置为
BT(列→行),使BT[j][k]连续加载;这步预处理仅占总时间 1.2%,却减少 65% 地址计算 - 每个 tile 计算中,A 数据只加载一次,B 数据按 8 元素复用,C 累加进寄存器再批量回写
_mm_prefetch 不加,AVX 吞吐率掉一半
硬件预取器对非规则访存(如 B 的列访问)基本失效。L2→L1 延迟约 12–15 cycle,而 _mm256_fmadd_ps 只需 1–2 cycle,空等占大头。
立即学习“C++免费学习笔记(深入)”;
- 计算当前 A 块前,用
_mm_prefetch(&A[i + 64], _MM_HINT_NTA)提前拉取后续 64 字节(2 个 AVX 向量) - 对 B,沿列方向每隔 8 行预取一次:
_mm_prefetch(&B[(t+8) * ldb + j], _MM_HINT_NTA) - 统一用
_MM_HINT_NTA:避免污染 cache,适合只读一次的大块数据 - 预取位置必须校准——太早拉取可能被驱逐,太晚等于没 prefetch
batch 和 transpose 场景别硬塞进一个函数
深度学习里 matmul 多数不是纯 A×B,而是 B×AT 或 batched([b, m, k] × [b, k, n])。强行用 if-else 切换 layout,容易错在指针偏移和步长(ldA、ldB)上。
- 定义统一接口:
simd_gemm(bool transA, bool transB, int M, int N, int K, const float* A, int ldA, ...) - transpose 不现场转,改访问模式:
transA ? A[j*ldA + i] : A[i*ldA + j] - batch 场景下,外层循环跑 b 次,每次调用前更新指针:
A += b_stride、B += b_stride、C += b_stride - 步长(
ldA/ldB)必须随 layout 动态调整,否则越界或错位读写
最容易被忽略的是:维度没按 8 或 16 向上补齐时,尾部处理逻辑退化成标量,这部分耗时可能超预期;还有 _mm256_set1_ps 放在内层循环里重复生成广播常量,白白浪费指令周期。


















