【问题标题】:how to proper read data vertically from horizontal array?如何正确从水平数组垂直读取数据?
【发布时间】:2021-11-04 15:05:15
【问题描述】:

这是我从 SDK 获得的基础设施声明:

struct alignas(32) Input {
    union {
        float values[16] = {};
        float value;
    };
    
    // other members variables
}

std::vector<Input> myInputs;

const int numInputsA = 4;
const int numInputsB = 4;
const int numInputsC = 4;
const int numInputsD = 4;
const int numInputsE = 4;
myInputs.resize(numInputsA + numInputsB + numInputsC + numInputsD + numInputsE);

使用 simd 更快地加载记录的最佳方法是什么,例如:

__m128 targetA0 = { myInputs[0].values[0], myInputs[1].values[0], myInputs[2].values[0], myInputs[3].values[0] }
__m128 targetB0 = { myInputs[4 + 0].values[0], myInputs[4 + 1].values[0], myInputs[4 + 2].values[0], myInputs[4 + 3].values[0] }
__m128 targetC0 = { myInputs[8 + 0].values[0], myInputs[8 + 1].values[0], myInputs[8 + 2].values[0], myInputs[8 + 3].values[0] }
...
__m128 targetA1 = { myInputs[0].values[1], myInputs[1].values[1], myInputs[2].values[1], myInputs[3].values[1] }
__m128 targetB1 = { myInputs[4 + 0].values[1], myInputs[4 + 1].values[1], myInputs[4 + 2].values[1], myInputs[4 + 3].values[1] }
__m128 targetC1 = { myInputs[8 + 0].values[1], myInputs[8 + 1].values[1], myInputs[8 + 2].values[1], myInputs[8 + 3].values[1] }
...
... and so on

如您所见,我继承的结构并不是真正面向以这种方式捕获数据,但无法更改它。

所以这个问题,感谢您的经验:是否可以加载数据以在每个起始索引上注册“偏移量”?还是缓存线需要加载整个块,涉及大量缓存未命中?

也许有一些技巧可以加快整个过程。 至于我之前的帖子,仍然在 windows/64 位机器上,使用 FLAGS += -O3 -march=nocona -funsafe-math-optimizations(由我正在发展的生态系统强加)。

感谢您提供给我的任何帮助/提示/建议。

【问题讨论】:

  • 我想我在上一个问题中提到了这一点,但您可能想要-march=nocona -mtune=generic,除非您实际上更关心 P4 上的性能而不是典型的现代 CPU。它仍会在那些旧 P4 上运行,但调整选项(例如何时内联以及使用哪些指令)将基于主流 AMD 和 Intel CPU 的优势。
  • x86 没有跨步加载,但如果您可以使用 4x8 或 8x8 转置,则进行矢量加载和随机转置可能是值得的,尽管只有 16 个 XMM regs 持有每个 4 个花车,你不能容纳 12x 16 个花车。
  • @PeterCordes 是的,过去你对-mtune=generic 的建议已经做过了,但我没有得到任何显着的收益(不到 1%)
  • @PeterCordes 实际上我可以为每个输入水平“加载”values(这是 16xFloat,因此是 64 字节,可以一次加载),然后转置每个相关索引垂直。这种“换位”的最佳方式是什么?有什么例子吗?
  • 你完全没有抓住重点。手动预取无关紧要,重要的是执行实际预取需要多少条指令。如果缓存中的数据一开始是冷的,内存流水线和软件预取将在负载尝试执行时将其引入。如果发生任何缓存未命中,调整您的代码以提高吞吐量,最小化 uops 的数量,将使 OoO exec 能够更好地重叠之前和之后的代码。

标签: c++ arrays vectorization simd sse


【解决方案1】:

唯一的边际改进可能是将 alignas 更改为 64,因为您有 64 个字节,希望使其对齐到单个缓存行中。

如今,64 字节恰好是高速缓存行的大小。因此,假设您需要从 RAM 中获取数据,那么您的 simd 设置几乎无关紧要。昂贵的部分是将数据获取到 L1 缓存,其余的操作将是噪音。甚至,如果您因为对齐而需要两条缓存线,我希望增加的幅度非常小。请记住,今天的处理器不会按顺序执行操作。很可能所有这些任务都在某种程度上并行运行,因此实际顺序并不重要。

我建议获取一个相当简单的代码版本(两个循环)并查看生成的汇编代码。您正在使用 O3 运行,因此即使是幼稚的代码也可能会得到相当好的(如果不是更好的话)优化。如果您认真考虑优化这一点,您应该设置一个基准来验证您正在做的事情实际上是在加快速度。我希望简单的版本足够快(如果你得到更好的结果,请发布)。

您还应该分析整个应用程序。您可能会发现其他更容易优化并为您带来更多好处的代码。

你能更快吗?可能,但是您开始为代码添加显着的复杂性和限制。我可以想象这样一种情况,您的代码在您的工作站上运行速度很快,但在其他 CPU 上只是平均水平。此外,您还会使一些重要的代码复杂化。这对你来说值得吗?

【讨论】:

  • 代价高昂的部分是将数据获取到 L1 缓存,其余的操作将是噪音。 - 取决于数据的存储时间。缓存(通常)有效,这就是我们拥有它们的原因。与 L2 甚至 L3 命中相比,SIMD shuffle 可能会为 16 x N 转置节省大量成本,可能使用更小的构建块,并且这些指令将停留在 RS 中,直到加载数据到达为止。所以更少的 uops 意味着更好的 OoO exec 更好地隐藏缓存未命中延迟; OoO exec 有价值的主要原因之一。
  • 如果您的数据在 DRAM 中通常是冷的,那么您已经丢失了,所以是的,首先通过缓存阻止您的算法来解决这个问题,然后其他优化变得更加相关。
  • @PeterCordes 缓存确实有效,但很难保证所有内容都在缓存中。根据设置,即使是一些缓存未命中也会影响速度,但如果没有基准,很难争论多少。该问题明确指出它正在使用-O3 进行编译。因此,我们将优化代码与手动优化代码进行比较,因此我不相信节省的成本会很大。如果我们在谈论 SIMD 与-O0,那么您是绝对正确的。
  • 带有-O0 的手动内在函数通常很糟糕,因为您最终会使用更多单独的语句,因此需要更多的存储/重新加载。此外,被定义为包装函数的内在函数意味着额外的存储/重新加载仅用于 arg 传递,即使它们内联(使用-O0)。您的论点将适用于-O1gcc -O2(GCC 仅在-O3 包括-ftree-vectorize,与clang 不同)。无论如何,是的,检查编译器输出以查看它是否自动矢量化。如果没有,那么可能会有显着的收益。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-22
  • 1970-01-01
  • 2022-11-16
  • 2014-11-10
  • 2014-04-19
相关资源
最近更新 更多