【问题标题】:Visual Studio 2017: _mm_load_ps often compiled to movupsVisual Studio 2017:_mm_load_ps 经常编译为 movups
【发布时间】:2017-07-30 13:01:50
【问题描述】:

我正在查看为我的代码生成的程序集(使用 Visual Studio 2017),并注意到 _mm_load_ps 通常(总是?)编译为 movups。

我使用 _mm_load_ps 的数据是这样定义的:

struct alignas(16) Vector {
    float v[4];
}

// often embedded in other structs like this
struct AABB {
    Vector min;
    Vector max;
    bool intersection(/* parameters */) const;
}

现在当我使用这个构造时,会发生以下情况:

// this code
__mm128 bb_min = _mm_load_ps(min.v);

// generates this
movups  xmm4, XMMWORD PTR [r8]

由于 alignas(16),我期待 movaps。在这种情况下,我是否需要其他东西来说服编译器使用 movaps?

编辑:我的问题与this question 不同,因为我没有遇到任何崩溃。该结构是专门对齐的,我也使用对齐分配。相反,我很好奇为什么编译器将 _mm_load_ps (对齐内存的内在属性)切换到 movups。如果我知道 struct 分配在一个对齐的地址并且我通过 this* 调用它,那么使用 movaps 是安全的,对吧?

【问题讨论】:

  • 您特别想要movaps 的目的是什么?
  • @J... 是的 Core2。据我所知,只要地址实际上是对齐的,对任何更新的东西都没有关系
  • 另外,请通读Remarks section here。 (这指的是__declspec(align(#)),但由于VS2015 alignas 支持被实现为相同的单板)。
  • @J... 是的,_mm_load_ps 也可以这样做,尽管它不是必须的
  • 在 VS 和 ICC 上,如果您为 AVX 或更高版本进行编译,编译器几乎不会发出对齐的 SIMD 加载/存储问题。这样做是允许的,因为它不会丢失功能,并且从 Nehalem 开始的所有处理器在地址对齐时都不会因使用未对齐的加载/存储而受到惩罚。他们这样做是因为它使编译器更简单(不必在对齐/未对齐之间进行选择)并且如果它未对齐也不会崩溃。虽然我强烈不同意后者,因为我更喜欢它实际上在未对齐时崩溃,因为这是一个应该修复而不是隐藏的错误。

标签: c++ assembly sse intrinsics visual-studio-2017


【解决方案1】:

在最新版本的 Visual Studio 和英特尔编译器(最近是 2013 年之后?)上,编译器很少再生成对齐的 SIMD 加载/存储。

为 AVX 或更高版本编译时:

  • Microsoft 编译器 (>VS2013?) 不会生成对齐的负载。但它仍然会生成对齐的商店。
  • Intel 编译器 (> Parallel Studio 2012?) 根本不再这样做了。但您仍会在其手动优化库(如 memset())内的 ICC 编译二进制文件中看到它们。
  • 从 GCC 6.1 开始,当您使用对齐的内在函数时,它仍会生成对齐的加载/存储。

允许编译器执行此操作,因为正确编写代码时不会丢失功能。当地址对齐时,从 Nehalem 开始的所有处理器都不会因未对齐的加载/存储而受到惩罚。

微软在这个问题上的立场是它“通过不崩溃来帮助程序员”。不幸的是,我再也找不到来自 Microsoft 的该声明的原始来源了。在我看来,这完全相反,因为它隐藏了错位惩罚。从正确性的角度来看,它也隐藏了错误的代码。

无论如何,无条件地使用未对齐的加载/存储确实会稍微简化编译器。

新关联:

  • 从 Parallel Studio 2018 开始,英特尔编译器不再生成对齐的移动 - 即使是 Nehalem 之前的目标也是如此。
  • 从 Visual Studio 2017 开始,Microsoft 编译器也不再生成对齐的移动 - 即使针对 AVX 之前的硬件也是如此。

这两种情况都会导致旧处理器的性能不可避免地下降。但似乎this is intentional 作为英特尔和微软都不再关心旧处理器了。


唯一不受此影响的加载/存储内在函数是非临时加载/存储。它们没有未对齐的等价物,因此编译器别无选择。

因此,如果您只想测试代码的正确性,可以在加载/存储内在函数中替换非临时代码。但请注意不要让这样的事情溜进生产代码,因为 NT 加载/存储(尤其是 NT 存储)是一把双刃剑,如果您不知道自己在做什么,可能会伤害到您。

【讨论】:

  • 相关:gcc 在自动矢量化时也非常喜欢对齐,并且在对齐边界之前一直是标量(使用完全展开的介绍/清理代码,这是 AVX2 和小元素的大量代码膨胀)。即使使用-mtune=skylake 或其他东西,它也会这样做。无论如何,确保 gcc 知道您可以提供的任何对齐保证将减少代码膨胀并在自动矢量化时避免一两个条件分支。
  • 写回内存上的 NT 负载运行与正常负载完全相同,至少在 Intel Sandybridge 系列上是这样。他们本可以让它像 prefetchNTA 一样工作,但没有(可能是因为它需要能够识别 NT 的硬件预取器才能让它不烂)。 (正在对stackoverflow.com/questions/32103968/… 进行更新;结果我的猜测是错误的,因为它做了一些事情,比如只获取一种缓存方式以避免污染。只有 pfNTA 这样做。)
  • @PeterCordes 有趣的是,Skylake X 上的 NT 负载吞吐量仅为 1 个/周期,而所有其他负载为 2 个/周期。 (according to AIDA64)
  • 在 Skylake-S(桌面)上,使用 movntdqa xmm0, [rsi] / movntdqa xmm1, [rsi+16] 等重新加载相同的 64 字节。它每时钟运行约 1.71 个字节,而 movdqa 每时钟运行 2.0 个字节。因此,即使对于最微不足道的情况,它也较慢。感谢您指出这一点。
  • 这些 AIDA64 数字表明 AVX512 EVEX vmovntdqa(每 1.08 1 个)不同于常规 SSE 或 AVX VEX movntdqa(每 0.52 1 个)。而且那个 EVEX VMOVNTDQA + VMOVNTDQ x/y/zmm reload/store 仍然有可怕的延迟,但吞吐量是 1 per ~19.25c,而不是与延迟相同。 (并且 ZMM NT 存储/重新加载延迟低于其他两种大小,这是另一个暗示全高速缓存行 NT 存储是特殊的。单线程带宽比更窄的 NT 存储高得多已经是一个很大的暗示。)
猜你喜欢
  • 1970-01-01
  • 2018-12-08
  • 2017-08-28
  • 1970-01-01
  • 1970-01-01
  • 2018-05-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多