在Linux / ARMv7上编译代码时,为什么sinf()这么慢?

时间:2015-04-10 22:48:24

标签: linux performance math gcc sin

与我在Windows上获得的程序相比,在ARMv7上编译程序时,使用数学sin()函数的相同C代码运行速度要慢得多。我正在使用-O2 -Wall -Wextra -mfpu=neon -mtune=cortex-a9 -march=armv7 -std=c++11进行编译,而我的gcc是gcc (Ubuntu/Linaro 4.8.2-19ubuntu1) 4.8.2

我不认为sin()对于实时操作来说不是那么快,而且我知道对于更快的sin函数的一个好的折衷方案是使用查找表,但我在这里遇到的可能是编译器中的异常或错误,因为运行sin()函数确实需要很长时间。

我的程序在启动时创建了一些wavetables,虽然它几乎立即在Windows上启动,但在Linux / ARM上启动大约需要25-30秒......

以下是一些代码,显示了使用sinf()函数减慢所有内容的位置。

for (int n = 0; n < 73; ++n)
{
    // Max number of harmonics
    int hrm = int(16000.f / twf[n]);

    // Set vectors
    basic_wf.assign(wavelength[n], 0);

    for (int i = 0; i < wavelength[n]; ++i)
    {
        // Add harmonics
        for (int h = 1; h < hrm; ++h)
        {
            const float harm = 0.14f * (sinf((float)i * FACTOR * twf[n] * (float)h) / (float)h);
            if (h % 2 == 0) basic_wf[i] -= harm;    // add even negative harmonic
            else basic_wf[i] += harm;               // add odd positive harmonic
        }
    }
}

这里我用73个表填充锯齿波形,为每个频率添加所需的谐波次数。音符的音高越低,谐波的数量越多(实际的sin()计算)。这几乎立即在Windows上运行......在我的Linux机器上花了一辈子的时间。

2 个答案:

答案 0 :(得分:1)

代码建议,并且您在评论中的分析证实,sinf()的论证的幅度可以变得非常大,当然可以达到几千。在trig函数的公共库实现中使用的精确参数减少可能是计算密集型的,因此对于大型参数来说很慢,尤其是当硬件平台不支持融合乘法加法运算时。这可能是您观察到的sinf()性能低的一个因素。

您在评论中提到sinf()的操作数包含因子π。这表明你实际上想要使用sinpif(),其中sinpi(x)= sin(x *π)。 sinpi函数是在IEEE-754(2008)浮点标准中引入的,但尚未将其纳入语言标准。然而,一些工具链将其作为扩展。 sinpi()的优点在于,无论参数的大小如何,它都只需要非常简单的参数减少,这可以大大减少执行时间。这导致性能提高。由于π的乘法是隐含的,因此它可以使用sinf()提供比离散方法更高的精度。

我正在展示下面sinpif()的示例C99实现。请注意,此代码在很大程度上依赖于标准数学函数fmaf()来实现高处理速度和出色的准确性。如果您的CPU没有硬件支持融合乘法 - 加法(FMA)操作,则此函数执行速度非常慢,因为正确模拟fmaf()并非易事。由于代码是以模块化方式编写的,因此您需要将编译器配置为应用最大量的函数内联,或者为所有组成函数添加适当的内联属性。

当您指出您的硬件平台不提供对FMA的本机支持时,您可以将fmaf(a,b,c)替换为(a*b+c),但准确性会有所降低。根据我的测试,最大ulp误差增加到1.71364 ulps。这仍然非常好,但在这种情况下my_sinf()不再被忠实地舍入,这通常被认为是理想的财产。

/* Argument reduction for sinpi, cospi, sincospi. Reduces to [-0.25, +0.25] */
float trig_red_pi_f (float a, int *i)
{
    float r;
    r = rintf (a + a);
    *i = (int)r;
    r = a - 0.5f * r;
    return r;
}

/* Approximate cos(pi*x) for x in [-0.25,0.25]. Maximum ulp error = 0.87440 */
float cospif_poly (float s)
{
    float r;
    r =              0x1.d98dcep-3f;   //  2.31227502e-1f
    r = fmaf (r, s, -0x1.55c4e8p+0f);  // -1.33503580e+0f
    r = fmaf (r, s,  0x1.03c1d4p+2f);  //  4.05870533e+0f
    r = fmaf (r, s, -0x1.3bd3ccp+2f);  // -4.93480206e+0f
    r = fmaf (r, s,  0x1.000000p+0f);  //  1.00000000e+0f
    return r;
}

/* Approximate sin(pi*x) for x in [-0.25,0.25]. Maximum ulp error = 0.96441 */
float sinpif_poly (float a, float s)
{
    float r;
    r =             -0x1.2dc6f8p-1f;   // -5.89408636e-1f
    r = fmaf (r, s,  0x1.46602ep+1f);  //  2.54981017e+0f
    r = fmaf (r, s, -0x1.4abbc0p+2f);  // -5.16770935e+0f
    r = r * s;
    r = fmaf (r, a, -0x1.777a5cp-24f * a); // PI_lo // -8.74227766e-8f
    r = fmaf (a, 0x1.921fb6p+1f, r);       // PI_hi //  3.14159274e+0f
    return r;
}

/* Compute sin(pi*x) and cos(pi*x) based on quadrant */
float sinpif_cospif_core (float a, int i)
{
    float r, s;
    s = a * a;
    r = (i & 1) ? cospif_poly (s) : sinpif_poly (a, s);
    if (i & 2) {
        r = 0.0f - r; // don't change "sign" of NaNs or create negative zeros
    }
    return r;
}

/* maximum ulp error = 0.96411 */
float my_sinpif (float a)
{
    float r;
    int i;
    r = trig_red_pi_f (a, &i);
    r = sinpif_cospif_core (r, i);
    /* IEEE-754: sinPi(+n) is +0 and sinPi(-n) is -0 for positive integers n */
    r = (a == truncf (a)) ? (a * 0.0f) : r;
    return r;
}

答案 1 :(得分:0)

你可以做Napier和Co.为计算对数表所做的事情 - 或者更准确地说是1.000001或类似力量的表格。

如果您需要值为sin(k*w)的向量,则计算c1000=cos(1000*w)s1000=sin(1000*w),设置

c[0] = 1; s[0] = 0;
c[1000]=c1000; s[1000] = s1000;

然后迭代

c[1000*(k+1)] = c1000*c[1000*k]-s1000*s[1000*k];
s[1000*(k+1)] = c1000*s[1000*k]+s1000*c[1000*k];

然后使用三角形标识再次使用c1=cos(w)s1=sin(w)填充空白,向前迈出1000步,或者如果你想要500前锋和500后退。这种多级方法应该使浮点误差的积累足够小。

On&#34; big&#34;处理器应该没有大的差别,2次乘法和sincos评估的成本应该是可比较的。在你的情况下,乘法过程应该有一些优势。