Приближение косинуса к [0,pi] с использованием только одинарной точности с плавающей запятой

В настоящее время я работаю над приближением косинуса. Поскольку конечным целевым устройством является самостоятельная разработка, работающая с 32-битным ALU/LU с плавающей запятой, и существует специализированный компилятор для C, я не могу использовать математические функции библиотеки c (cosf,...). Я стремлюсь кодировать различные методы, которые отличаются точностью и количеством инструкций/циклов.

Я уже пробовал много разных алгоритмов аппроксимации, начиная с fdlibm, расширения Тейлора, аппроксимации Паде, алгоритма Ремеза с использованием клена и так далее....

Но как только я реализую их, используя только точность с плавающей запятой, происходит значительная потеря точности. И будьте уверены: я знаю, что с двойной точностью гораздо более высокая точность вообще не проблема...

Прямо сейчас у меня есть некоторые приближения, которые точны до нескольких тысяч ulp около pi/2 (диапазон, в котором возникают самые большие ошибки), и я чувствую, что я ограничен преобразованиями с одинарной точностью.

Чтобы решить проблему сокращения аргумента темы: ввод в радианах. я предполагаю, что уменьшение аргумента приведет к еще большей потере точности из-за деления/умножения.... поскольку мой общий диапазон ввода составляет всего 0..pi, я решил уменьшить аргумент до 0..pi/2.

Поэтому мой вопрос: кто-нибудь знает одноточное приближение к функции косинуса с высокой точностью (и в лучшем случае с высокой эффективностью)? Существуют ли какие-либо алгоритмы, оптимизирующие приближения для одинарной точности? Знаете ли вы, вычисляет ли встроенная функция cosf значения с одинарной или двойной точностью? ~

float ua_cos_v2(float x)
{
    float output;
    float myPi = 3.1415927410125732421875f;
    if (x < 0) x = -x;
    int quad = (int32_t)(x*0.63661977236f);//quad = x/(pi/2) = x*2/pi
    if (x<1.58f && x> 1.57f) //exclude approximation around pi/2
    {
        output = -(x - 1.57079637050628662109375f) - 2.0e-12f*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f) + 0.16666667163372039794921875f*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f) + 2.0e-13f*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)+ 0.000198412701138295233249664306640625f*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f)*(x - 1.57079637050628662109375f);
        output -= 4.37E-08f;
    }
    else {
        float param_x;
        int param_quad = -1;
        switch (quad)
        {
        case 0:
            param_x = x;
            break;
        case 1:
            param_x = myPi - x;
            param_quad = 1;
            break;
        case 2:
            param_x = x - myPi;
            break;
        case 3:
            param_x = 2 * myPi - x;
            break;
        }
        float c1 = 1.0f,
            c2 = -0.5f,
            c3 = 0.0416666679084300994873046875f,
            c4 = -0.001388888922519981861114501953125f,
            c5 = 0.00002480158218531869351863861083984375f,
            c6 = -2.75569362884198199026286602020263671875E-7f,
            c7 = 2.08583283978214240050874650478363037109375E-9f,
            c8 = -1.10807162057025010426514199934899806976318359375E-11f;
        float _x2 = param_x * param_x;
        output = c1 + _x2*(c2 + _x2*(c3 + _x2*(c4 + _x2*(c5 + _x2*(c6 + _x2*(c7 
        + _x2* c8))))));
        if (param_quad == 1 || param_quad == 0)
            output = -output;
    }
    return output;
}

~

если я забыл какую-либо информацию, пожалуйста, не стесняйтесь спрашивать!

заранее спасибо


