什么时候装配比C快?

了解汇编程序的其中一个原因是,有时它可以用来编写比用高级语言编写代码更具性能的代码,特别是C语言。 然而,我也多次听到它说过,虽然这并非完全错误,但汇编程序实际上可用于生成更高性能代码的情况非常罕见,需要有关汇编的专业知识和经验。

这个问题甚至没有涉及到汇编指令将是机器特定的和非便携式的,或者汇编器的其他方面的事实。 当然,除了这个之外,还有很多很好的理解组装的理由,但这是一个征求例子和数据的特定问题,而不是关于汇编语言和更高级语言的扩展话语。

任何人都可以提供一些具体的例子说明使用现代编译器汇编将比编写良好的C代码更快的情况,并且您是否可以使用概要证据来支持该声明? 我非常相信这些案例存在,但我真的很想知道这些案件究竟有多深奥,因为它似乎是一些争议点。


这是一个真实世界的例子:固定点乘法。

这些功能不仅适用于没有浮点的设备,而且在精度方面也很出色,因为它们可以为您提供32位精度和可预测的错误(浮点只有23位,并且更难以预测精度损失)

在32位体系结构上编写固定点乘法的一种方法如下所示:

int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

这段代码的问题是我们做了一些无法直接用C语言表达的东西。 我们想要乘以两个32位数并得到一个64位的结果,其中我们返回中间的32位。 但是,在C中这个乘法不存在。 你所能做的就是将整数提升到64位,并进行64 * 64 = 64的乘法运算。

x86(ARM,MIPS等)可以在单个指令中进行乘法运算。 很多编译器仍然忽略这个事实并生成调用运行时库函数来执行乘法的代码。 通过图书馆例行程序也常常完成16位的转换(x86也可以进行这种转换)。

所以我们只剩一个或两个库调用来进行乘法运算。 这有严重的后果。 不仅移动速度较慢,寄存器必须在函数调用中保留,也不能帮助内联和代码展开。

如果你在汇编器中重写相同的代码,你可以获得显着的速度提升。

除此之外:使用ASM不是解决问题的最佳方法。 如果你不能用C语言表达它们,大多数编译器都允许你使用一些内在形式的汇编指令。例如,VS.NET2008编译器将32 * 32 = 64位mul公开为__emul,64位移位为__ll_rshift。

使用内在函数,您可以用C编译器有机会理解正在发生的事情的方式来重写函数。 这允许代码被内联,寄存器分配,共同的子表达式消除以及常量传播也可以完成。 通过这种手写汇编代码,您将获得巨大的性能提升。

供参考:VS.NET编译器的定点mul的最终结果是:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

定点划分的性能差异更加严重。 通过编写几条汇总代码,我对分部重定点代码的分解因子10进行了改进。

使用Visual C ++ 2013为这两种方式提供了相同的汇编代码。


许多年前,我正在教一个人用C编程。这个练习是把图形旋转90度。 他回来的解决方案需要几分钟才能完成,主要是因为他使用的是乘法和除法等。我向他展示了如何使用位移来重现问题,并且处理时间降低到大约30秒,优化他的编译器。 我刚刚得到了一个优化编译器,并且相同的代码在<5秒内旋转了图形。 我看着编译器生成的汇编代码,从我看到的汇编代码那里决定,然后我编写汇编程序的日子结束了。


几乎任何时候编译器都会看到浮点代码,手写的版本会更快。 主要原因是编译器无法执行任何强大的优化。 有关该主题的讨论,请参阅MSDN的这篇文章。 下面是一个例子,其中汇编版本的速度是C版本的两倍(用VS2K5编译):

#include "stdafx.h"
#include <windows.h>

float KahanSum
(
  const float *data,
  int n
)
{
   float
     sum = 0.0f,
     C = 0.0f,
     Y,
     T;

   for (int i = 0 ; i < n ; ++i)
   {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum
(
  const float *data,
  int n
)
{
  float
    result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int
    count = 1000000;

  float
    *source = new float [count];

  for (int i = 0 ; i < count ; ++i)
  {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER
    start,
    mid,
    end;

  float
    sum1 = 0.0f,
    sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

和我的电脑上运行默认版本构建*的一些数字:

  C code: 500137 in 103884668
asm code: 500137 in 52129147

出于兴趣,我用dec / jnz交换了循环,并且对时间没有影响 - 有时更快,有时更慢。 我想这个内存有限的方面会使其他优化变得矮小。

哎呀,我正在运行一个稍微不同的代码版本,并输出错误的方式(即C更快!)。 修复并更新了结果。

链接地址: http://www.djcxy.com/p/5399.html

上一篇: When is assembly faster than C?

下一篇: In .NET, which loop runs faster, 'for' or 'foreach'?