使用SSE最快地实现指数函数

我正在寻找在SSE元素上运算的指数函数的近似值.即 – __m128 exp(__ m128 x).

我有一个快速但实际上准确度非常低的实现:

static inline __m128 FastExpSse(__m128 x)
{
    __m128 a = _mm_set1_ps(12102203.2f); // (1 << 23) / ln(2)
    __m128i b = _mm_set1_epi32(127 * (1 << 23) - 486411);
    __m128  m87 = _mm_set1_ps(-87);
    // fast exponential function,x should be in [-87,87]
    __m128 mask = _mm_cmpge_ps(x,m87);

    __m128i tmp = _mm_add_epi32(_mm_cvtps_epi32(_mm_mul_ps(a,x)),b);
    return _mm_and_ps(_mm_castsi128_ps(tmp),mask);
}

任何人都可以以更快的速度(或更快)获得更高精度的实现吗?

如果我用C风格写的话,我会很高兴的.

谢谢.

解决方法

下面的C代码是我在 previous answer中用于类似问题的算法的SSE内在函数的转换.

基本思想是将标准指数函数的计算转换为2的幂的计算:expf(x)= exp2f(x / logf(2.0f))= exp2f(x * 1.44269504).我们将t = x * 1.44269504分成整数i和分数f,使得t = if和0 <= f <= 1.我们现在可以用多项式近似计算2f,然后通过添加将结果缩放2i i到单精度浮点结果的指数字段. SSE实现存在的一个问题是我们想要计算i = floorf(t),但是没有快速计算floor()函数的方法.但是,我们观察到正数,floor(x)== trunc(x),而对于负数,floor(x)== trunc(x) – 1,除非x是负整数.但是,由于核近似可以处理f值为1.0f,因此使用负参数的近似值是无害的. SSE提供了将单精度浮点操作数转换为具有截断的整数的指令,因此该解决方案是有效的. Peter Cordes指出SSE4.1支持快速楼层功能_mm_floor_ps(),因此使用SSE4.1的变体也如下所示.当启用SSE 4.1代码生成时,并非所有工具链都会自动预定义宏__SSE4_1__,但gcc会这样做.

编译器资源管理器(Godbolt)显示gcc 7.2将下面的代码编译为sixteen instructions,用于普通SSE,twelve instructions用于SSE 4.1.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <emmintrin.h>
#ifdef __SSE4_1__
#include <smmintrin.h>
#endif

/* max. rel. error = 1.72863156e-3 on [-87.33654,88.72283] */
__m128 fast_exp_sse (__m128 x)
{
    __m128 t,f,e,p,r;
    __m128i i,j;
    __m128 l2e = _mm_set1_ps (1.442695041f);  /* log2(e) */
    __m128 c0  = _mm_set1_ps (0.3371894346f);
    __m128 c1  = _mm_set1_ps (0.657636276f);
    __m128 c2  = _mm_set1_ps (1.00172476f);

    /* exp(x) = 2^i * 2^f; i = floor (log2(e) * x),0 <= f <= 1 */   
    t = _mm_mul_ps (x,l2e);             /* t = log2(e) * x */
#ifdef __SSE4_1__
    e = _mm_floor_ps (t);                /* floor(t) */
    i = _mm_cvtps_epi32 (e);             /* (int)floor(t) */
#else /* __SSE4_1__*/
    i = _mm_cvttps_epi32 (t);            /* i = (int)t */
    j = _mm_srli_epi32 (_mm_castps_si128 (x),31); /* signbit(t) */
    i = _mm_sub_epi32 (i,j);            /* (int)t - signbit(t) */
    e = _mm_cvtepi32_ps (i);             /* floor(t) ~= (int)t - signbit(t) */
#endif /* __SSE4_1__*/
    f = _mm_sub_ps (t,e);               /* f = t - floor(t) */
    p = c0;                              /* c0 */
    p = _mm_mul_ps (p,f);               /* c0 * f */
    p = _mm_add_ps (p,c1);              /* c0 * f + c1 */
    p = _mm_mul_ps (p,f);               /* (c0 * f + c1) * f */
    p = _mm_add_ps (p,c2);              /* p = (c0 * f + c1) * f + c2 ~= 2^f */
    j = _mm_slli_epi32 (i,23);          /* i << 23 */
    r = _mm_castsi128_ps (_mm_add_epi32 (j,_mm_castps_si128 (p))); /* r = p * 2^i*/
    return r;
}