person Dexter S    schedule 16.09.2020    source источник
comment
Какая точность вам нужна? Покажите код приближения с недостаточной точностью. Возможно, есть способы повысить точность. (Мы не можем сказать, не видя кода.) Пожалуйста, отредактируйте свой вопрос, чтобы добавить эту информацию, не используйте комментарии. отвечать.   -  person Bodo    schedule 16.09.2020
comment
Ваш фактический ввод в радианах, или вы действительно хотите вычислить cos(x*pi) для 0<=x<=1? В любом случае, прежде чем применять какую-либо полиномиальную аппроксимацию, вы должны уменьшить входной диапазон до [-pi/4, pi/4] и использовать такие тождества, как cos(x+pi/2) = -sin(x).   -  person chtz    schedule 16.09.2020
comment
Для x около π/2, где, как вы говорите, самая большая ошибка, cos(x) близок к π/2−x. Это означает, что аппроксимировать его полиномом несложно. В частности, вы должны использовать y=π/2−x, а затем конкретный многочлен для этого случая будет y, но даже более общий многочлен некоторой формы, такой как y+c3•y^3+c5•y^5+…, будет иметь небольшая ошибка вычислений, так как члены высокого порядка будут фактически равны нулю. Ошибка возникает при вычислении y=π/2−x. Если это сделать с высокой точностью, y будет результатом с точностью до доли ULP около π/2. Если сделать с точностью float, ошибка будет огромной.   -  person Eric Postpischil    schedule 16.09.2020
comment
В этом конкретном случае вы можете рассмотреть возможность хранения π/2 в двух частях. Первый, p0, равен π/2, округленному до float. Второй, p1, равен π/2−p0 (предварительно рассчитанный, результат записан в исходный код). Тогда π/2−x можно точно вычислить с точностью float с точностью p0-x+p1. Когда x является float ближайшим π/2, это дает ошибку около ⅓ ULP. Кроме того, нам нужно будет увидеть код, который вы используете.   -  person Eric Postpischil    schedule 16.09.2020
comment
Спасибо за ваши быстрые ответы. Я надеюсь, что обратился ко всей оставшейся информации. @EricPostpischil: возможно, стоит попробовать ваш подход, особенно если число pi/2 хранится в двух числах с плавающей запятой...   -  person Dexter S    schedule 16.09.2020
comment
Вы уже пробовали CORDIC?   -  person Bob__    schedule 16.09.2020
comment
@Bob__ да и нет. Я много читал о CORDIC и думаю, что даже fdlibm link может использовать алгоритмы CORDIC в своей основе. и я еще не нашел хорошей реализации одинарной точности в C...   -  person Dexter S    schedule 16.09.2020
comment
@EricPostpischil только что попробовал ваше предложение, которое действительно работает очень хорошо, используя следующий код: int b = 0x3fc90fdb; //pi/2 float p0 = *((float*)&b); float p1 = -4.371139000186242830836e-8f; float cos = p0 - x + p1; , по крайней мере, для очень небольших интервалов, таких как 1.5707f .. 1.5709f. Есть ли у вас какие-либо другие идеи для приближений, которые можно использовать для остальной части интервала, скажем, от 1,4 до 1,9?   -  person Dexter S    schedule 16.09.2020
comment
с высокой точностью (и в лучшем случае с высокой эффективностью) --› что важнее? Я бы предложил самый быстрый, если точность ‹= N ULP, где N — ваш выбор в диапазоне [1...2].   -  person chux - Reinstate Monica    schedule 16.09.2020
comment
Вам нужен синус ()? Часто лучше написать вспомогательную версию helper_sin(sub_range_x) и helper_cos() для решения my_cos(wider_x_range)?   -  person chux - Reinstate Monica    schedule 16.09.2020
comment
Если вы ищете скорость, может быть быстрее закодировать cosd(some_degree_x), а не cos(), так как уменьшение аргумента легче выполнить быстро и точно, чем начинать с радианов.   -  person chux - Reinstate Monica    schedule 16.09.2020
comment
@DexterS Обеспечивает ли ваше целевое оборудование операцию плавного умножения-сложения (FMA) или просто умножение и сложение с плавающей запятой?   -  person njuffa    schedule 17.09.2020
comment
точность поначалу важнее, поскольку у нас есть только 1024 инструкции для хранения в IMEM, эффективность может ограничивать точность. ввод должен быть в радианах...   -  person Dexter S    schedule 17.09.2020


Ответы (2)


Безусловно, можно вычислить косинус на [0, π] с любой желаемой границей ошибки ›= 0,5 ulp, используя только собственные операции точности. Однако чем ближе цель к правильно округленной функции, тем больше требуется предварительных проектных и вычислительных работ во время выполнения.

