【问题标题】:How not to mess up the cache when working on some long vectors in memory?在内存中处理一些长向量时如何不弄乱缓存?
【发布时间】:2015-04-26 13:07:48
【问题描述】:

前提

我想做一些涉及k 长数据向量(每个长度为n)的计算,我在主存中接收到这些数据,并将某种结果写回主存。为简单起见,还假设计算只是

for(i = 0; i < n; i++)
    v_out[i] = foo(v_1[i],v_2[i], ... ,v_k[i])

或许

for(i = 0; i < n; i++)
    v_out[i] = bar_k(...bar_2(bar_1(v_1[i]),v_2[i]), ... ),v_k[i])

(这不是代码,它是伪代码。)foo()bar_i() 函数没有副作用。 k 是常量(在编译时已知),n 仅在此计算发生之前才知道(并且它相对较大 - 至少比整个 L2 缓存大小大几倍甚至更多)。

假设我在 x86_64 处理器(Intel,或 AMD,或你有什么;选择很重要,可能)的单个内核上的单个线程上。最后,假设 foo() (resp. bar_i()) 不是密集计算,即从内存中读取数据并将其写回的时间相对于 n (resp. kx@ 987654333@) 调用 foo() (resp. bar_i())。

问题

我该如何安排这个计算,以避免:

  • 来自一个输入向量的数据正在清除另一个向量的缓存数据。
  • 输入矢量数据清除缓存的输出矢量数据。
  • bar_j(...bar_1(v_1[i])...) 的中间结果保留在寄存器或 L1 缓存中,如果有足够的容量将它们保存在那里,直到 v_{j+1}[i] ... v_k[i] 的数据到达并允许我们完成计算。 L2 也一样。
  • 当我们打算继续处理该缓存行中的元素时,输出向量的 L1 缓存行被清除。 L2 也一样。
  • 内存带宽利用率不足。
  • 尽可能的核心空闲时间。
  • v_out 上的写-读-写-读-写序列(如果这些写操作需要更新到主内存,这可能会非常昂贵;这里的动机是可能很容易只读取一个向量,更新输出并重复)。

注意事项:

  • 输入数据的任何重新排列都计入总计算时间。向量不会在交替排列中重复使用,因此基本上是浪费时间。
  • 如果它让您更容易假设对齐或缺乏对齐,那很好,直接说出来。
  • 使用 bar_i 函数进行计算可以提高访问模式的灵活性,但会带来额外的挑战 w.r.t. v_out 值的缓存。

【问题讨论】:

    标签: c++ c performance caching optimization


    【解决方案1】:

    来自一个输入向量的数据为另一个清除缓存数据 向量。

    如果您在同一个函数调用中使用 v_1[i]、v_2[i]、...、v_k[i],则输入向量不会清除为其他向量缓存的数据。对于您在向量中读取的每个元素,CPU 将只获取一个缓存行,而不是整个向量。所以如果你读取了 k 个元素,你会从每个向量中带来 k 个缓存行。

    输入向量数据清除缓存的输出向量数据。

    您的情况与上述相同。事实并非如此。

    输出向量数据保留在寄存器或 L1 缓存中的时间为 我们需要它(在酒吧的情况下)。

    您可以尝试在写入数据之前使用 _mm_prefetch 内部函数来获取数据。

    内存带宽利用率不足。

    为此,您需要最大化全幅交易的数量。基本上,您需要在 CPU 获取缓存行时立即使用所有元素。为此,您必须重新排列数据。我会将所有 k 个向量视为 k x n 个元素的矩阵,以列主要格式存储

    type* pMat = (type*)aligned_alloc(CACHE_LINE_SIZE, n * k * sizeof(type));
    v_0[i] = pMat[i * k + 0];
    v_1[i] = pMat[i * k + 1];
    // ...
    v_k-1[i] = pMat[i * k + k-1];
    

    这会将 v_0, ... v_k 元素放入 SIMD 寄存器中,您可能有机会进行更好的矢量化。

    尽可能的核心空闲时间。

    更少的缓存未命中,更少的超越指令将导致更少的空闲时间。

    v_out 上的写-读-写-读-写序列(这可能非常 如果这些写入需要更新到主内存,成本会很高;这 这里的动机是只阅读一个向量可能很诱人, 更新输出并重复)。

    您可以使用预取 (_mm_prefetch) 降低序列的价格。

    【讨论】:

    • 但是如果 v_1[i] 和 v_2[i] 驻留在同一个缓存行中怎么办?这是否意味着每当我阅读 v_1[i] 时,我就会失去 v_2[i](和 v_2[i+1]),而每当我阅读 v_2[i] 时,我就会失去 v_1[i](和 v_1[i+1 ])?
    • 查看我的编辑,了解我想避免的事情。从缓存中清除输出(和中间输出)数据。预取真的相关吗?关键是我根本不希望它进入主内存,只是留在缓存中代替可能替换它的其他东西,这样我就可以继续处理更多的 bar 或 i+1 输出。
    • 最后,不幸的是,数据重新排列不是一种选择 - 请参阅我的笔记。
    • @einpoklum v_1[i] 和 v_2[i] 不能保留在同一缓存行上,除非您按照我的建议重新排列数据。 W.r.t 预取,可用于避免中间输出的缓存行驱逐。这样做,您就可以让 CPU 知道不能清除该数据。
    • 您是否使用 perf 或任何其他分析器分析您的程序以查看您有多少缓存未命中?
    【解决方案2】:

    为了减少缓存数据的清除,您可以将 k 向量重新排列为 1 向量,其中包含具有 k 成员的结构。这样,循环将按顺序访问这些元素,而不是在内存中跳转。

    struct VectorData
    {
        Type1 Var1;
        Type2 Var2;
        // ...
        TypeK VarK;
    };
    
    std::vector<VectorData> v_in;
    
    for (i = 0; i < n; i++){
        v_out[i] = foo(v_in[i].Var1, v_in[i].Var2, ... , v_in[i].VarK);
        // Or just pass the whole element:
        v_out[i] = foo(v_in[i]);
    }
    

    【讨论】:

    • 这意味着使用 kn 额外空间并花时间读取和写入 kn 元素 - 这一切都计入我的计算(刚刚编辑以强调这一点) .你确定这有用吗?
    • @einpoklum 我不知道您不能免费重新排列输入数据。您应该分析您的代码以了解它是否有帮助。但你是对的,在那种情况下可能会更糟。
    • 鉴于 SIMD 在 SOA 上工作,转换为 AOS可能是一个糟糕的主意。
    猜你喜欢
    • 1970-01-01
    • 2012-07-31
    • 1970-01-01
    • 1970-01-01
    • 2013-07-08
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多