int main (void)
{
    union {
        float f[4];
        unsigned int i[4];
    } arg,res;
    double relerr,maxrelerr = 0.0;
    int i,j;
    __m128 x,y;

    float start[2] = {-0.0f,0.0f};
    float finish[2] = {-87.33654f,88.72283f};

    for (i = 0; i < 2; i++) {

        arg.f[0] = start[i];
        arg.i[1] = arg.i[0] + 1;
        arg.i[2] = arg.i[0] + 2;
        arg.i[3] = arg.i[0] + 3;
        do {
            memcpy (&x,&arg,sizeof(x));
            y = fast_exp_sse (x);
            memcpy (&res,&y,sizeof(y));
            for (j = 0; j < 4; j++) {
                double ref = exp ((double)arg.f[j]);
                relerr = fabs ((res.f[j] - ref) / ref);
                if (relerr > maxrelerr) {
                    printf ("arg=% 15.8e  res=%15.8e  ref=%15.8e  err=%15.8e\n",arg.f[j],res.f[j],ref,relerr);
                    maxrelerr = relerr;
                }
            }   
            arg.i[0] += 4;
            arg.i[1] += 4;
            arg.i[2] += 4;
            arg.i[3] += 4;
        } while (fabsf (arg.f[3]) < fabsf (finish[i]));
    }
    printf ("maximum relative errror = %15.8e\n",maxrelerr);
    return EXIT_SUCCESS;
}

fast_sse_exp()的另一种设计使用众所周知的技术添加“魔法”转换常量1.5 * 223来强制舍入,从而以舍入到最接近的模式提取调整后的参数x / log(2)的整数部分.正确的位位置,然后再次减去相同的数字.这要求在添加期间有效的SSE舍入模式是“舍入到最接近或甚至”,这是默认值. wim在评论中指出,当使用积极优化时,某些编译器可能会优化转换常量cvt的加法和减法,从而干扰此代码序列的功能,因此建议检查生成的机器代码.用于计算2f的近似间隔现在以零为中心,因为-0.5 <= f <= 0.5,需要不同的核近似.

/* max. rel. error <= 1.72860465e-3 on [-87.33654,j;

    const __m128 l2e = _mm_set1_ps (1.442695041f); /* log2(e) */
    const __m128 cvt = _mm_set1_ps (12582912.0f);  /* 1.5 * (1 << 23) */
    const __m128 c0 =  _mm_set1_ps (0.238428936f);
    const __m128 c1 =  _mm_set1_ps (0.703448006f);
    const __m128 c2 =  _mm_set1_ps (1.000443142f);

    /* exp(x) = 2^i * 2^f; i = rint (log2(e) * x),-0.5 <= f <= 0.5 */
    t = _mm_mul_ps (x,l2e);             /* t = log2(e) * x */
    r = _mm_sub_ps (_mm_add_ps (t,cvt),cvt); /* r = rint (t) */
    f = _mm_sub_ps (t,r);               /* f = t - rint (t) */
    i = _mm_cvtps_epi32 (t);             /* i = (int)t */
    p = c0;                              /* c0 */
    p = _mm_mul_ps (p,c2);              /* p = (c0 * f + c1) * f + c2 ~= exp2(f) */
    j = _mm_slli_epi32 (i,_mm_castps_si128 (p))); /* r = p * 2^i*/
    return r;
}

问题中代码的算法似乎取自Nicol N. Schraudolph的工作,它巧妙地利用了IEEE-754二进制浮点格式的半对数性质:

N. N. Schraudolph. “指数函数的快速,紧凑近似.” Neural Computation,11(4),1999年5月,pp.853-862.

删除参数钳位代码后,它只减少到三个SSE指令. “神奇”校正常数486411对于最小化整个输入域上的最大相对误差不是最佳的.基于简单的二进制搜索,值298765似乎更优越,将fastExpSse()的最大相对误差降低到3.56e-2,而fast_exp_sse()的最大相对误差为1.73e-3.