Реализации трансцендентных функций обычно состоят из сокращения аргументов, основного приближения, окончательного исправления для противодействия уменьшению аргументов. В тех случаях, когда сокращение аргумента включает вычитание, необходимо избегать катастрофической отмены, явно или неявно используя более высокую точность. Неявные методы могут быть разработаны так, чтобы полагаться только на собственное вычисление точности, например, путем разделения константы, такой как π, на невычисленную сумму, такую ​​​​как 1.57079637e+0f - 4.37113883e-8f, при использовании IEEE-754 binary32 (одинарная точность).

Достичь высокой точности с помощью встроенных вычислений точности намного проще, если аппаратно реализована операция плавного умножения-сложения (FMA). OP не указал, обеспечивает ли их целевая платформа эту операцию, поэтому я сначала покажу очень простой подход, предлагающий умеренную точность (максимальная ошибка ‹ 5 ulps), основанный только на умножениях и добавлениях. Я предполагаю, что оборудование соответствует стандарту IEEE-754, и предполагаю, что float отображается в формат IEEE-754 binary32.

Нижеследующее основано на сообщении в блоге Колина Уоллеса под названием «Приближение sin(x) к 5 ULP с полиномами Чебышева», которое на момент написания недоступно в Интернете. Первоначально я получил его здесь, и в настоящее время Google сохраняет кешированную копию здесь. Они предлагают аппроксимировать синус на [-π, π] с помощью полинома от x² от sin(x)/(x*(x²-π²)), а затем умножить его на x*(x²-π²). Стандартный прием для более точного вычисления a²-b² состоит в том, чтобы переписать его как (a-b) * (a+b). Представление π в виде невычисленной суммы двух чисел с плавающей запятой pi_high и pi_low позволяет избежать катастрофической отмены во время вычитания, которая превращает вычисление x²-π² в ((x - pi_hi) - pi_lo) * ((x + pi_hi) + pi_lo).

В идеале полиномиальная базовая аппроксимация должна использовать минимаксную аппроксимацию, которая минимизируетминимальную максимальную ошибку. Я сделал так здесь. Для этого можно использовать различные стандартные инструменты, такие как Maple или Mathematics, или создать собственный код на основе алгоритма Remez.

Для вычисления косинуса на [0, PI] мы можем использовать тот факт, что cos (t) = sin (π/2 - t). Подстановка x = (π/2 - t) в x * (x - π/2) * (x + π/2) дает (π/2 - t) * (3π/2 - t) * (-π/2 - т). Константы могут быть разделены на старшую и младшую части (или голову и хвост, если использовать другую распространенную идиому), как и раньше.

/* Approximate cosine on [0, PI] with maximum error of 4.704174 ulp */
float cosine (float x)
{
    const float half_pi_hi       =  1.57079637e+0f; //  0x1.921fb6p+0
    const float half_pi_lo       = -4.37113883e-8f; // -0x1.777a5cp-25
    const float three_half_pi_hi =  4.71238899e+0f; //  0x1.2d97c8p+2
    const float three_half_pi_lo = -1.19248806e-8f; // -0x1.99bc5cp-27
    float p, s, hpmx, thpmx, nhpmx;

    /* cos(x) = sin (pi/2 - x) = sin (hpmx) */
    hpmx = (half_pi_hi - x) + half_pi_lo;               // pi/2-x
    thpmx = (three_half_pi_hi - x) + three_half_pi_lo;  // 3*pi/2 - x
    nhpmx = (-half_pi_hi - x) - half_pi_lo;             // -pi/2 - x

    /* P(hpmx*hpmx) ~= sin (hpmx) / (hpmx * (hpmx * hpmx - pi * pi)) */
    s = hpmx * hpmx;
    p =         1.32729383e-10f;
    p = p * s - 2.33177868e-8f;
    p = p * s + 2.52223435e-6f;
    p = p * s - 1.73503853e-4f;
    p = p * s + 6.62087463e-3f;
    p = p * s - 1.01321176e-1f;
    return hpmx * nhpmx * thpmx * p;
}

