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

13

我想尝试编写自己的绝对值函数。我认为计算绝对值最快的方法是简单地屏蔽掉符号位(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++。 然而,这只是为了教育目的,并且根据文档,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 (unstable branch) 上使用 g++ 编译的。g++ --version 输出:

g++ (Debian 10.2.1-6) 10.2.1 20210110

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


4
在调试模式下进行性能比较是没有意义的。他们可能在调试模式下启用了一些检查,而这些检查在发布版本中不存在。 - t.niese
1
你尝试过将函数内联吗? - Xoozee
3
如果您指的是inline关键字,那么您应该对现在inline的含义进行一些研究。如果您在头文件中定义了一个函数并在多个编译单元中使用该头文件,则inline非常重要。但是对于现代编译器来说,仅使用inline关键字并不能改变内联函数的概率。 - t.niese
2
你正在编译哪个操作系统?ABI很奇怪。 - Marc Glisse
1
在掩码初始化中,1 应该被写作 1u,否则会出现实现定义的行为(将 1 左移至符号位)。 - M.M
显示剩余9条评论
2个回答

7

我的汇编代码和普通的有些不同。根据x86_64 Linux ABI,一个float类型的参数通过xmm0传递。使用标准的fabs函数时,位运算符AND直接在这个寄存器上执行(Intel语法):

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

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

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

在线演示:https://godbolt.org/z/xj8MMo

我没有找到任何一种方法能够强制GCC优化器直接在纯C/C++源代码中对xmm0执行AND操作。看起来高效的实现需要建立在汇编代码或英特尔内置函数之上。

相关问题:如何对浮点数执行按位运算。所有提出的解决方案基本上会得到相同的结果。

我还尝试使用copysign函数,但结果甚至更糟糕。生成的机器码包含x87指令。


无论如何,有趣的是,Clang优化器足够聪明,能够使3种情况下的汇编程序等效:https://godbolt.org/z/b6Khv5


1
你是如何使用copysign得到x87指令的?GCC将copysignf(x,1.)识别为fabsf(x) - Marc Glisse
@MarcGlisse 不,至少在我的情况下不是这样的:https://godbolt.org/z/KcsGx8。你需要区分`fabs`函数和x87 fabs指令。 - Daniel Langr
最好使用copysignf(或std :: copysign)而不是普通的copysign。使用gcc trunk也有所帮助。仍然很奇怪,可能存在具有copysign的x87指令...我无法在本地重现它,也许godbolt设置有些奇怪。 - Marc Glisse
@MarcGlisse 这个问题不是关于 copysign 的,关于它的讨论在这里是离题的。然而,这可能是一个很好的单独问题的话题。 - Daniel Langr

3
为什么标准的“abs”函数比我的快?
因为大多数优化编译器(特别是GCCClang)会使用一个专门的机器指令由编译器知道 GCC编译器甚至有一个用于abs内置函数 请确保使用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获取预处理形式)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接