/* max. rel. error = 3.55959567e-2 on [-87.33654,88.72283] */
__m128 FastExpSse (__m128 x)
{
    __m128 a = _mm_set1_ps (12102203.0f); /* (1 << 23) / log(2) */
    __m128i b = _mm_set1_epi32 (127 * (1 << 23) - 298765);
    __m128i t = _mm_add_epi32 (_mm_cvtps_epi32 (_mm_mul_ps (a,b);
    return _mm_castsi128_ps (t);
}

Schraudolph算法基本上对[0,1]中的f使用线性逼近2f~ = 1.0 f,并且通过添加二次项可以提高其精度. Schraudolph方法的聪明部分是计算2i * 2f而没有明确地将整数部分i = floor(x * 1.44269504)与分数分开.我认为无法将该技巧扩展到二次近似,但是当然可以将Schraudolph的floor()计算与上面使用的二次近似相结合:

/* max. rel. error <= 1.72886892e-3 on [-87.33654,88.72283] */
__m128 fast_exp_sse (__m128 x)
{
    __m128 f,r;
    __m128i t,j;
    const __m128 a = _mm_set1_ps (12102203.0f); /* (1 << 23) / log(2) */
    const __m128i m = _mm_set1_epi32 (0xff800000); /* mask for integer bits */
    const __m128 ttm23 = _mm_set1_ps (1.1920929e-7f); /* exp2(-23) */
    const __m128 c0 = _mm_set1_ps (0.3371894346f);
    const __m128 c1 = _mm_set1_ps (0.657636276f);
    const __m128 c2 = _mm_set1_ps (1.00172476f);

    t = _mm_cvtps_epi32 (_mm_mul_ps (a,x));
    j = _mm_and_si128 (t,m);            /* j = (int)(floor (x/log(2))) << 23 */
    t = _mm_sub_epi32 (t,j);
    f = _mm_mul_ps (ttm23,_mm_cvtepi32_ps (t)); /* f = (x/log(2)) - floor (x/log(2)) */
    p = c0;                              /* c0 */
    p = _mm_mul_ps (p,c2);              /* p = (c0 * f + c1) * f + c2 ~= 2^f */
    r = _mm_castsi128_ps (_mm_add_epi32 (j,_mm_castps_si128 (p))); /* r = p * 2^i*/
    return r;
}

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


一.C语言中的static关键字 在C语言中,static可以用来修饰局部变量,全局变量以及函数。在不同的情况下static的作用不尽相同。 (1)修饰局部变量 一般情况下,对于局部变量是存放在栈区的,并且局部变量的生命周期在该语句块执行结束时便结束了。但是如果用static进行修饰的话,该变量便存
浅谈C/C++中的指针和数组(二) 前面已经讨论了指针和数组的一些区别,然而在某些情况下,指针和数组是等同的,下面讨论一下什么时候指针和数组是相同的。C语言标准对此作了说明:规则1:表达式中的数组名被编译器当做一个指向该数组第一个元素的指针; 注:下面几种情况例外 1)数组名作为sizeof的操作数
浅谈C/C++中的指针和数组(一)指针是C/C++的精华,而指针和数组又是一对欢喜冤家,很多时候我们并不能很好的区分指针和数组,对于刚毕业的计算机系的本科生很少有人能够熟练掌握指针以及数组的用法和区别。造成这种原因可能跟现在大学教学以及现在市面上流行的很多C或者C++教程有关,这些教程虽然通俗易懂,
从两个例子分析C语言的声明 在读《C专家编程》一书的第三章时,书中谈到C语言的声明问题,《C专家编程》这本书只有两百多页,却花了一章的内容去阐述这个问题,足以看出这个问题的重要性,要想透彻理解C语言的声明问题仅仅看书是远远不够的,需要平时多实践并大量阅读别人写的代码。下面借鉴《C专家编程》书中的两个
C语言文件操作解析(一)在讨论C语言文件操作之前,先了解一下与文件相关的东西。一.文本文件和二进制文件 文本文件的定义:由若干行字符构成的计算机文件,存在于计算机系统中。文本文件只能存储文件中的有效字符信息,不能存储图像、声音等信息。狭义上的二进制文件则指除开文本文件之外的文件,如图片、DOC文档。
C语言文件操作解析(三) 在前面已经讨论了文件打开操作,下面说一下文件的读写操作。文件的读写操作主要有4种,字符读写、字符串读写、块读写以及格式化读写。一.字符读写 字符读写主要使用两个函数fputc和fgetc,两个函数的原型是: int fputc(int ch,FILE *fp);若写入成功则
浅谈C语言中的位段 位段(bit-field)是以位为单位来定义结构体(或联合体)中的成员变量所占的空间。含有位段的结构体(联合体)称为位段结构。采用位段结构既能够节省空间,又方便于操作。 位段的定义格式为: type [var]:digits 其中type只能为int,unsigned int,s
C语言文件操作解析(五)之EOF解析 在C语言中,有个符号大家都应该很熟悉,那就是EOF(End of File),即文件结束符。但是很多时候对这个理解并不是很清楚,导致在写代码的时候经常出错,特别是在判断文件是否到达文件末尾时,常常出错。1.EOF是什么? 在VC中查看EOF的定义可知: #def
关于VC+ʶ.0中getline函数的一个bug 最近在调试程序时,发现getline函数在VC+ʶ.0和其他编译器上运行结果不一样,比如有如下这段程序:#include &lt;iostream&gt;#include &lt;string&gt;using namespace std;int
C/C++浮点数在内存中的存储方式 任何数据在内存中都是以二进制的形式存储的,例如一个short型数据1156,其二进制表示形式为00000100 10000100。则在Intel CPU架构的系统中,存放方式为 10000100(低地址单元) 00000100(高地址单元),因为Intel CPU
浅析C/C++中的switch/case陷阱 先看下面一段代码: 文件main.cpp#includeusing namespace std;int main(int argc, char *argv[]){ int a =0; switch(a) { case ...
浅谈C/C++中的typedef和#define 在C/C++中,我们平时写程序可能经常会用到typedef关键字和#define宏定义命令,在某些情况下使用它们会达到相同的效果,但是它们是有实质性的区别,一个是C/C++的关键字,一个是C/C++的宏定义命令,typedef用来为一个已有的数据类型
看下面一道面试题:#include&lt;stdio.h&gt;#include&lt;stdlib.h&gt;int main(void) { int a[5]={1,2,3,4,5}; int *ptr=(int *)(&amp;aʱ); printf(&quot;%d,%d&quot;,*(
联合体union 当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。在C Programming Language 一书中对于联合体是这么描述的: 1)联合体是一个结构; 2)它的所有成员相对于基地址的偏移量都为0; 3)此结构空间要大到足够容纳最&quot;宽&quo
从一个程序的Bug解析C语言的类型转换 先看下面一段程序,这段程序摘自《C 专家编程》:#include&lt;stdio.h&gt;int array[]={23,34,12,17,204,99,16};#define TOTAL_ELEMENTS (sizeof(array)/sizeof(ar
大端和小端 嵌入式开发者应该对大端和小端很熟悉。在内存单元中数据是以字节为存储单位的,对于多字节数据,在小端模式中,低字节数据存放在低地址单元,而在大端模式中,低字节数据存放在高地址单元。比如一个定义一个short型的变量a,赋值为1,由于short型数据占2字节。在小端模式中,其存放方式为0X40
位运算和sizeof运算符 C语言中提供了一些运算符可以直接操作整数的位,称为位运算,因此位运算中的操作数都必须是整型的。位运算的效率是比较高的,而且位运算运用好的话会达到意想不到的效果。位运算主要有6种:与(&amp;),或(|),取反(~),异或(^),左移(&gt;)。1.位运算中的类型转换位
C语言文件操作解析(四)在文件操作中除了打开操作以及读写操作,还有几种比较常见的操作。下面介绍一下这些操作中涉及到的函数。一.移动位置指针的函数 rewind函数和fseek函数,这两个函数的原型是:void rewind(FILE *fp); 将位置指针移动到文件首 int fseek(FILE
结构体字节对齐 在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排
C语言文件操作解析(二)C语言中对文件进行操作必须首先打开文件,打开文件主要涉及到fopen函数。fopen函数的原型为 FILE* fopen(const char *path,const char *mode) 其中path为文件路径,mode为打开方式 1)对于文件路径,只需注意若未明确给出绝