Реализация CORDIC Arcsine не выполняется

Недавно я реализовал библиотеку функций CORDIC, чтобы уменьшить требуемую вычислительную мощность (мой проект основан на PowerPC и очень строгий по своим спецификациям времени выполнения). Язык - это ANSI-C.

Другие функции (sin/cos/atan) работают с точностью до пределов точности как в 32, так и в 64-битных реализациях.

К сожалению, функция asin() систематически не работает для определенных входов.

В целях тестирования я применил файл .h, который будет использоваться в симуляционной S-функции. (Это только для моего удобства, вы можете скомпилировать следующее как самостоятельное .exe с минимальными изменениями)

Примечание. Я заставил 32 итерации, потому что я работаю с 32-битной точностью и требуется максимально возможная точность.

Cordic.h:

#include <stdio.h>
#include <stdlib.h>

#define FLOAT32 float
#define INT32 signed long int
#define BIT_XOR ^

#define CORDIC_1K_32 0x26DD3B6A                  
#define MUL_32       1073741824.0F     /*needed to scale float -> int*/            
#define INV_MUL_32   9.313225746E-10F  /*needed to scale int -> float*/

INT32 CORDIC_CTAB_32 [] = {0x3243f6a8, 0x1dac6705, 0x0fadbafc, 0x07f56ea6, 0x03feab76, 0x01ffd55b, 0x00fffaaa, 0x007fff55, 
                           0x003fffea, 0x001ffffd, 0x000fffff, 0x0007ffff, 0x0003ffff, 0x0001ffff, 0x0000ffff, 0x00007fff, 
                           0x00003fff, 0x00001fff, 0x00000fff, 0x000007ff, 0x000003ff, 0x000001ff, 0x000000ff, 0x0000007f, 
                           0x0000003f, 0x0000001f, 0x0000000f, 0x00000008, 0x00000004, 0x00000002, 0x00000001, 0x00000000};

/* CORDIC Arcsine Core: vectoring mode */
INT32 CORDIC_asin(INT32 arc_in)
{
  INT32 k;
  INT32 d;
  INT32 tx;
  INT32 ty;
  INT32 x;
  INT32 y;
  INT32 z;

  x=CORDIC_1K_32;
  y=0;
  z=0;

  for (k=0; k<32; ++k)
  {
    d = (arc_in - y)>>(31);
    tx = x - (((y>>k) BIT_XOR d) - d);
    ty = y + (((x>>k) BIT_XOR d) - d);
    z += ((CORDIC_CTAB_32[k] BIT_XOR d) - d);

    x = tx; 
    y = ty; 
  }  
  return z; 
}

/* Wrapper function for scaling in-out of cordic core*/
FLOAT32 asin_wrap(FLOAT32 arc)
{
  return ((FLOAT32)(CORDIC_asin((INT32)(arc*MUL_32))*INV_MUL_32));
}

Это можно вызвать аналогично:

#include "Cordic.h"
#include "math.h"

void main()
{
    y1 = asin_wrap(value_32); /*my implementation*/
    y2 = asinf(value_32);     /*standard math.h for comparison*/
}

Результаты показаны так: enter image description here

В верхнем левом углу находится вкладка [-1;1] более чем на 2000 шагов (0.001), в нижней части слева выводится моя функция, внизу справа - стандартный вывод, а верхний правый - разность двух выходов.

Немедленно увидеть, что ошибка не соответствует 32-битной точности.

Я проанализировал выполненные шаги (и промежуточные результаты) по моему коду, и мне кажется, что в какой-то момент значение y "достаточно близко" к начальному значению arc_in, и что может быть связанными с бит-сдвигом, приводит к расходимости решения.


Мои вопросы:

  • Я в недоумении, это ошибка, присущая реализации CORDIC, или я допустил ошибку в реализации? Я ожидал снижения точности вблизи экстремумов, но эти всплески в середине довольно неожиданны. (наиболее заметные из них находятся за пределами +/- 0.6, но даже удалены они больше при меньших значениях, хотя и не столь выражены)
  • Если это какая-то часть реализации CORDIC, существуют ли известные способы обхода?

