【问题标题】:Why is the standard "abs" function faster than mine?为什么标准的“abs”功能比我的快?
【发布时间】:2021-05-07 10:36:29
【问题描述】:

我想尝试制作自己的绝对值函数。我认为计算绝对值的最快方法是简单地屏蔽符号位(IEEE 754 中的最后一位)。我想将它的速度与标准的abs 函数进行比较。这是我的实现:

// Union used for type punning
union float_uint_u
{
    float f_val;
    unsigned int ui_val;
};

// 'MASK' has all bits == 1 except the last one
constexpr unsigned int MASK = ~(1 << (sizeof(int) * 8 - 1));

float abs_bitwise(float value)
{
    float_uint_u ret;
    ret.f_val = value;
    ret.ui_val &= MASK;
       
    return ret.f_val;
}

为了记录,我知道这种类型的双关语不是标准的 C++。但是,这仅用于教育目的,根据文档,this is supported in GCC

我认为这应该是计算绝对值的最快方法,因此它至少应该与标准实现一样快。但是,对随机值的 100000000 次迭代计时,我得到了以下结果:

Bitwise time: 5.47385 | STL time: 5.15662
Ratio: 1.06152

我的abs 函数慢了大约 6%。

汇编输出

我使用-O2 优化和-S 选项(汇编输出)进行编译,以帮助确定发生了什么。我已经提取了相关部分:

; 16(%rsp) is a value obtained from standard input
movss   16(%rsp), %xmm0
andps   .LC5(%rip), %xmm0 ; .LC5 == 2147483647
movq    %rbp, %rdi
cvtss2sd    %xmm0, %xmm0

movl    16(%rsp), %eax
movq    %rbp, %rdi
andl    $2147483647, %eax
movd    %eax, %xmm0
cvtss2sd    %xmm0, %xmm0

观察

我不擅长汇编,但我注意到的主要一点是标准函数直接在xmm0 寄存器上运行。但是对于我的,它首先将值移动到eax(出于某种原因),执行and,然后将其移动到xmm0。我假设额外的mov 是减速发生的地方。我还注意到,对于标准,它将位掩码存储在程序的其他位置而不是立即数。不过,我猜这并不重要。这两个版本也使用不同的指令(例如movl vs movss)。

系统信息

这是在 Debian Linux(不稳定分支)上使用 g++ 编译的。 g++ --version 输出:

g++ (Debian 10.2.1-6) 10.2.1 20210110

如果这两个版本的代码都以相同的方式计算绝对值(通过and),为什么优化器不生成相同的代码?具体来说,为什么它在优化我的实现时感觉需要包含一个额外的mov

【问题讨论】:

  • 不知道为什么,但在 MSVC 中,您的实现在调试中要快得多,但在发行版中是一样的。
  • @D-RAJ 在调试中进行性能比较是没有意义的。他们可能在调试模式下激活了一些在发布版本中不存在的检查。
  • 你试过内联函数吗?
  • @WolfgangLorenz 如果您的意思是 inline 关键字,那么您应该研究一下 inline 现在的含义。 inline 如果您在头文件中定义一个函数并在多个编译单元中使用该头文件,那么inline 很重要。但是对于现代编译器来说,inline 关键字本身并不会改变它是否内联函数的概率。
  • 你为什么操作系统编译? ABI 很奇怪。

标签: c++ gcc g++ compiler-optimization


【解决方案1】:

我得到了一些不同的程序集。根据 x86_64 Linux ABI,float 参数通过xmm0 传递。使用标准fabs,直接在此寄存器上执行按位AND 操作(Intel 语法):

andps xmm0, XMMWORD PTR .LC0[rip] # .LC0 contains 0x7FFFFFFF
ret

但是,在您的情况下,按位 AND 是在 unsigned int 类型的对象上执行的。因此,GCC 执行相同操作,需要先将 xmm0 移动到 eax

movd eax, xmm0
and  eax, 2147483647
movd xmm0, eax
ret

现场演示:https://godbolt.org/z/xj8MMo

我还没有找到任何方法来强制 GCC 优化器直接在 xmm0 上执行 AND,只使用纯 C/C++ 源代码。看来高效的实现需要建立在汇编代码或 Intel 内部代码之上。

相关问题:How to perform a bitwise operation on floating point numbers。所有提出的解决方案基本上都会产生相同的结果。

我也尝试使用copysign函数,但结果更糟。生成的机器代码然后包含 x87 指令。


无论如何,非常有趣的是,Clang 优化器足够聪明,可以使所有 3 种情况下的程序集等效:https://godbolt.org/z/b6Khv5

【讨论】:

  • 您是如何通过 copysign 获得 x87 指令的? Gcc 将copysignf(x,1.) 识别为fabsf(x)
  • @MarcGlisse 不,至少在我的情况下不是:godbolt.org/z/KcsGx8。你需要区分fabs函数和x87 fabs指令。
  • 如果您使用 copysignf(或 std::copysign)而不是普通的 copysign,它会有所帮助。使用 gcc 主干也有帮助。尽管如此,奇怪的是可以有带有copysign的x87指令......而且我无法在本地复制它,也许godbolt有一个奇怪的设置。
  • @MarcGlisse 这个问题与copysign 无关,关于它的讨论在这里是题外话。但是,这可能是一个单独的问题的好主题。
【解决方案2】:

为什么标准的“abs”函数比我的快?

因为对于大多数optimizing compilers(尤其是GCCClang),它会使用编译器已知的专用机器指令

GCC 编译器甚至为 abs 提供了一个 builtin

一定要使用gcc -O3 编译,也许-ffast-math

您可以研究汇编代码:将您的 example.c 编译为 gcc -Wall -O3 -ffast-math -fverbose-asm example.c 并查看发出的 example.s 汇编文件。

在 Linux 系统上(例如Debian),您可以研究GNU libc 的源代码并查看math.h 标准头的内部(并使用g++ -O3 -C -E 来获取预处理形式)

【讨论】:

    猜你喜欢
    • 2021-08-06
    • 2010-09-11
    • 2010-10-19
    • 2017-05-14
    • 2011-10-19
    • 1970-01-01
    • 2023-04-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多