【问题标题】:Why pushing double on the stack with two 32s bits pushes can be a lot slower than pushing it using float instructions (fldl & fstpl)?为什么使用两个 32s 位推送在堆栈上双倍推送可能比使用浮点指令(fldl 和 fstpl)推送它慢很多?
【发布时间】:2015-12-03 09:48:16
【问题描述】:

这是一小段汇编代码(我使用了 gnu 汇编器的语法)。

.extern cos
.section .data
pi: .double 3.14
.section .text
.global slowcos
.global fastcos

fastcos:
  fldl pi         
  subl $8, %esp   # makes some space for a double on the stack
  fstpl 0(%esp)   # copy pi on top of the stack
  call cos
  addl $8, %esp
  ret

slowcos:
  pushl pi+4      # push the last 4 bytes of pi on top of the stack
  pushl pi        # push the first 4 bytes of pi on top of the stack
  call cos
  addl $8, %esp
  retx

可以使用以下原型从 C 中轻松调用这些符号:

extern double fastcos ();
extern double slowcos ();

它们都返回“cos(3.14)”的值,但在英特尔 32 位架构上,slowcos 比 fastcos 慢两倍。我的问题如下:

什么可以解释如此大的性能差异?

在 linux 上,您可以通过将此代码复制到文件调用 cos.asm 中并调用以下方法来测试:

as --32 cos.asm -o cos.o 
gcc -m32 -O0 cos.o test.c -lm -o test

(如果您不在 64 位系统上,您可以删除 --32/-m32(应该吗?)) 其中 test.c 是以下 C 源文件:

#include <stdio.h>
#include <time.h>

#define N 40000000

extern double fastcos ();
extern double slowcos ();

int main() {
  int k;
  double r; 
  clock_t t;

  t = clock();
  for (k = 0; k < N;k ++) 
    r = fastcos();
  printf ("%gs\n",(double) (clock() - t) / CLOCKS_PER_SEC);
  printf("fastcos = %g\n", r);

  t = clock();
  for (k = 0; k < N;k ++)
    r = slowcos();
  printf ("%gs\n",(double) (clock() - t) / CLOCKS_PER_SEC);
  printf("slowcos = %g\n", r);

  return 0;
}

在我的电脑上输出:

1.55687s
fastcos = -0.999999
2.29821s
slowcos = -0.999999

再说一句。如果在标题中添加“.global id”行,请将 fastcos 和 slowcos 中的“call cos”行替换为“call id”,并在C 文件。然后,您获得:

0.360433s
fastpi = 3.14
0.370393s
slowpi = 3.14

这段代码应该在函数 cos(或 id)的内部调用之外花费大约相同的时间。所以这应该表明差异发生在 cosinus 函数的执行过程中。但我不明白什么可以证明这种差异是合理的。 %esp的对齐方式没有区别。

最后,我想说的是,我在现实生活中的“数值”代码中观察到了这些差异,其中瓶颈通常是“基本数学函数”(如 cos 或 exp)的计算。此外,这两个版本都是由高级编程语言的编译器生成的。我主要关心的是了解那里发生了什么。

【问题讨论】:

  • 加载存储转发。
  • 嗯,很确定我的编译器会将 cos() 调用提升到循环之外,然后完全消除循环。所以你可能没有运行优化的构建。你怎么知道导致减速的不是 for() 循环?例如,分支目标不会对齐。尝试将调用交换为 slow/fastcos()。
  • @HansPassant:编译器不解析程序集,它所要做的只是函数原型。如果没有__attribute__((const)) 或类似的东西,编译器就无法知道对fastcos() 的每次调用都会执行相同的操作。不过,您可能会遇到功能对齐的问题。
  • 4000 万次迭代,在 32 位机器上只慢 0.7 秒?这值得浪费你的脑细胞吗?
  • @e4c5 这种特殊功能的性能在实际代码中确实很重要。

标签: assembly floating-point double 32-bit x87


【解决方案1】:

当现代 x86 写入内存后不久再次读取相同的内存时,它作弊以避免对内存/缓存进行完整的往返:

英特尔® 64 和 IA-32 架构优化参考手册

2.3.4.4 存储转发

如果加载跟随存储并重新加载存储写入的数据 到内存,英特尔酷睿微架构可以转发数据 直接从存储到加载。这个过程,称为 store to 负载转发,通过使负载获取数据来节省周期 直接从存储操作而不是通过内存。

文本继续关于对齐要求,但重要的是:

存储的大小必须等于或大于正在加载的数据大小。

在慢速函数中,您将 8 字节双精度数据存储在两个 4 字节块中。据推测,cos()-function 将其加载到单个块中,因此加载必须等到 store 提交到缓存。

另一方面,在 fast 函数中,您存储一个 8 字节的块,该块保留在 cpu 的内部缓冲区中,可以立即满足 cos() 中的负载。

【讨论】:

    猜你喜欢
    • 2014-08-04
    • 2017-02-15
    • 1970-01-01
    • 2018-09-25
    • 2015-07-11
    • 1970-01-01
    • 2017-03-02
    • 2015-08-03
    • 2016-11-12
    相关资源
    最近更新 更多