CPU如何读取双精度值?

时间:2017-08-26 00:52:37

标签: assembly x86

article说:

  

请注意,双变量将在32字节边界上分配   位机,需要两个存储器读周期。

所以这意味着x86 CPU有一些读取double值的指令(我认为x86 CPU可以读取的最大值是4个字节!)。任何人都可以提供读取双精度值的指令示例吗?

1 个答案:

答案 0 :(得分:3)

  

任何人都可以提供读取双精度值的指令示例吗?

MOVSD xmm0, [rsi]加载8个字节,并将xmm0的上半部分加零。对于旧版x87,有fld qword [rsi]。当然,您可以使用内存操作数来执行ALU指令,例如addsd xmm0, [rsi]。或者使用AVX,有vbroadcastsd ymm0, [rsi]vaddsd xmm1, xmm0, [rsi]之类的内容。

所有这些都解码了所有现代x86 CPU上的单个uop,并且只能对缓存进行一次访问。

  

我认为x86 CPU可以读取的最大值是4个字节!

是吗?自8086以来,支持8字节x87加载。在64位模式下,mov rax, [rdi]pop rax都是8字节加载。

使用AVX,您可以vmovups ymm0, [rsi + rdx](即使在32位模式下)执行32- 字节加载。或者使用AVX512,vmovups zmm0进行64字节加载或存储。

现代CPU上的高速缓存行是64字节,因此内存(逻辑上)以CPU内部和核心之间的64字节块进行复制。 Intel CPU在内核之间使用32字节总线(在通往内存的路上使用L2和L3之间)。

有关手册和(准确)文档/指南/文章的许多良好链接,请参阅 tag wiki。如果TutorialsPoint或GeeksForGeeks等网站上的某些内容看起来令人困惑,或者与您在其他地方阅读的内容不匹配,那么它很有可能是错误的。如果没有SO这样的投票机制,那么不准确的内容就不会被淘汰。

让我们直接记录http://www.geeksforgeeks.org/structure-member-alignment-padding-and-data-packing/。它充满了错误的信息,它所说的关于硬件的大部分内容可能在1995年大致正确,但现在还没有。很多逻辑/推理都是错误的。

它甚至不包含单词" cache"!谈论访问RAM和"银行"对现代x86 CPU来说完全是胡说八道。同样的概念适用于分区缓存,因此对于某些CPU来说,16B或32B等对齐边界很重要,即使所有x86 CPU上的缓存行都是64B(自Pentium III以来)。

现代x86通常具有非常好的非对齐访问支持,特别是对于8B和更窄,但在AMD CPU(包括Ryzen)上跨越16B或especially 32B boundary可能会受到惩罚。

  

在最新的处理器上,我们得到struct_c的大小为16个字节。 [...]

     

在使用相同工具集(GCC 4.7)的旧处理器(AMD Athlon X2)上,我将struct_c大小设置为24字节。大小取决于内存银行在硬件级别的组织方式。

这显然是无稽之谈。 struct布局必须与针对同一ABI的所有编译器相同,无论使用什么-march=pentium3-mtune=znver1设置,或者您编译的硬件,都可以链接到从代码传递(指针)struct类型到库函数的库,反之亦然。一个明显的例子是stat(const char *pathname, struct stat *statbuf) system call,您传递指针并且内核在结构中写入字段。如果您的代码与内核不同意内存中哪些字节代表哪些C struct成员,那么您的代码将无法正常工作。指定布局/对齐规则(以及调用约定)是ABI的主要部分。

很可能是"更新的"测试针对的是32位i386 System V psABI,而#34;更老的" test正在为x86-64 System V psABI(或Windows 32或64位,它们都有24字节structc和MSVC CL19)编译64位代码。

typedef struct structc_tag {
   char        c;
   double      d;
   int         s;
} structc_t;
int sc = sizeof(structc_t);

#include  <stddef.h>
int alignof_double = alignof(double);
int c_offset_d = offsetof(structc_t, d);

Compiler output for clang -m32 (Godbolt compiler explorer)

alignof_double:
    .long   8
c_offset_d:
    .long   4

所以 32位ABI会在结构中错位double,即使它更喜欢将double与其他地方的8个字节对齐,但是64位ABI赢了& #39;吨。 i386 System V ABI可以追溯到很久以前,可能是实际的386或486个CPU,可能确实需要两个内存读取周期来加载double。仅考虑高达4B的对齐边界的打包规则对旧CPU或32位模式下的整数有意义。新设计的32位ABI可能需要double对齐,也可能int64_t(用于MMX / SSE2)。但是,打破ABI兼容性以在struct内部对齐64位类型将是不值得的。

有关ABI文档,请参阅标记wiki。

请注意std::atomic<double>即使在-m32中也会获得完整的8B对齐。

  

请注意,双变量将在32位机器上的8字节边界上分配,并且需要两个内存读取周期。

对64位对齐地址的qword加载或存储(例如fldfstp)保证是原子的(自P5奔腾以来),因此它绝对是单一访问到L1D缓存(或用于未缓存访问的RAM)。请参阅Why is integer assignment on a naturally aligned variable atomic?

