【问题标题】:How can I get an intrinsic for the exp() function in x64 code?如何在 x64 代码中获得 exp() 函数的内在函数?
【发布时间】:2012-04-23 03:21:19
【问题描述】:

我有以下代码,并期望使用 exp() 函数的内在版本。不幸的是,它不是 x64 版本,因此比类似的 Win32(即 32 位版本)要慢:

#include "stdafx.h"
#include <cmath>
#include <intrin.h>
#include <iostream>

int main()
{
  const int NUM_ITERATIONS=10000000;
  double expNum=0.00001;
  double result=0.0;

  for (double i=0;i<NUM_ITERATIONS;++i)
  {
    result+=exp(expNum); // <-- The code of interest is here
    expNum+=0.00001;
  }

  // To prevent the above from getting optimized out...
  std::cout << result << '\n';
}

我正在为我的构建使用以下开关:

/Zi /nologo /W3 /WX-
/Ox /Ob2 /Oi /Ot /Oy /GL /D "WIN32" /D "NDEBUG" 
/D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /Gm- 
/EHsc /GS /Gy /arch:SSE2 /fp:fast /Zc:wchar_t /Zc:forScope 
/Yu"StdAfx.h" /Fp"x64\Release\exp.pch" /FAcs /Fa"x64\Release\" 
/Fo"x64\Release\" /Fd"x64\Release\vc100.pdb" /Gd /errorReport:queue 

如您所见,根据MSDN article on intrinsics 的要求,我确实有/Oi/O2/fp:fast。然而,尽管我做出了努力,还是调用了标准库,这使得 exp() 在 x64 构建上的执行速度变慢了。

这是生成的程序集:

  for (double i=0;i<NUM_ITERATIONS;++i)
000000013F911030  movsd      xmm10,mmword ptr [__real@3ff0000000000000 (13F912248h)]  
000000013F911039  movapd     xmm8,xmm6  
000000013F91103E  movapd     xmm7,xmm9  
000000013F911043  movaps     xmmword ptr [rsp+20h],xmm11  
000000013F911049  movsd      xmm11,mmword ptr [__real@416312d000000000 (13F912240h)]  
  {
    result+=exp(expNum);
000000013F911052  movapd     xmm0,xmm7  
000000013F911056  call       exp (13F911A98h) // ***** exp lib call is here *****
000000013F91105B  addsd      xmm8,xmm10  
    expNum+=0.00001;
000000013F911060  addsd      xmm7,xmm9  
000000013F911065  comisd     xmm8,xmm11  
000000013F91106A  addsd      xmm6,xmm0  
000000013F91106E  jb         main+52h (13F911052h)  
  }

正如您在上面的程序集中看到的,有一个对exp() 函数的调用。现在,让我们看看使用 32 位构建为 for 循环生成的代码:

  for (double i=0;i<NUM_ITERATIONS;++i)
00101031  xorps       xmm1,xmm1  
00101034  rdtsc  
00101036  push        ebx  
00101037  push        esi  
00101038  movsd       mmword ptr [esp+1Ch],xmm0  
0010103E  movsd       xmm0,mmword ptr [__real@3ee4f8b588e368f1 (102188h)]  
00101046  push        edi  
00101047  mov         ebx,eax  
00101049  mov         dword ptr [esp+3Ch],edx  
0010104D  movsd       mmword ptr [esp+28h],xmm0  
00101053  movsd       mmword ptr [esp+30h],xmm1  
00101059  lea         esp,[esp]  
  {
    result+=exp(expNum);
00101060  call        __libm_sse2_exp (101EC0h) // <--- Quite different from 64-bit
00101065  addsd       xmm0,mmword ptr [esp+20h]  
0010106B  movsd       xmm1,mmword ptr [esp+30h]  
00101071  addsd       xmm1,mmword ptr [__real@3ff0000000000000 (102180h)]  
00101079  movsd       xmm2,mmword ptr [__real@416312d000000000 (102178h)]  
00101081  comisd      xmm2,xmm1  
00101085  movsd       mmword ptr [esp+20h],xmm0  
    expNum+=0.00001;
0010108B  movsd       xmm0,mmword ptr [esp+28h]  
00101091  addsd       xmm0,mmword ptr [__real@3ee4f8b588e368f1 (102188h)]  
00101099  movsd       mmword ptr [esp+28h],xmm0  
0010109F  movsd       mmword ptr [esp+30h],xmm1  
001010A5  ja          wmain+40h (101060h)  
  }

