【问题标题】:SSE instruction MOVSD (extended: floating point scalar & vector operations on x86, x86-64)SSE 指令 MOVSD(扩展:x86、x86-64 上的浮点标量和向量运算)
【发布时间】:2016-03-16 22:52:52
【问题描述】:

我对 MOVSD 汇编指令感到困惑。我写了一些数字代码来计算一些矩阵乘法,只是使用没有 SSE 内在函数的普通 C 代码。我什至不包括用于编译的 SSE2 内在函数的头文件。但是当我检查汇编输出时,我看到了:

1) 使用128位向量寄存器XMM; 2) SSE2指令MOVSD被调用。

我知道 MOVSD 本质上是在单双精度浮点上运行的。它只使用XMM寄存器的低64位,将高64位设置为0。但我只是不明白两件事:

1) 我从来没有给编译器任何使用 SSE2 的提示。另外,我使用的是 GCC 而不是英特尔编译器。据我所知,intel 编译器会自动寻找向量化的机会,但 GCC 不会。那么 GCC 是如何知道使用 MOVSD 的呢?或者,这条 x86 指令是否早在 SSE 指令集之前就已经存在,而 SSE2 中的 _mm_load_sd() 内部函数只是为了提供向后兼容,以便使用 XMM 寄存器进行标量计算?

2) 为什么编译器不使用其他浮点寄存器,无论是80位浮点堆栈,还是64位浮点寄存器?为什么必须使用 XMM 寄存器(通过设置高 64 位 0 并实质上浪费该存储空间)来付出代价? XMM 是否提供更快的访问?


顺便说一句,我还有一个关于 SSE2 的问题。我只是看不出 _mm_store_sd() 和 _mm_storel_sd() 之间的区别。两者都将低 64 位值存储到一个地址。有什么区别?性能差异??对齐差异??

谢谢。


更新 1:

好的,很明显,当我第一次问这个问题时,我缺乏一些关于 CPU 如何管理浮点运算的基本知识。所以专家们倾向于认为我的问题是无意义的。由于我什至没有包含最短的示例 C 代码,因此人们可能也会认为这个问题含糊不清。在这里,我将提供a review 作为答案,希望对任何不清楚现代 CPU 上的浮点运算的人有用。

【问题讨论】:

  • 在 64 位模式下,调用约定已经要求 SSE 寄存器用于浮点参数和返回值。由于 SSE 寄存器不是以堆栈形式组织的,而且它们的数量更多,因此编译器更容易使用。有标量 SSE 指令。此外,有趣的是,您担心会浪费其中一半的空间 - 如果您根本不使用它们,所有它们都会浪费;)
  • 没有 64 位浮动指针寄存器。只有 80 位浮点堆栈和 XMM 寄存器。 MOVSD 指令是标量指令,而不是向量指令,因此它的使用并不意味着自动向量化。
  • 那些不是浮点寄存器。
  • 好的,_mm_storel_pd 生成 MOVLPD 指令,而 _mm_store_sd 生成 MOVSD。在写入内存时,它们具有相同的功能。至少我看不出区别。
  • @Jester:AFAICT,movlpdmovsd 都存在的唯一原因是编码的一致性/规律性:movlpsmovss 不同,所以这两个指令都已经存在。 SSE2 对 double 使用具有不同转义字节的相同操作码,而不是 single 版本。 (movlpd 与每个 CPU 上的movlps 相比,已经只是浪费了指令字节,因为 AFAIK 没有 CPU 关心任何数据移动 insns 的单版本和双版本。英特尔一直通过始终使单与双版本相比,直到vextractf128 和其他东西)

标签: c assembly x86-64 sse sse2


【解决方案1】:

现代 CPU 上的浮点标量/向量处理综述

矢量处理的想法可以追溯到旧时代vector processors,但这些处理器已被具有缓存系统的现代架构所取代。所以我们专注于现代 CPU,尤其是x86x86-64。这些架构是main stream in high performance scientific computing

自 i386 以来,英特尔引入了浮点堆栈,其中可以保存高达 80 位宽的浮点数。这个栈俗称x87 or 387 floating point "registers",有一组x87 FPU instructions。 x87 堆栈不是真实的、可直接寻址的寄存器,如通用寄存器,因为它们位于堆栈上。访问寄存器 st(i) 是通过偏移栈顶寄存器 %st(0) 或简单的 %st。借助指令 FXCH 在当前堆栈顶部 %st 和某个偏移寄存器 %st(i) 之间交换内容,可以实现随机访问。但是 FXCH 可以施加一些性能损失,尽管可以最小化。 x87 堆栈通过默认以 80 位精度计算中间结果来提供高精度计算,以最大限度地减少数值不稳定算法中的舍入误差。但是,x87 指令是完全标量的。

向量化的第一个尝试是MMX instruction set,它实现了整数向量运算。 MMX下的向量寄存器是64位宽的寄存器MMX0、MMX1、...、MMX7。每个都可以用来保存 64 位整数,或者以“打包”格式保存多个较小的整数。然后可以一次将一条指令应用于两个 32 位整数、四个 16 位整数或八个 8 位整数。因此,现在有用于标量整数运算的传统通用寄存器,以及用于不共享执行资源的整数向量运算的新 MMX。但是 MMX 与标量 x87 FPU 操作共享执行资源:每个 MMX 寄存器对应一个 x87 寄存器的低 64 位,而 x87 寄存器的高 16 位未使用。这些 MMX 寄存器每个都是可直接寻址的。但是混叠使得在同一个应用程序中处理浮点和整数向量操作变得很困难。为了最大限度地提高性能,程序员通常只在一种模式或另一种模式下使用处理器,尽可能长时间地推迟它们之间相对较慢的切换。