Ниже я показываю классический подход, который сначала сводит аргумент к [-π/4, π/4] при записи квадранта. Затем квадрант сообщает нам, нужно ли нам вычислять полиномиальное приближение к синусу или косинусу на этом интервале первичного приближения, и нужно ли нам менять знак конечного результата. Этот код предполагает, что целевая платформа поддерживает операцию FMA, указанную в IEEE-754, и что она отображается через стандартную функцию C fmaf() для одинарной точности.

Код прост, за исключением преобразования числа с плавающей запятой в целое число с режимом округления до ближайшего или четного, которое используется для вычисления квадранта, которое выполняется методом сложения магических чисел и в сочетании с умножением 2/π ( эквивалентно делению на π/2). Максимальная ошибка составляет менее 1,5 ulps.

/* compute cosine on [0, PI] with maximum error of 1.429027 ulp */
float my_cosf (float a)
{
    const float half_pi_hi =  1.57079637e+0f; //  0x1.921fb6p+0
    const float half_pi_lo = -4.37113883e-8f; // -0x1.777a5cp-25
    float c, j, r, s, sa, t;
    int i;

    /* subtract closest multiple of pi/2 giving reduced argument and quadrant */
    j = fmaf (a, 6.36619747e-1f, 12582912.f) - 12582912.f; // 2/pi, 1.5 * 2**23
    a = fmaf (j, -half_pi_hi, a);
    a = fmaf (j, -half_pi_lo, a);

    /* phase shift of pi/2 (one quadrant) for cosine */
    i = (int)j;
    i = i + 1;

    sa = a * a;
    /* Approximate cosine on [-PI/4,+PI/4] with maximum error of 0.87444 ulp */
    c =               2.44677067e-5f;  //  0x1.9a8000p-16
    c = fmaf (c, sa, -1.38877297e-3f); // -0x1.6c0efap-10
    c = fmaf (c, sa,  4.16666567e-2f); //  0x1.555550p-5
    c = fmaf (c, sa, -5.00000000e-1f); // -0x1.000000p-1
    c = fmaf (c, sa,  1.00000000e+0f); //  1.00000000p+0
    /* Approximate sine on [-PI/4,+PI/4] with maximum error of 0.64196 ulp */
    s =               2.86567956e-6f;  //  0x1.80a000p-19
    s = fmaf (s, sa, -1.98559923e-4f); // -0x1.a0690cp-13
    s = fmaf (s, sa,  8.33338592e-3f); //  0x1.111182p-7
    s = fmaf (s, sa, -1.66666672e-1f); // -0x1.555556p-3
    t = a * sa;
    s = fmaf (s, t, a);

    /* select sine approximation or cosine approximation based on quadrant */
    r = (i & 1) ? c : s;
    /* adjust sign based on quadrant */
    r = (i & 2) ? (0.0f - r) : r;

    return r;
}

Как оказалось, в данном конкретном случае использование FMA дает лишь незначительное преимущество с точки зрения точности. Если я заменю вызовы fmaf(a,b,c) на ((a)*(b)+(c)), максимальная ошибка увеличится минимально до 1,451367 ulps, то есть останется ниже 1,5 ulps.