此保证一般适用于x86(包括AMD和其他供应商)。  实际上,gcc -m32使用SSE2 std::atomic<int64_t>或x87 movq加载/存储来实现fild

更宽和/或未对齐的加载/存储不是保证是单一访问,但是在某些CPU上。例如对于未跨越64B缓存线边界的未对齐数据,Intel Haswell / Skylake可以在每个周期执行两次32B未对齐矢量加载,每次加载作为L1D缓存的单次读取。如果它确实越过了缓存行边界(如vmovups ymm0, [rdi+33],其中rdi是64B对齐的),则吞吐量限制为每个周期一个,因为每个加载必须从两个缓存行读取和合并数据。

对未对齐负载的硬件支持非常好,因此只需要额外的负载使用延迟。然而,4k-splits更昂贵,特别是在Skylake之前。

  

值得注意的是,大多数处理器都有数学协处理器,称为浮点单元(FPU)。代码中的任何浮点运算都将转换为FPU指令。 主处理器与浮点执行无关。

这(以及之后的挥手)完​​全是虚假的。自Pentium以来,FPU已集成到主CPU核心中。 FP加载和整数加载在大多数CPU中使用相同的执行端口。

一个例外是AMD Bulldozer系列,其中一对整数核心共享一个FP /向量单元。但它们的耦合非常紧密,FP负载仍然使用相同的dTLB和L1D缓存。

根据David Kanter's Bulldozer writeup,有一个小的浮点加载缓冲区(上面未显示),它作为加载存储单元和FP簇之间负载的模拟管道。

即使Bulldozer仍然在整数和FP /向量uop之间共享一个无序ReOrder缓冲区(ROB),并且整数/ FP指令必须按程序顺序退出(一如既往地支持精确异常)。其他AMD设计也有单独的调度程序,但这是一件小事。

Intel CPU对整数和FP使用单个统一的无序调度程序,执行端口具有整数和FP ALU的混合。例如,Haswell port 0 can run integer shift and simple ALU uops, and it also has a vector multiply / FMA unit.

与PowerPC不同,从FP存储到整数负载的存储转发工作正常。 (在PPC上,Load-Hit-Store档位显然是一个问题。在x86上,它只比普通的存储转发工作没有太多问题。在Bulldozer上它很慢,ALU {{1}也是如此},因为协调2个核心与一个FPU通信。)

  

按照标准,double类型将占用8个字节。并且,在FPU中执行的每个浮点运算都将是64位长度。在执行之前,偶数movd r32, xmm类型将被提升为64位。

也是假的。对于x87,内部寄存器为80位(64位尾数!)。除非将x87精度控制寄存器设置为53位尾数或24位尾数,否则此描述适用于x87。 (参见Bruce Dawson的优秀系列浮点文章。this one about Intermediate Float Precision提到在Windows上,D3D9库将x87 FPU设置为24位精度,因此除以sqrt会更快一些,并且旧版本的MSVCRT将其设置为53位float!)

但是,由于本文讨论的是64位计算机,忽略x86-64 Windows和Linux在double寄存器中传递/返回FP args的事实是错误的,而且#&# 39;假设FP数学将使用SSE / SSE2标量或向量指令完成,而不是x87。像xmm这样的SSE2指令在xmm寄存器中生成IEEE二进制64结果,因此它们在每一步之后舍入到53位尾数精度。 (如果你想要更快的分割,你可以使用mulsd而不是divps。SSE没有精确控制寄存器;你只需要使用不同的指令。)

divpd传递给float等可变参数函数会根据C的默认促销规则将其提升为printf,但double不会将其float a = f1 * f2;推广到float。必须提升加倍,然后将结果舍入为double

  

64位长度的FPU寄存器强制在8字节边界上分配double类型。 [...]

     

因此,对于双类型(预期在8字节边界上),地址解码将是不同的。这意味着,浮点单元的地址解码电路不会有最后3个引脚。

总废话。 x87(fld)和SSE2(movsd)支持未对齐的qword fld次加载/存储,自{80}起0xabc001

  

少数处理器不会有最后两个地址线,这意味着无法访问奇数字节边界。

以这种方式设计的CPU可以通过总线执行32位加载并提取所需的字节。这种说法是为什么它如此愚蠢,文章没有提到缓存。

有趣的事实是:旧版本的ARM使用地址的低2位作为字节旋转。因此,从0xabc000加载可以获得{{1}}处应用了旋转的4个字节。我听说调试很有趣,而硬件只是在未对齐的负载上出现问题:P

早期的Alpha CPU确实没有字节加载支持,所以你总是需要进行32或64位加载和掩码和/或移位以获得你想要的字节。

我可以更多地谈论本文所暗示的错误......

我确定如果我仔细阅读,我会看到更多问题,但这是我从略读中得到的问题。这篇文章的作者在20年前阅读了一些关于硬件的内容,并在此基础上提出了一些错误的想法。