EDIT:

  • Так как некоторые комментарии упоминают об этом, да, я тестировал определение INT32, даже записывая #define INT32 int32_T не изменяет результаты по малейшей сумме.

  • Время вычисления на целевом оборудовании было измерено сотнями повторений блока из 10.000 итераций функции со случайным входом в диапазоне допустимости. Наблюдаемые средние результаты (для одного вызова функции) заключаются в следующем: math.h asinf() 100.00 microseconds CORDIC asin() 5.15 microseconds
    (по-видимому, предыдущий тест был неисправен, новый кросс-тест получил не лучше, чем в среднем 100 микросекунд в диапазоне допустимости)

  • Я, по-видимому, нашел лучшую реализацию. Его можно загрузить в версии matlab здесь и в C здесь. Я буду более подробно анализировать его внутренние работы и сообщать позже.

Ответ 1

Чтобы рассмотреть несколько вещей, упомянутых в комментариях:

  • Данный код выводит значения, идентичные другим реализация CORDIC. Это включает в себя указанные неточности.
  • Самая большая ошибка при приближении к arcsin(1).
  • Вторая по величине ошибка заключается в том, что значения от arcsin(0.60726) до arcsin(0.68514) все возвращают 0.754805.
  • Есть некоторые неопределенные ссылки на неточности в методе CORDIC для некоторых функций, включая arcsin. Данное решение должно выполнять "двойные итерации", хотя мне не удалось заставить его работать (все значения дают большую ошибку).
  • альтернативная реализация CORDIC имеет комментарий /* |a| < 0.98 */ в реализации arcsin(), который, похоже, усиливает то, что существуют известные неточности до 1.

В качестве грубого сравнения нескольких разных методов рассмотрим следующие результаты (все тесты, выполненные на настольном компьютере Windows7 с использованием MSVС++ 2010, тесты, рассчитанные с использованием 10M итераций в диапазоне arcsin() 0-1):

  • Вопрос Код CORDIC: 1050 мс, ошибка 0.008 avg, ошибка 0.173 max.
  • Альтернативный код CORDIC (ref): 2600 мс, ошибка 0.008 avg, ошибка 0.173 max
  • atan() Код CORDIC: 2900 мс, ошибка 0.21 avg, ошибка 0.28 max
  • CORDIC Использование двойных итераций: 4700 мс, ошибка 0.26 avg, максимальная ошибка 0.917 (???)
  • Математика Встроенный asin(): 200 мс, 0 avg error, 0 max error
  • Рациональная аппроксимация (ref): 250 мс, ошибка 0.21 avg, ошибка 0,26 max
  • Линейный просмотр таблицы (см. ниже) 100 мс, 0.000001 средняя ошибка, 0.00003 максимальная ошибка
  • Серия Тейлора (7-я степень, ref): 300 мс, ошибка 0.01, ошибка 0.16 max

Эти результаты находятся на рабочем столе, поэтому, насколько они актуальны для встроенной системы, это хороший вопрос. Если у вас есть сомнения, рекомендуется профилирование/бенчмаркинг соответствующей системы. Большинство тестируемых решений не имеют очень хорошей точности в диапазоне (0-1), и все, кроме одного, на самом деле медленнее, чем встроенная функция asin().

Ниже приведен порядок поиска линейных таблиц и является моим обычным методом для любой дорогостоящей математической функции, когда требуется высокая скорость. Он просто использует таблицу с 1024 элементами с линейной интерполяцией. Кажется, это самый быстрый и самый точный из всех проверенных методов, хотя встроенный asin() не намного медленнее (проверьте его!). Его можно легко настроить для большей или меньшей точности, изменив размер таблицы.

// Please test this code before using in anything important!
const size_t ASIN_TABLE_SIZE = 1024;
double asin_table[ASIN_TABLE_SIZE];