person njuffa    schedule 17.09.2020
comment
Большое спасибо! Мне действительно нужно проверить, обеспечивает ли оборудование FMA. В вашей реализации используется та же идея, о которой упоминал ericpostpischil. На самом деле я очень впечатлен тем, как эти несколько строк кода дают такой результат. Очень интересно и при этом так просто. Я уже протестировал ваш первый подход, он отлично работает примерно с 30 инструкциями, что действительно приятно. теперь я собираюсь реализовать ваше второе решение. Небольшое преимущество fma хорошо подходит, поскольку у меня нет доступной стандартной математической библиотеки .... Есть ли конкретная причина для круглых скобок вокруг каждого значения (a), (b) - person Dexter S; 17.09.2020
comment
@DexterS В скобках: просто вырежьте и вставьте из определения макроса: #define fmaf(a,b,c) ((a)*(b)+(c)). Методы невычисленных сумм и разделенных констант для повышения эффективной точности промежуточных вычислений существуют с 1970-х годов и предшествуют началу моей профессиональной деятельности. Авторами оригинала являются Кахан, Деккер, Коди/Уэйт. - person njuffa; 17.09.2020
comment
Только что проверил ваш второй подход, он отлично работает. Я приму ваш ответ, спасибо! если я не хочу использовать макросы, я могу жестко запрограммировать их, например, j = ((a)* (6.36619747e-1f) +(12582912.f)) - 12582912.f; , верно? - person Dexter S; 17.09.2020
comment
@DexterS Абсолютно. Я бы не рекомендовал использовать макросы для переопределения стандартных функций C в производственном коде. Поскольку вы новичок, я упомяну, что считается хорошим тоном подождать не менее 24 часов, прежде чем принять ответ, чтобы предоставить поставщикам ответов из всех часовых поясов равную возможность внести свой вклад. - person njuffa; 17.09.2020
comment
Спасибо. хорошо, я подожду с принятием ответа. У меня есть еще два вопроса, просто чтобы я понял алгоритм: Вы упомянули, что можно легко вычислить алгоритм Ремеза, используя, например, кленовый минимакс. Таким образом, идея всегда состоит в том, чтобы сначала получить алгоритм с двойной точностью, а на втором этапе преобразовать уравнение в одинарную точность, разделив константы? а как ((a)*(b)+(c)) /= (a*b+c) ? - person Dexter S; 17.09.2020
comment
@DexterS В расширениях макросов рекомендуется заключать все экземпляры аргументов макроса в круглые скобки, чтобы избежать неприятных сюрпризов из-за приоритета оператора. Алгоритм Ремеза — это столетний алгоритм вычисления минимаксных приближений (обычно полиномов, но возможно расширение до рациональных функций). Maple, Mathematics и бесплатный инструмент Sollya имеют встроенные средства для создания таких минимаксных приближений, например. Команда Солля fpminimax. Константы разделения: например. double pi =3.14159265358979323; float pi_hi = (float)pi; float pi_lo = (float)(pi- (double)pi_hi); - person njuffa; 17.09.2020
comment
@njuffla Хорошо. у меня есть несколько вопросов, вытекающих из этой темы, касающихся расчета максимального значения ulp, сокращения аргументов, перевода для синуса ..... как лучше всего решить эти вопросы? кажется, что комментарии не являются правильным способом сделать это.... - person Dexter S; 17.09.2020
comment
@DexterS Правильно, мы уже злоупотребляли комментариями здесь. Это не сайт для учебных пособий или обсуждения темы. Этот сайт предназначен для конкретных вопросов по теме, на которые есть конкретные ответы, основанные на фактах, а не на мнениях. - person njuffa; 17.09.2020
comment
Re «Конечно, можно вычислить косинус на [0, π] с любой желаемой границей ошибки ›= 0,5 ulp, используя только собственные операции точности»: Технически верно; косинус можно вычислить, но может быть полезно уточнить это, если «исходная точность», к которой он предназначен, — это любая точность, для которой кто-то может спроектировать машину. Я не уверен, в какой степени люди доказали, что его можно вычислить с известной фиксированной границей времени выполнения. Я думаю, что Crlibm сделал это для двоичных файлов 32 и 64, но не более того? Таким образом, для реализации более высокой точности может потребоваться цикл с неизвестной границей. - person Eric Postpischil; 18.09.2020
comment
@njuffa Я все еще очень доволен вашим решением. Я хочу подробно разобраться в первом алгоритме, и, читая уже несколько раз ваше объяснение, я так и не понял всего расчета: я понимаю расщепление пи/2 и переход от х к пи/2-х для косинус. Я предполагаю, что вычисление p является результатом упомянутого вами минимаксного приближения. Но откуда последняя строчка? Поскольку первоисточник до сих пор недоступен, я был бы признателен, если бы вы могли объяснить алгоритм немного подробнее. Я также думаю, что другим пользователям могут быть полезны ваши знания. - person Dexter S; 22.09.2020

