【问题标题】:Efficient gather (of whole rows) from a large matrix从大型矩阵中高效收集(整行)
【发布时间】:2021-08-28 16:52:16
【问题描述】:

我正在尝试执行一个简单的操作。我有一个大小为 A x B 的矩阵。我有一个长度为 C 的索引列表,我想通过根据索引从第一个矩阵中收集行来制作一个 C x B 矩阵。即索引 i 告诉我将第一个矩阵中的哪一行放入第二个矩阵的第 i 行。

我对索引进行了预排序,因此算法输入是固定的:我从 A x B 矩阵加载行并将该行写入 C x B 矩阵中的所有行。

代码如下所示:

for(int i = 0;i < A; i ++)
{
    for(int k = offsets[i]; k < offsets[i+1]; k ++)
    {
           int dest = index1[k];
                
           for(int j = 0;j < C/ 8; j++)
           {
                __m256 a = _mm256_load_ps(&input[i * C + j * 8]);
                _mm256_store_ps(&output[dest * C + j * 8] ,a);
           }
     }
 }

代码完全被写入内存所限制。 当 C 较小时,此代码是有效的。然而,当 C 增加时,它的扩展性很差,我推测这是由于缓存行为。 (与 C = 256 相比,C = 1024 需要 10 倍的时间)。

我尝试在 C 维度进行阻塞:

for(int c = 0; c < C; c+= K){
for(int i = 0;i < A; i ++)
{
    for(int k = offsets[i]; k < offsets[i+1]; k ++)
    {
           int dest = index1[k];
                
           for(int j = 0;j < C/ 8 / K; j++)
           {
                __m256 a = _mm256_load_ps(&input[i * C + c + j * 8]);
                _mm256_store_ps(&output[dest * C + c + j * 8] ,a);
           }
     }
 }
}

这实际上会进一步减慢代码速度。

有什么建议吗?

【问题讨论】:

  • 如果您只触及每个值一次,阻塞无法帮助您解决内存瓶颈问题。它可以提供帮助,例如如果您使用更昂贵的操作(例如矩阵-矩阵乘积)进行提取。
  • 我不明白为什么当C维度增加时内存瓶颈会加剧。不幸的是,我没有任何东西可以用于内存写入。
  • 请注意,在许多机器上,内存通常不会因为只有一个核心而饱和(这里有点平行 mat vélo(除了后面说的流存储)
  • 流媒体商店你指的是vmovntps吗?

标签: c performance caching linear-algebra avx2


【解决方案1】:

似乎内部循环只是一个流式复制操作。在这种情况下,缓存无关紧要。而是尝试使用简单的 memcpy() 代替,以便编译器可以产生更好的执行代码,希望如此。

//for(int j = 0;j < C/ 8; j++)
//{
//     __m256 a = _mm256_load_ps(&input[i * C + j * 8]);
//     _mm256_store_ps(&output[dest * C + j * 8] ,a);
//}

memcpy(&output[dest * C], &input[i * C], C * sizeof(float));

附录

如果不能得到满意的结果,最后还是选择 C++ 并用 parllel_for() 替换外循环。那么就有可能使缓存(或其他管道?)工作得更好一点。

parallel_for(0, A, [&](const int i) {

    for(int k = offsets[i]; k < offsets[i+1]; k++)
    {
       int dest = index1[k];
       memcpy(&output[dest * C], &input[i * C], C * sizeof(float));
     }
});

【讨论】:

  • memcpy 在后台执行完全相同的操作。 (或者我相信)至少我无法区分运行时。
  • 哦,可能是这样。编译器已经使用了所有可能的优化。如果编译器也已经适当地插入了预取指令,则可能没有太多空间进行进一步优化。
  • @bumpbump:glibc memcpy 有一个展开循环,对于 L1d 缓存中热数据的中型副本(如 2 到 16kiB)可能更快。不过,它确实有函数调用开销。不过,对于非大数据,每次迭代复制 32 字节的紧密循环是合理的,尤其是在 Ice Lake 之前的 CPU 上(对于 code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/…
猜你喜欢
  • 1970-01-01
  • 2014-07-06
  • 1970-01-01
  • 1970-01-01
  • 2013-10-21
  • 2014-04-18
  • 1970-01-01
  • 2023-03-27
  • 1970-01-01
相关资源
最近更新 更多