Избегать переполнения при вычислении π путем оценки ряда с использованием 16-разрядной арифметики?

Я пытаюсь написать программу, которая рассчитывает десятичные цифры от π до 1000 цифр и более.

Чтобы практиковать низкоуровневое программирование для развлечения, конечная программа будет написана на ассемблере, на 8-битном процессоре, который не имеет умножения или деления и выполняет только 16-битные сложения. Чтобы упростить реализацию, желательно иметь возможность использовать только 16-разрядные целочисленные операции без знака и использовать итерационный алгоритм. Скорость не является серьезной проблемой. А быстрое умножение и деление выходят за рамки этого вопроса, поэтому не стоит также рассматривать эти проблемы.

Перед тем, как реализовать его в сборке, я все еще пытаюсь выяснить пригодный для использования алгоритм на C на моем настольном компьютере. До сих пор я обнаружил, что следующие серии достаточно эффективны и относительно просты в реализации.

Формула получена из ряда Лейбница с использованием метода ускорения сходимости. Чтобы получить ее, см. "Вычисление цифр в π" Карла Д. Оффнера (https://cs.umb.edu/~offner/files/pi.pdf), стр. 19-26. Окончательная формула показана на странице 26. Первоначальная формула, которую я написал, содержала некоторые опечатки, обновите страницу, чтобы увидеть фиксированную формулу. Постоянный член 2 для наибольшего члена объясняется на странице 54. В статье также описан продвинутый итерационный алгоритм, но я здесь не использовал его.

Series to Calculate π (fixed typo)

Если оценить ряд, используя много (например, 5000) терминов, можно легко получить тысячи цифр числа π, и я обнаружил, что этот ряд также легко оценить итеративно, используя этот алгоритм:

Алгоритм

  1. Сначала измените формулу, чтобы получить ее постоянные члены из массива.

Rearranged Formula (fixed another typo)

  1. Заполните массив 2, чтобы начать первую итерацию, поэтому новая формула напоминает исходную.

  2. Пусть carry = 0.

  3. Начните с самого большого срока. Получите одно слагаемое (2) из массива, умножьте его на PRECISION чтобы выполнить деление с фиксированной точкой на 2 * я + 1, и сохраните напоминание как новый термин в массиве. Затем добавьте следующий термин. Теперь уменьшите значение i, переходите к следующему члену, повторяйте, пока i == 1. Наконец добавьте последний член x_0.

  4. Поскольку используется 16-битное целое число, PRECISION равно 10, следовательно, получаются 2 десятичных знака, но действительна только первая цифра. Сохраните вторую цифру как перенос. Показать первую цифру плюс перенос.

  5. x_0 - это целое число 2, его нельзя добавлять для последовательных итераций, очистите его.

  6. Перейдите к шагу 4, чтобы вычислить следующую десятичную цифру, пока у нас не появятся все нужные цифры.

Реализация 1

Перевод этого алгоритма в C:

#include <stdio.h>
#include <stdint.h>

#define N 2160
#define PRECISION 10

uint16_t terms[N + 1] = {0};

int main(void)
{
    /* initialize the initial terms */
    for (size_t i = 0; i < N + 1; i++) {
        terms[i] = 2;
    }

    uint16_t carry = 0;
    for (size_t j = 0; j < N / 4; j++) {
        uint16_t numerator = 0;
        uint16_t denominator;
        uint16_t digit;

        for (size_t i = N; i > 0; i--) {
            numerator += terms[i] * PRECISION;
            denominator = 2 * i + 1;

            terms[i] = numerator % denominator;
            numerator /= denominator;
            numerator *= i;
        }
        numerator += terms[0] * PRECISION;
        digit = numerator / PRECISION + carry;
        carry = numerator % PRECISION;

        printf("%01u", digit);

        /* constant term 2, only needed for the first iteration. */
        terms[0] = 0;
    }
    putchar('\n');
}

Код может вычислять от π до 31 десятичного знака, пока не произойдет ошибка.

31415926535897932384626433832794
10 <-- wrong

Иногда digit + carry больше 9, поэтому требуется дополнительный перенос. Если нам очень не повезло, может быть даже двойной перенос, тройной перенос и т.д. Мы используем кольцевой буфер для хранения последних 4 цифр. Если обнаружен дополнительный перенос, мы выводим клавишу возврата, чтобы стереть предыдущую цифру, выполнить перенос и перепечатать их. Это просто уродливое решение Proof-of-Concept, которое не имеет отношения к моему вопросу о переполнении, но для полноты вот оно. Что-то лучшее будет реализовано в будущем.

Реализация 2 с повторным переносом

#include <stdio.h>
#include <stdint.h>

#define N 2160
#define PRECISION 10

#define BUF_SIZE 4

uint16_t terms[N + 1] = {0};

int main(void)
{
    /* initialize the initial terms */
    for (size_t i = 0; i < N + 1; i++) {
        terms[i] = 2;
    }

    uint16_t carry = 0;
    uint16_t digit[BUF_SIZE];
    int8_t idx = 0;

    for (size_t j = 0; j < N / 4; j++) {
        uint16_t numerator = 0;
        uint16_t denominator;

        for (size_t i = N; i > 0; i--) {
            numerator += terms[i] * PRECISION;
            denominator = 2 * i + 1;

            terms[i] = numerator % denominator;
            numerator /= denominator;
            numerator *= i;
        }
        numerator += terms[0] * PRECISION;
        digit[idx] = numerator / PRECISION + carry;

        /* over 9, needs at least one carry op. */
        if (digit[idx] > 9) {
            for (int i = 1; i <= 4; i++) {
                if (i > 3) {
                    /* allow up to 3 consecutive carry ops */
                    fprintf(stderr, "ERROR: too many carry ops!\n");
                    return 1;
                }
                /* erase a digit */
                putchar('\b');

                /* carry */
                digit[idx] -= 10;
                idx--;
                if (idx < 0) {
                    idx = BUF_SIZE - 1;
                }
                digit[idx]++;            
                if (digit[idx] < 10) {
                    /* done! reprint the digits */
                    for (int j = 0; j <= i; j++) {
                        printf("%01u", digit[idx]);
                        idx++;
                        if (idx > BUF_SIZE - 1) {
                            idx = 0;
                        }
                    }
                    break;
                }
            }
        }
        else {
            printf("%01u", digit[idx]);
        }

        carry = numerator % PRECISION;
        terms[0] = 0;

        /* put an element to the ring buffer */
        idx++;
        if (idx > BUF_SIZE - 1) {
            idx = 0;
        }
    }
    putchar('\n');
}

Отлично, теперь программа может правильно вычислить 534 цифры π, пока не сделает ошибку.

3141592653589793238462643383279502884
1971693993751058209749445923078164062
8620899862803482534211706798214808651
3282306647093844609550582231725359408
1284811174502841027019385211055596446
2294895493038196442881097566593344612
8475648233786783165271201909145648566
9234603486104543266482133936072602491
4127372458700660631558817488152092096
2829254091715364367892590360011330530
5488204665213841469519415116094330572
7036575959195309218611738193261179310
5118548074462379962749567351885752724
8912279381830119491298336733624406566
43086021394946395
22421 <-- wrong

16-разрядное целочисленное переполнение

Оказывается, при вычислении самых больших слагаемых в начале, слагаемое ошибки становится довольно большим, поскольку делители в начале находятся в диапазоне ~ 4000. При оценке ряда numerator фактически начинает сразу переполняться при умножении.

Целочисленное переполнение незначительно при вычислении первых 500 цифр, но начинает ухудшаться и ухудшаться, пока не даст неверный результат.

Изменение uint16_t numerator = 0 на uint32_t numerator = 0 может решить эту проблему и вычислить цифры от π до 1000+.

Однако, как я упоминал ранее, моей целевой платформой является 8-битный процессор, и он выполняет только 16-битные операции. Есть ли хитрость для решения проблемы переполнения 16-битного целого числа, которую я здесь вижу, используя только один или несколько uint16_t? Если невозможно избежать арифметики с множественной точностью, какой самый простой способ реализовать это здесь? Я как-то знаю, что мне нужно ввести дополнительное 16-разрядное "слово расширения", но я не уверен, как это реализовать.

И заранее спасибо за ваше терпение, чтобы понять длинный контекст здесь.

Ответы

Ответ 1

Посмотрите на соответствующий QA:

Он использует Wiki: Bailey – Borwein – Plouffe_formula, который больше подходит для целочисленной арифметики.

Реальная проблема, однако, будет:

Как вы, вероятно, хотите напечатать число в базе dec...

Также, если вам нужно нести язык более высокого уровня, чем asm, взгляните на это:

Вы можете изменить его так, чтобы он обрабатывал столько битов переноса, сколько вам нужно (если он все еще меньше ширины битов типа данных).

[Edit1] Пример BBP в C++/VCL

Я использовал эту формулу (взято со страницы Wiki, указанной выше):

BBP

преобразован в фиксированную точку...

//---------------------------------------------------------------------------
AnsiString str_hex2dec(const AnsiString &hex)
    {
    char c;
    AnsiString dec="",s;
    int i,j,l,ll,cy,val;
    int  i0,i1,i2,i3,sig;
    sig=+1; l=hex.Length();
    if (l) { c=hex[l]; if (c=='h') l--; if (c=='H') l--; }
    i0=0; i1=l; i2=0; i3=l;
    for (i=1;i<=l;i++)      // scan for parts of number
        {
        char c=hex[i];
        if (c=='-') sig=-sig;
        if ((c=='.')||(c==',')) i1=i-1;
        if ((c>='0')&&(c<='9')) { if (!i0) i0=i; if ((!i2)&&(i>i1)) i2=i; }
        if ((c>='A')&&(c<='F')) { if (!i0) i0=i; if ((!i2)&&(i>i1)) i2=i; }
        if ((c>='a')&&(c<='f')) { if (!i0) i0=i; if ((!i2)&&(i>i1)) i2=i; }
        }

    l=0; s=""; if (i0) for (i=i0;i<=i1;i++)
        {
        c=hex[i];
             if ((c>='0')&&(c<='9')) c-='0';
        else if ((c>='A')&&(c<='F')) c-='A'-10;
        else if ((c>='a')&&(c<='f')) c-='A'-10;
        for (cy=c,j=1;j<=l;j++)
            {
            val=(s[j]<<4)+cy;
            s[j]=val%10;
            cy  =val/10;
            }
        while (cy>0)
            {
            l++;
            s+=char(cy%10);
            cy/=10;
            }
        }
    if (s!="")
        {
        for (j=1;j<=l;j++) { c=s[j]; if (c<10) c+='0'; else c+='A'-10; s[j]=c; }
        for (i=l,j=1;j<i;j++,i--) { c=s[i]; s[i]=s[j]; s[j]=c; }
        dec+=s;
        }
    if (dec=="") dec="0";
    if (sig<0) dec="-"+dec;

    if (i2)
        {
        dec+='.';
        s=hex.SubString(i2,i3-i2+1);
        l=s.Length();
        for (i=1;i<=l;i++)
            {
            c=s[i];
                 if ((c>='0')&&(c<='9')) c-='0';
            else if ((c>='A')&&(c<='F')) c-='A'-10;
            else if ((c>='a')&&(c<='f')) c-='A'-10;
            s[i]=c;
            }
        ll=((l*1234)>>10);  // num of decimals to compute
        for (cy=0,i=1;i<=ll;i++)
            {
            for (cy=0,j=l;j>=1;j--)
                {
                val=s[j];
                val*=10;
                val+=cy;
                s[j]=val&15;
                cy=val>>4;
                }
            dec+=char(cy+'0');
            for (;;)
                {
                if (!l) break;;
                if (s[l]) break;
                l--;
                }
            if (!l) break;;
            }
        }

    return dec;
    }
//---------------------------------------------------------------------------
AnsiString pi_BBP() // https://en.wikipedia.org/wiki/Bailey–Borwein–Plouffe_formula
    {
    const int N=100;        // 32*N bit uint arithmetics
    int sh;
    AnsiString s;
    uint<N> pi,a,b,k,k2,k3,k4;

    for (pi=0,sh=(N<<5)-8,k=0;sh>=0;k++,sh-=4)
        {
        k2=k*k;
        k3=k2*k;
        k4=k3*k;
        a =k2* 120;
        a+=k * 151;
        a+=     47;
        b =k4* 512;
        b+=k3*1024;
        b+=k2* 712;
        b+=k * 194;
        b+=     15;
        a<<=sh;
        pi+=a/b;
        }
    pi<<=4;
    s=pi.strhex();
    s=s.Insert(".",2);
    return str_hex2dec(s);
    }
//---------------------------------------------------------------------------

В коде используется VCL AnsiString который является самораспределяющейся строкой, и мой шаблон uint<N> представляющий собой целочисленную арифметику без знака 32*N основанную на моем ALU32. Как видите, для этого нужно только сложение и умножение с большим целочисленным делением (все остальные вещи выполнимы на обычных целых числах).

Вот десятичный результат по сравнению с 1000-значным пи-кодом:

ref: 3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303819644288109756659334461284756482337867831652712019091456485669234603486104543266482133936072602491412737245870066063155881748815209209628292540917153643678925903600113305305488204665213841469519415116094330572703657595919530921861173819326117931051185480744623799627495673518857527248912279381830119491298336733624406566430860213949463952247371907021798609437027705392171762931767523846748184676694051320005681271452635608277857713427577896091736371787214684409012249534301465495853710507922796892589235420199561121290219608640344181598136297747713099605187072113499999983729780499510597317328160963185950244594553469083026425223082533446850352619311881710100031378387528865875332083814206171776691473035982534904287554687311595628638823537875937519577818577805321712268066130019278766111959092164201989
BPP: 3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196442881097566593344612847564823378678316527120190914564856692346034861045432664821339360726024914127372458700660631558817488152092096282925409171536436789259036001133053054882046652138414695194151160943305727036575959195309218611738193261179310511854807446237996274956735188575272489122793818301194912983367336244065664308602139494639522473719070217986094370277053921717629317675238467481846766940513200056812714526356082778577134275778960917363717872146844090122495343014654958537105079227968925892354201995611212902196086403441815981362977477130996051870721134999999837297804995105973173281609631859502445945534690830264252230825334468503526193118817101000313783875288658753320838142061717766914730359825349042875546873115956286388235378759375195778185778048187

Вычисленное значение bigint экспортируется в шестнадцатеричную строку, а затем преобразуется в десятичную основу с использованием str_hex2dec по ссылке выше. Количество итераций зависит от целевой битовой пропускной способности.

Код еще не оптимизирован...

Ответ 2

Как насчет реализации 32-битной арифметики?

Для сложения добавьте два старших слова (16 битов), затем два младших слова, проверьте бит переполнения и перенесите к старшему результату, если необходимо.

Если вы можете предсказать, когда произойдет переполнение, вы можете при необходимости переключиться с арифметики с 16 на 32 бита.


Проверка бита переполнения не может быть выполнена в чистом C, это потребует некоторой встроенной сборки или встроенной функции.

В противном случае вы можете быть вдохновлены этим ответом: https://codereview.stackexchange.com/a/37178/39646

Ответ 3

Есть хитрость:

Рассмотрите возможность использования массива для числителей и другого массива для знаменателей. Каждая позиция будет представлять количество раз, которое это число умножается, чтобы получить фактическое число.

Пример:

(1 * 2 * 3 * 7 * 7) / (3 * 6 * 8)

Будет представлен как:

num[] = {1, 1, 1, 0, 0, 0, 2};
denom[] = {0, 0, 1, 0, 0, 1, 0, 1};

Затем рассмотрите возможность деления каждого числа на простые числа перед сохранением, чтобы у вас были более низкие числа. Теперь вам понадобится еще один массив для хранения всех простых чисел:

primes[] = {2, 3, 5, 7};
num[] = {1, 1, 0, 2};
denom[] = {4, 2, 0, 0};

Это позволит вам хранить невообразимо большие числа, но вы рано или поздно захотите преобразовать их обратно в числа, поэтому сначала вы захотите упростить это. Способ сделать это - просто вычесть factors[i] += num[i] - denom[i] для каждого поля в массивах, для каждой дроби в ряду. Вы захотите упростить после каждой итерации, чтобы минимизировать риск переполнения.

factors[] = {-3, -1, 0, 2};

Когда вам нужно число, просто num *= pow(primes[i], factors[i]); если коэффициент положительный или num/= pow(primes, -factors[i]); если оно отрицательно, для каждого поля в массивах. (Ничего не делать, если это 0.


num и denom - это временные массивы, используемые для хранения дроби, а массив, в котором хранится результат - это factors. Не забывайте memset временные массивы перед каждым использованием.


Это объяснение полезно для любой большой фракции. Чтобы приспособить его к вашей конкретной задаче, вам может понадобиться целочисленная степенная функция, а также умножить на 10 ^ что-то, чтобы превратить десятичную часть в неотъемлемую часть. Это ваша миссия, если вы примете это :)