那里的代码更多,但速度更快。我在 3.3 GHz Nehalem-EP 主机上进行的时序测试产生了以下结果:

32 位:

对于循环体平均执行时间:34.849229 个周期 / 10.560373 ns

64 位:

对于循环体平均执行时间:45.845323 个周期 / 13.892522 ns

确实是非常奇怪的行为。为什么会这样?

更新:

我创建了一个Microsoft Connect bug report。请随意投票,以获得 Microsoft 本身关于浮点内在函数使用的权威答案,尤其是在 x64 代码中。

【问题讨论】:

  • This article(解释为什么 VS 没有 64 位版本)指出 64 位版本可能比 32 位版本慢。不过,我不知道这种解释是否适用于您的具体情况。
  • 那篇文章是关于 64 位版本的 Visual Studio 本身,它与提出的问题无关。有许多因素可以使 64 位应用程序比 32 位应用程序慢。但是,除非我遗漏了什么,否则这些因素都与我关于浮点计算的问题无关。
  • @MichaelGoldshteyn - 我的错误
  • GregC,删除 /D "WIN32" 对生成的代码没有影响。
  • @GregC,关于您指向 software.intel.com 的链接...,我们没有在我们的项目中使用 SVML 库,所以我没有。我只是想让构建不辜负微软基于 MSDN 的“保证”。

标签: c++ visual-studio-2010 visual-c++ visual-c++-2010 intrinsics


【解决方案1】:

在 x64 上,使用 SSE 执行浮点运算。这没有针对exp() 的内置操作,因此调用标准库是不可避免的,除非您编写自己的内联手动矢量化__m128d exp(__m128d) (Fastest Implementation of Exponential Function Using SSE)。

我想你提到的 MSDN 文章是用 32 位代码编写的,考虑到了 8087 FP。

【讨论】:

  • 请查看我编辑的问题,其中包括由 32 位构建生成的代码以及 32 位与 64 位的时序比较。两种构建都没有使用“真正的”内在函数,但调用的函数存在差异,32 位构建明显更快。
  • 也许吧,但事实仍然是在任何 SSE 操作码中都没有 exp 内在
  • 这是真的,但我期待根据 MSDN 文档,将 exp() 的内在实现内联到我的(汇编)代码中。
  • 我敢打赌,文档根本没有更新以解释 SSE 代码生成。而且我怀疑如果您从选项中删除 /arch:sse2 并以 8087 FPU 为目标,那么您将看到正在进行的内部调用。
  • 从 32 位构建中删除 SSE2 确实会产生完全不同的代码,它使用 8087“f”指令,我没有看到任何 exp() lib 调用。不过,代码几乎慢了三倍。但是,您似乎确实在做某事。对于 64 位版本,不可能在编译器中禁用 SSE2,因为所有 64 位处理器都必须支持它。因此,生成的(汇编)代码没有变化。
【解决方案2】:

我认为 Microsoft 提供 32 位 SSE2 exp() 的内在版本的唯一原因是标准调用约定。 32 位调用约定要求将操作数压入主堆栈,并将结果返回到 FPU 堆栈的顶部寄存器中。如果您启用了 SSE2 代码生成,则返回值可能会从 FPU 堆栈弹出到内存中,然后从该位置加载到 SSE2 寄存器中,以便对结果进行任何数学运算。显然,在 SSE2 寄存器中传递操作数并在 SSE2 寄存器中返回结果会更快。这就是 __libm_sse2_exp() 所做的。在 64 位代码中,标准调用约定传递操作数并在 SSE2 寄存器中返回结果,因此使用内部版本没有优势。

exp() 的 32 位 SSE2 和 64 位实现之间的性能差异的原因是微软在这两种实现中使用了不同的算法。我不知道他们为什么要这样做,并且它们会为某些操作数产生不同的结果(相差 1ulp)。

【讨论】:

    【解决方案3】:

    编辑我想在此讨论中添加指向AMD's x64 instruction set manualsIntel's reference 的链接。

    在初始检查时,应该有一种方法可以使用 F2XM1 来计算指数。但是,它在 x87 指令集中,hidden in x64 mode.

    正如VirtualDub discussion boards. 上的帖子所述,明确使用 MMX/x87 是有希望的,这是how to actually write asm in VC++.

    【讨论】:

      猜你喜欢
      • 2010-09-26
      • 1970-01-01
      • 2019-01-09
      • 1970-01-01
      • 1970-01-01
      • 2015-05-03
      • 2017-05-03
      • 2021-09-28
      相关资源
      最近更新 更多