c++ - 缓存敏感矩阵乘法的多级平铺

标签 c++ matrix-multiplication cpu-cache tiling

为了好玩,我正在编写自己的 GeMM 子程序。我已经设法使用 AVX256 内核在 L1 缓存上实现了平铺版本。我知道有一些循环不变量我可以从下面提升。

template<size_t N, size_t P, size_t M>
void intrinsic_multiply(double* A, double* B, void* C) {
    alignas(32) double _A[PACK_SIZE * PACK_SIZE];
    alignas(32) double _B[PACK_SIZE * PACK_SIZE];
    double* _C = (double*)(C);

    constexpr size_t N_rem = N%PACK_SIZE;
    constexpr size_t M_rem = M%PACK_SIZE;
    constexpr size_t P_rem = P%PACK_SIZE;

    constexpr size_t N_spill = N-N_rem+(PACK_SIZE)*(size_t)(N_rem!=0);
    constexpr size_t M_spill = M-M_rem+(PACK_SIZE)*(size_t)(M_rem!=0);
    constexpr size_t P_spill = P-P_rem+(PACK_SIZE)*(size_t)(P_rem!=0);

    for(size_t i = 0; i < N_spill; i += PACK_SIZE) {
        for(size_t j = 0; j < M_spill; j += PACK_SIZE) {
            for(size_t k = 0; k < P_spill; k += PACK_SIZE) {
                pad_cols<N>(A + i + k * N, _A, ((size_t)(i+PACK_SIZE>N))*N_rem, ((size_t)(k+PACK_SIZE>P))*P_rem);
                pad_cols<P>(B + k + j * P, _B, ((size_t)(k+PACK_SIZE>P))*P_rem, ((size_t)(j+PACK_SIZE>M))*M_rem);
                macro_kernal_intrinsic<N_spill>(_A, _B, _C + i + (j * N_spill));
            }
        }
    }
}
我很难实现多级缓存平铺,因为缓存不是彼此的倍数。每个缓存的步幅大小的估计计算如下, CACHE_SIZE s 以字节为单位。
static constexpr size_t L3_CACHESIZE = 6291456;
static size_t L3_STRIDE_SIZE = (size_t)floor((sqrt(L3_CACHESIZE/sizeof(double))));

static constexpr size_t L2_CACHESIZE = 262144;
static size_t L2_STRIDE_SIZE = (size_t)floor((sqrt(L2_CACHESIZE/sizeof(double))));

static constexpr size_t L1_CACHESIZE = 32768;
static size_t L1_STRIDE_SIZE = (size_t)floor((sqrt(L1_CACHESIZE/sizeof(double))));

static constexpr size_t PACK_SIZE = 64;
这给出了 L1 步幅大小为 64 和 L2 步幅大小为 181。显然这些不是彼此的倍数。我有两个选择——
  • 每次 L2 迭代只适合 4 个 L1 块,从 0 到 63 到 127。这似乎我正在利用我的 L2 缓存。
  • 使用整个 L2 缓存并在最后一次迭代中用 0 填充 (64*3-180) 元素。这会引入很多冗余操作,但只会将 L2 步幅大小减少 1。
  • 为下一次迭代预取小数块。
  • 有一种我不知道的规范方法。

  • 在实践中处理彼此不是倍数的块大小的最佳方法是什么?
    编辑 - 回应 MSalters:
    我跑了
    nm -AP -t d --demangle ./src/example-GeMM | grep "intrinsic"
    
    这使
    ./src/example-GeMM: void intrinsic_multiply<2048ul, 3136ul, 2240ul>(double*, double*, void*) W 17081 434
    ./src/example-GeMM: void macro_kernal_intrinsic<2048ul>(double*, double*, double*) W 20409 234
    ./src/example-GeMM: void micro_kernal_intrinsic<2048ul>(double*, double*, double*) W 21343 454
    
    这意味着相关的代码部分占用 (234+454+434)/262144 < 1% 的 L2 缓存,因此我仍然有动力将 L2 缓存的更大部分用于数据。

    最佳答案

    我的假设是 L1 被拆分,然后 32Kb 只是 L1d 缓存。 L2 将是统一的,这意味着您不应仅将其全部用于您的数据。
    另外,我还不会太担心循环提升。与所有 constexpr ,优化器有机会发现提升机。一个问题是C似乎是外变量。因为那是 void* ,它可以别名 size_t .你写了吗double* C ,这不能别名 size_t .这是一个例子,类型安全不仅对安全有好处,而且对速度也有好处。

    关于c++ - 缓存敏感矩阵乘法的多级平铺,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/66009625/

    相关文章:

    c++ - 将 Python 3 Unicode 转换为 std::string 的简洁方法

    c++ - 从 WndProc 调用非静态成员

    python - 在 numpy 中,将两个结构化矩阵简洁地相乘

    c++ - 数据大于缓存的 3D FFT

    x86 - Core i7 中每核 L2 和 L3 之间的互连

    c - 测试缓存失效和刷新

    c++ - C++ 中的无限 While 循环

    c++ - C++重载函数,包括bool和整数参数

    java - LWJGL - 使用四元数和平移矩阵在 6DOF 相机中实现 'roll' 的问题

    java - 使用 lwjgl 实现第三人称相机