int init_asin_table (void)
{
    for (size_t i = 0; i < ASIN_TABLE_SIZE; ++i)
    {
        float f = (float) i / ASIN_TABLE_SIZE;
        asin_table[i] = asin(f);
    }    

    return 0;
}

double asin_table (double a)
{
    static int s_Init = init_asin_table(); // Call automatically the first time or call it manually
    double sign = 1.0;

    if (a < 0) 
    {
        a = -a;
        sign = -1.0;
    }

    if (a > 1) return 0;

    double fi = a * ASIN_TABLE_SIZE;
    double decimal = fi - (int)fi;

    size_t i = fi;
    if (i >= ASIN_TABLE_SIZE-1) return Sign * 3.14159265359/2;

    return Sign * ((1.0 - decimal)*asin_table[i] + decimal*asin_table[i+1]);
}

Ответ 2

Арксина "с одним вращением" плохо ошибочна, если аргумент просто больше начального значения "x", где это магический масштабный коэффициент - 1/An ~ = 0.607252935 ~ = 0x26DD3B6A.

Это потому, что для всех аргументов > 0 первый шаг всегда имеет y = 0 < arg, поэтому d = +1, который устанавливает y = 1/An и оставляет x = 1/An. Глядя на второй шаг:

  • если arg <= 1/An, то d = -1, а следующие шаги сходятся к хорошему ответу

  • если arg > 1/An, тогда d = +1, и этот шаг отходит от правильного ответа, а для диапазона значений немного больше, чем 1/An, последующие шаги все имеют d = -1, но не могут исправить результат: - (

Я нашел:

 arg = 0.607 (ie 0x26D91687), relative error 7.139E-09 -- OK    
 arg = 0.608 (ie 0x26E978D5), relative error 1.550E-01 -- APALLING !!
 arg = 0.685 (ie 0x2BD70A3D), relative error 2.667E-04 -- BAD !!
 arg = 0.686 (ie 0x2BE76C8B), relative error 1.232E-09 -- OK, again

Описания метода предупреждают об abs (arg) >= 0.98 (или так), и я обнаружил, что где-то после 0.986 процесс не сходится, и относительная ошибка перескакивает до ~ 5E-02 и достигает 1E-01 (!!) при arg = 1: - (

Как и вы, я также обнаружил, что для 0.303 < arg < 0.313 относительная ошибка переходит в ~ 3E-02 и медленно уменьшается до тех пор, пока все не вернется в нормальное состояние. (В этом случае шаг 2 до сих пор выходит за пределы того, что остальные шаги не могут его исправить.)

Итак... одиночный поворот CORDIC для арксина выглядит мусором для меня: - (


Добавлено позже... когда я посмотрел еще ближе на одиночный поворот CORDIC, я нашел еще много небольших областей, где относительная ошибка BAD...

... поэтому я бы не стал касаться этого как метода вообще... это не просто мусор, он бесполезный.


BTW: Я полностью рекомендую "Руководство по программному обеспечению для элементарных функций", Уильям Коди и Уильям Уэйт, Prentice-Hall, 1980. Методы вычисления функций уже не так интересны (но есть основательная, практическая дискуссия требуемых соответствующих диапазонов). Однако для каждой функции они дают хорошую процедуру тестирования.

Ответ 3

дополнительный источник Я связал в конце вопроса, по-видимому, содержащее решение.

Предлагаемый код можно свести к следующему:

#define M_PI_2_32    1.57079632F
#define SQRT2_2      7.071067811865476e-001F /* sin(45°) = cos(45°) = sqrt(2)/2 */

FLOAT32 angles[] = {
    7.8539816339744830962E-01F, 4.6364760900080611621E-01F, 2.4497866312686415417E-01F, 1.2435499454676143503E-01F,
    6.2418809995957348474E-02F, 3.1239833430268276254E-02F, 1.5623728620476830803E-02F, 7.8123410601011112965E-03F,
    3.9062301319669718276E-03F, 1.9531225164788186851E-03F, 9.7656218955931943040E-04F, 4.8828121119489827547E-04F,
    2.4414062014936176402E-04F, 1.2207031189367020424E-04F, 6.1035156174208775022E-05F, 3.0517578115526096862E-05F,
    1.5258789061315762107E-05F, 7.6293945311019702634E-06F, 3.8146972656064962829E-06F, 1.9073486328101870354E-06F,
    9.5367431640596087942E-07F, 4.7683715820308885993E-07F, 2.3841857910155798249E-07F, 1.1920928955078068531E-07F,
    5.9604644775390554414E-08F, 2.9802322387695303677E-08F, 1.4901161193847655147E-08F, 7.4505805969238279871E-09F,
    3.7252902984619140453E-09F, 1.8626451492309570291E-09F, 9.3132257461547851536E-10F, 4.6566128730773925778E-10F};

FLOAT32 arcsin_cordic(FLOAT32 t)
{            
    INT32 i;
    INT32 j;
    INT32 flip;
    FLOAT32 poweroftwo;
    FLOAT32 sigma;
    FLOAT32 sign_or;
    FLOAT32 theta;
    FLOAT32 x1;
    FLOAT32 x2;
    FLOAT32 y1;
    FLOAT32 y2;

    flip       = 0; 
    theta      = 0.0F;
    x1         = 1.0F;
    y1         = 0.0F;
    poweroftwo = 1.0F;

    /* If the angle is small, use the small angle approximation */
    if ((t >= -0.002F) && (t <= 0.002F))
    {
        return t;
    }

    if (t >= 0.0F) 
    {
        sign_or = 1.0F;
    }
    else
    {
        sign_or = -1.0F;
    }

    /* The inv_sqrt() is the famous Fast Inverse Square Root from the Quake 3 engine
       here used with 3 (!!) Newton iterations */
    if ((t >= SQRT2_2) || (t <= -SQRT2_2))
    {
        t =  1.0F/inv_sqrt(1-t*t);
        flip = 1;
    }

    if (t>=0.0F) 
    {
        sign_or = 1.0F;
    }
    else
    {
        sign_or = -1.0F;
    }

    for ( j = 0; j < 32; j++ ) 
    {
        if (y1 > t)
        {
            sigma = -1.0F;
        }
        else
        {
            sigma = 1.0F;
        }

        /* Here a double iteration is done */
        x2 =                       x1  - (sigma * poweroftwo * y1);
        y2 = (sigma * poweroftwo * x1) +                       y1;

        x1 =                       x2  - (sigma * poweroftwo * y2);
        y1 = (sigma * poweroftwo * x2) +                       y2;

        theta  += 2.0F * sigma * angles[j];

        t *= (1.0F + poweroftwo * poweroftwo);

        poweroftwo *= 0.5F;
    }

    /* Remove bias */
    theta -= sign_or*4.85E-8F;

    if (flip)
    {
        theta = sign_or*(M_PI_2_32-theta);
    }

    return theta;
}

Следует отметить следующее:

  • Это реализация CORDIC с двойной итерацией.
  • Таким образом, таблица angles отличается конструкцией от старой таблицы.
  • И вычисление выполняется в нотации с плавающей запятой, это приведет к значительному увеличению времени вычисления на целевом оборудовании.
  • Небольшое смещение присутствует на выходе, удаляемом через проход theta -= sign_or*4.85E-8F;.

На следующем рисунке показаны абсолютные (левые) и относительные ошибки (справа) старой реализации (вверху) против реализации, содержащейся в этом ответе (внизу).

Относительная ошибка получается только путем деления выхода CORDIC на выход встроенной реализации math.h. По этой причине она построена вокруг 1, а не 0.

Пиковая относительная ошибка (при отсутствии деления на ноль) равна 1.0728836e-006.

Средняя относительная ошибка 2.0253509e-007 (почти в соответствии с 32-разрядной точностью).

enter image description here