后来,SSE 在 x87 堆栈的一侧创建了一组单独的 128 位宽寄存器 XMM0–XMM7。 SSE 指令专门专注于单精度浮点运算(32 位);整数向量运算仍然使用 MMX 寄存器和 MMX 指令集执行。但是现在这两个操作可以同时进行,因为它们不共享执行资源。 重要的是要知道,SSE 不仅可以进行浮点向量运算,还可以进行浮点标量运算。从本质上讲,它提供了一个进行浮动操作的新场所,并且 x87 堆栈不再是执行浮动操作的首选。使用 XMM 寄存器进行标量浮点运算比使用 x87 堆栈更快,因为所有 XMM 寄存器都更容易访问,而没有 FXCH 就无法随机访问 x87 堆栈。 当我发布我的问题时,我很清楚不知道这个事实。我不清楚的另一个概念是通用寄存器是整数/地址寄存器。即使它们在 x86-64 上是 64 位的,它们也无法容纳 64 位浮点。主要原因是与通用寄存器相关的执行单元是ALU(算术逻辑单元),不用于浮点计算。

SSE2 是一个重大进步,因为它扩展了向量数据类型,因此 SSE2 指令,无论是标量还是向量,都可以处理所有 C 标准数据类型。这种扩展实际上使 MMX 过时了。此外,x87 堆栈不再像以前那样重要。由于有两个可以进行浮点运算的替代位置,您可以为编译器指定您的选项。例如对于 GCC,使用 flag 编译

-mfpmath=387

将在旧版 x87 堆栈上安排浮点运算。 请注意,这似乎是 32 位 x86 的默认设置,即使 SSE 已经可用。 例如,我有一台 2007 年制造的 Intel Core2Duo 笔记本电脑,它已经配备了 SSE 版本到 SSE4 版本,而 GCC 在默认情况下仍将使用 x87 堆栈,这会使科学计算不必要地变慢。在这种情况下,我们需要使用标志编译

-mfpmath=sse

GCC 将在 XMM 寄存器上安排浮点运算。 64 位 x86-64 用户无需担心此类配置,因为这是 x86-64 上的默认配置。这样的信号只会影响标量浮点运算。如果我们使用向量指令编写代码并编译带有标志的代码

-msse2

那么 XMM 寄存器将是唯一可以进行计算的地方。换句话说,此标志打开 -mfpmath=sse。有关详细信息,请参阅GCC's configuration of x86, x86-64。有关编写 SSE2 C 代码的示例,请参阅我的另一篇帖子 How to ask GCC to completely unroll this loop (i.e., peel this loop)?

SSE 指令集虽然非常有用,但并不是最新的向量扩展。 AVX advanced vector extensions 通过提供 3 操作数和 4 操作数指令来增强 SSE。如果您不清楚这意味着什么,请参阅number of operands in instruction set。 3 操作数指令优化了科学计算中常见的fused multiply-add (FMA) 操作:1) 使用更少的寄存器; 2) 减少寄存器之间的显式数据移动量; 3) 本身加速 FMA 计算。使用 AVX 的示例,请参阅@Nominal Animal's answer to my post

【讨论】:

  • 针对 32 位 x86 的 gcc 默认生成可以在任何 x86 CPU 上运行的代码,最早可以追溯到 i686 (PPro)。如果您希望它针对当前主机的指令集扩展,请使用-march=native。此外,32 位 ABI 被定义为在 x87 堆栈上返回 FP args。有关 ABI 文档,请参阅 x86 tag wiki info page
  • 大多数 3 操作数 AVX 指令只有一个单独的目标,它不是源之一。 FMA 是一个主要的例外:它仍然会破坏它的一个操作数,就像 SSE 指令一样。 (英特尔在最后一刻改变了主意,从 FMA4 改为 FMA3,但没有告诉 AMD,因此 AMD Bulldozer 支持 4 操作数 FMA4 但不支持英特尔风格的 FMA3)。此外,SSE 有一些 3 操作数指令:pblendvb 使用 xmm0 作为隐式的第 4 操作数。所以英特尔本可以在没有 AVX 的情况下实现 FMA,而 FMA 是 AVX 如何减少对mov 指令的需求的一个坏例子。
  • 除此之外,从我快速浏览的大部分内容来看,这个答案似乎是正确的。 IDK 它对其他初学者有多大用处,因为它看起来有点长而且杂乱无章。 IIRC,很多x86 insn set extensions的历史已经在SSE tag wiki page,几个月前我整理了。
  • 我也没有发现tag-wiki 页面经常有很好的链接。这不是网站本身向人们指出的一个很好宣传的功能,所以我通常在我评论或回答的几乎每个新手问题上都链接到 x86 标签 wiki。回复:load1_pd:使用 SSE3,broadcast-load only takes a single instruction (movddup),与 Nehalem 及更高版本的常规负载一样便宜。在Core2上,那个insn也使用ALU,不能微熔。请参阅 Godbolt 上的代码:没有 SSE3,它是单独的加载和随机播放,因此即使在 Skylake 上也会更慢。
  • 是的,不要使用复合内在函数,除非没有办法用一条指令来做你需要的事情。只需使用_mm_load1_pd。正如我的godbolt 链接所证明的,它使用-msse3 编译为理想代码(movddup)。查看 asm 输出中的重要循环是个好主意,以确保编译器不会弄得一团糟。
猜你喜欢
  • 1970-01-01
  • 2013-05-15
  • 2014-05-07
  • 1970-01-01
  • 2012-10-04
  • 1970-01-01
  • 1970-01-01
  • 2019-02-19
  • 1970-01-01
相关资源
最近更新 更多