Я вижу, что у @njuffa есть хороший подход, но я хочу предложить другой подход:

  • Угол, скорее всего, изначально указан в градусах, а не в радианах, и воспользуйтесь этим.
  • Не зависит от того, является ли float IEEE.
  • fma может быть слабым и поэтому не использовать его.

Выполните сокращение диапазона, используя целочисленную математику, затем найдите ответ с помощью самонастраивающегося ряда Тейлора.

#include <assert.h>

static float my_sinf_helper(float xx, float term, unsigned n) {
  if (term + 1.0f == 1.0f) {
    return term;
  }
  return term - my_sinf_helper(xx, xx * term / ((n + 1) * (n + 2)), n + 2);
}

static float my_cosf_helper(float xx, float term, unsigned n) {
  if (term + 1.0f == 1.0f) {
    return term;
  }
  return term - xx * my_cosf_helper(xx, term / ((n + 1) * (n + 2)), n + 2);
}

// valid for [-pi/4 + pi/4]
static float my_sinf_primary(float x) {
  return x * my_sinf_helper(x * x, 1.0, 1);
}

// valid for [-pi/4 + pi/4]
static float my_cosf_primary(float x) {
  return my_cosf_helper(x * x, 1.0, 0);
}

#define MY_PIf 3.1415926535897932384626433832795f
#define D2Rf(d) ((d)*(MY_PIf/180))

float my_cosdf(float x) {
  if (x < 0) {x = -x;}
  unsigned long long ux = (unsigned long long) x;
  x -= (float) ux;
  unsigned ux_primary = ux % 360u;
  int uxq = ux_primary%90;
  if (uxq >= 45) uxq -= 90;
  x += uxq;
  switch (ux_primary/45) {
    case 7: //
    case 0: return my_cosf_primary(D2Rf(x));
    case 1: //
    case 2: return -my_sinf_primary(D2Rf(x));
    case 3: //
    case 4: return -my_cosf_primary(D2Rf(x));
    case 5: //
    case 6: return my_sinf_primary(D2Rf(x));
  }
  assert(0);
  return 0;
}

Тестовый код

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#define DBL_FMT "%+24.17e"

typedef struct {
  double x, y0, y1, adiff;
  unsigned n;
} test;

test worst = {0};

int my_cosd_test(float x) {
  test t;
  t.x = x;
  t.y0 = cos(x*acos(-1)/180);
  t.y1 = my_cosdf(x);
  t.adiff = fabs(t.y1 - t.y0);
  if (t.adiff > worst.adiff) {
    t.n = worst.n + 1;
    printf("n:%3u x:" DBL_FMT " y0:" DBL_FMT " y1:" DBL_FMT " d:" DBL_FMT "\n", //
        t.n, t.x, t.y0, t.y1, t.adiff);
    fflush(stdout);
    worst = t;
    if (t.n > 100)
      exit(-1);
  }
  return t.adiff != 0.0;
}

float rand_float_finite(void) {
  union {
    float f;
    unsigned char uc[sizeof(float)];
  } u;
  do {
    for (size_t i = 0; i < sizeof u.uc / sizeof u.uc[0]; i++) {
      u.uc[i] = (unsigned char) rand();
    }
  } while (!isfinite(u.f) || fabs(u.f) > 5000);
  return u.f;
}

int my_cosd_tests(unsigned n) {
  my_cosd_test(0.0);
  for (unsigned i = 0; i < n; i++) {
    my_cosd_test(rand_float_finite());
  }
  return 0;
}

int main(void) {
  my_cosd_tests(1000000);
}

Худшая ошибка броска: +8.2e-08. Примечание о максимальной глубине рекурсии: 6.

n: 14 x:+3.64442993164062500e+03 y0:+7.14107074054115110e-01 y1:+7.14107155799865723e-01 d:+8.17457506130381262e-08

Я рассмотрю больше позже. Я вижу более обширное тестирование, достигающее ошибки наихудшего случая 9e-08 и некоторых проблем TBD с x > about 1e10.

person chux - Reinstate Monica    schedule 17.09.2020