C++ math.h库中的abs()和我的abs()有什么不同?

17

我目前正在用C++编写一些类似于glsl的矢量数学类,并且刚刚实现了一个abs()函数如下:

template<class T>
static inline T abs(T _a)
{
    return _a < 0 ? -_a : _a;
}

我将其速度与math.h中默认的C++ abs函数进行了比较,如下所示:

clock_t begin = clock();
for(int i=0; i<10000000; ++i)
{
    float a = abs(-1.25);
};

clock_t end = clock();
unsigned long time1 = (unsigned long)((float)(end-begin) / ((float)CLOCKS_PER_SEC/1000.0));

begin = clock();
for(int i=0; i<10000000; ++i)
{
    float a  = myMath::abs(-1.25);
};
end = clock();
unsigned long time2 = (unsigned long)((float)(end-begin) / ((float)CLOCKS_PER_SEC/1000.0));

std::cout<<time1<<std::endl;
std::cout<<time2<<std::endl;

现在默认的 abs 函数需要大约 25 毫秒,而我的函数需要 60 毫秒。我猜测有一些低级别的优化正在进行。有人知道 math.h 中的 abs 函数是如何内部实现的吗?性能差异并不明显,但我只是好奇!


3
也许你可以展示编译器生成的两个循环的汇编代码,并进行比较。否则,我们只是在猜测。 - Greg Hewgill
2
abs 接受一个整数并返回一个整数。因此,在您的比较中使用 abs 是错误的。请改用 <cmath> 中的 fabs - kennytm
请告诉我们您的编译器/环境是什么。 - jpalecek
好的,我确保现在使用了std::abs,应该已经重载了。令我惊讶的是,那个比我的慢得多,花了146毫秒。 - moka
@moka:现在好了!你的代码比实现还要好,太棒了 :) - jpalecek
显示剩余2条评论
8个回答

20

由于实现者可以自由地做出许多假设,因此他们知道 double 的格式并且可以使用它来进行一些技巧操作。

很可能(几乎不用怀疑),您的 double二进制64位浮点数格式。这意味着符号位有自己的一个比特位,并且绝对值只需清除该比特位即可。例如,编译器实现者可以进行以下特殊化操作:

template <>
double abs<double>(const double x)
{
    // breaks strict aliasing, but compiler writer knows this behavior for the platform
    uint64_t i = reinterpret_cast<const std::uint64_t&>(x);
    i &= 0x7FFFFFFFFFFFFFFFULL; // clear sign bit

    return reinterpret_cast<const double&>(i);
}

这样做可以消除分支,可能会更快运行。


7
@jpalecek说:“我没有看到其中有任何未定义行为。reinterpret_cast具有实现定义的行为。(因此我的意思是,"它们可以做出任何实现特定的假设,因为它们是实现。")非库编写者则不能。一旦你编写了这段代码,你就对你的实现做出了某些假设。” - GManNickG
1
如果double不是IEEE-754,那么是的。它至少不太便携。 - Yann Ramin
5
重点在于实现可以编写这段代码,但你不能。 - rlbond
3
对于你来说,可能是未定义的。但对于库的实现者来说,它是明确定义的(因为数学库与编译器紧密耦合)。他们知道实现正在做什么,因此可以根据这些知识进行某些优化。而另一方面,你不能做出任何假设,因为你的代码与编译器没有耦合。 - Martin York
4
实际上,我认为你似乎没有理解重点。 - Martin York
显示剩余11条评论

11

有一些众所周知的技巧可以计算二进制补码有符号数的绝对值。如果这个数是负数,那么翻转所有位并加1,即使用-1进行异或,然后减去-1。如果它是正数,则无需更改,即使用0进行异或,然后减去0。

int my_abs(int x)
{
    int s = x >> 31;
    return (x ^ s) - s;
}

浮点数不是以二进制补码存储的,但这是绝对值的一个巧妙技巧。(为了确定符号,我执行相同的第一步,但然后返回s | 1;-1 = 负数,+1 = 正数) - dash-tom-bang
@jpal 好的,但是莫卡确切地在哪里表明他只对浮点类型感兴趣呢?也许应该将template<class T>替换为template<class Floating>,以便更清楚地表达这一点? - fredoverflow

9
你的编译器和设置是什么?我相信微软和GCC实现了许多数学和字符串操作的“内置函数”。下面这行代码:
printf("%.3f", abs(1.25));

落入以下“fabs”代码路径(在msvcr90d.dll中):
004113DE  sub         esp,8 
004113E1  fld         qword ptr [__real@3ff4000000000000 (415748h)] 
004113E7  fstp        qword ptr [esp] 
004113EA  call        abs (4110FFh) 

在MSVCR90D上,abs调用C运行时的“fabs”实现(相当大):

102F5730  mov         edi,edi 
102F5732  push        ebp  
102F5733  mov         ebp,esp 
102F5735  sub         esp,14h 
102F5738  fldz             
102F573A  fstp        qword ptr [result] 
102F573D  push        0FFFFh 
102F5742  push        133Fh 
102F5747  call        _ctrlfp (102F6140h) 
102F574C  add         esp,8 
102F574F  mov         dword ptr [savedcw],eax 
102F5752  movzx       eax,word ptr [ebp+0Eh] 
102F5756  and         eax,7FF0h 
102F575B  cmp         eax,7FF0h 
102F5760  jne         fabs+0D2h (102F5802h) 
102F5766  sub         esp,8 
102F5769  fld         qword ptr [x] 
102F576C  fstp        qword ptr [esp] 
102F576F  call        _sptype (102F9710h) 
102F5774  add         esp,8 
102F5777  mov         dword ptr [ebp-14h],eax 
102F577A  cmp         dword ptr [ebp-14h],1 
102F577E  je          fabs+5Eh (102F578Eh) 
102F5780  cmp         dword ptr [ebp-14h],2 
102F5784  je          fabs+77h (102F57A7h) 
102F5786  cmp         dword ptr [ebp-14h],3 
102F578A  je          fabs+8Fh (102F57BFh) 
102F578C  jmp         fabs+0A8h (102F57D8h) 
102F578E  push        0FFFFh 
102F5793  mov         ecx,dword ptr [savedcw] 
102F5796  push        ecx  
102F5797  call        _ctrlfp (102F6140h) 
102F579C  add         esp,8 
102F579F  fld         qword ptr [x] 
102F57A2  jmp         fabs+0F8h (102F5828h) 
102F57A7  push        0FFFFh 
102F57AC  mov         edx,dword ptr [savedcw] 
102F57AF  push        edx  
102F57B0  call        _ctrlfp (102F6140h) 
102F57B5  add         esp,8 
102F57B8  fld         qword ptr [x] 
102F57BB  fchs             
102F57BD  jmp         fabs+0F8h (102F5828h) 
102F57BF  mov         eax,dword ptr [savedcw] 
102F57C2  push        eax  
102F57C3  sub         esp,8 
102F57C6  fld         qword ptr [x] 
102F57C9  fstp        qword ptr [esp] 
102F57CC  push        15h  
102F57CE  call        _handle_qnan1 (102F98C0h) 
102F57D3  add         esp,10h 
102F57D6  jmp         fabs+0F8h (102F5828h) 
102F57D8  mov         ecx,dword ptr [savedcw] 
102F57DB  push        ecx  
102F57DC  fld         qword ptr [x] 
102F57DF  fadd        qword ptr [__real@3ff0000000000000 (1022CF68h)] 
102F57E5  sub         esp,8 
102F57E8  fstp        qword ptr [esp] 
102F57EB  sub         esp,8 
102F57EE  fld         qword ptr [x] 
102F57F1  fstp        qword ptr [esp] 
102F57F4  push        15h  
102F57F6  push        8    
102F57F8  call        _except1 (102F99B0h) 
102F57FD  add         esp,1Ch 
102F5800  jmp         fabs+0F8h (102F5828h) 
102F5802  mov         edx,dword ptr [ebp+0Ch] 
102F5805  and         edx,7FFFFFFFh 
102F580B  mov         dword ptr [ebp-0Ch],edx 
102F580E  mov         eax,dword ptr [x] 
102F5811  mov         dword ptr [result],eax 
102F5814  push        0FFFFh 
102F5819  mov         ecx,dword ptr [savedcw] 
102F581C  push        ecx  
102F581D  call        _ctrlfp (102F6140h) 
102F5822  add         esp,8 
102F5825  fld         qword ptr [result] 
102F5828  mov         esp,ebp 
102F582A  pop         ebp  
102F582B  ret   

在发布模式下,使用FPU FABS指令(仅在FPU >= Pentium上需要1个时钟周期),反汇编输出如下:

00401006  fld         qword ptr [__real@3ff4000000000000 (402100h)] 
0040100C  sub         esp,8 
0040100F  fabs             
00401011  fstp        qword ptr [esp] 
00401014  push        offset string "%.3f" (4020F4h) 
00401019  call        dword ptr [__imp__printf (4020A0h)] 

4
它可能只是使用位掩码将符号位设置为0。

嘿,我在位运算编程方面经验不是很丰富,那该怎么做呢? - moka

3

可能有以下几种情况:

  • 你确定第一个调用使用的是std::abs吗?它也可以使用C语言中的整数abs(明确调用std::abs,或者使用using std::abs;

  • 编译器可能具有某些浮点函数的内在实现(例如,将它们直接编译为FPU指令)

然而,我很惊讶编译器没有完全消除循环-因为你在循环内部没有做任何有影响的事情,至少对于abs来说,编译器应该知道没有副作用。


3
你的abs版本是内联的,可以计算一次,编译器可以轻松知道返回的值不会改变,因此它甚至不需要调用函数。
你真的需要查看生成的汇编代码(设置断点,并打开“大”调试器视图,如果我没记错的话,这个反汇编将在左下角),然后你就可以看到发生了什么。
你可以在网上找到有关处理器的文档,这些文档会告诉你所有指令的含义,以便你可以弄清楚发生了什么。或者,你可以在这里粘贴它,我们会告诉你的。 ;)

1

可能 abs 的库版本是内置函数,其行为由编译器完全知道,甚至可以在编译时计算值(因为在您的情况下已知)并优化调用。您应该尝试使用仅在运行时已知的值(由用户提供或在两个周期之前使用 rand() 获取)进行基准测试。

如果仍然有差异,可能是因为库中的 abs 是直接使用魔法技巧手工编写的汇编代码,因此可能比生成的代码稍快一些。


0

库函数 abs 操作的是整数,而你显然在测试浮点数。这意味着使用浮点参数调用 abs 函数需要将浮点数转换为整数(如果你使用常量,编译器可能会在编译时进行优化,使得这个过程不产生实际操作),然后进行整数 abs 操作并将结果转换回浮点数。你的模板函数涉及到浮点数的操作,这很可能会产生影响。


std::abs 也适用于浮点数。请参见 https://en.cppreference.com/w/cpp/numeric/math/fabs - VLL

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