Избегать переполнения при вычислении π путем оценки ряда с использованием 16-разрядной арифметики?
Я пытаюсь написать программу, которая рассчитывает десятичные цифры от π до 1000 цифр и более.
Чтобы практиковать низкоуровневое программирование для развлечения, конечная программа будет написана на ассемблере, на 8-битном процессоре, который не имеет умножения или деления и выполняет только 16-битные сложения. Чтобы упростить реализацию, желательно иметь возможность использовать только 16-разрядные целочисленные операции без знака и использовать итерационный алгоритм. Скорость не является серьезной проблемой. А быстрое умножение и деление выходят за рамки этого вопроса, поэтому не стоит также рассматривать эти проблемы.
Перед тем, как реализовать его в сборке, я все еще пытаюсь выяснить пригодный для использования алгоритм на C на моем настольном компьютере. До сих пор я обнаружил, что следующие серии достаточно эффективны и относительно просты в реализации.
Формула получена из ряда Лейбница с использованием метода ускорения сходимости. Чтобы получить ее, см. "Вычисление цифр в π" Карла Д. Оффнера (https://cs.umb.edu/~offner/files/pi.pdf), стр. 19-26. Окончательная формула показана на странице 26. Первоначальная формула, которую я написал, содержала некоторые опечатки, обновите страницу, чтобы увидеть фиксированную формулу. Постоянный член 2
для наибольшего члена объясняется на странице 54. В статье также описан продвинутый итерационный алгоритм, но я здесь не использовал его.
Если оценить ряд, используя много (например, 5000) терминов, можно легко получить тысячи цифр числа π, и я обнаружил, что этот ряд также легко оценить итеративно, используя этот алгоритм:
Алгоритм
- Сначала измените формулу, чтобы получить ее постоянные члены из массива.
-
Заполните массив 2, чтобы начать первую итерацию, поэтому новая формула напоминает исходную.
-
Пусть carry = 0
.
-
Начните с самого большого срока. Получите одно слагаемое (2) из массива, умножьте его на PRECISION
чтобы выполнить деление с фиксированной точкой на 2 * я + 1
, и сохраните напоминание как новый термин в массиве. Затем добавьте следующий термин. Теперь уменьшите значение i
, переходите к следующему члену, повторяйте, пока i == 1
. Наконец добавьте последний член x_0
.
-
Поскольку используется 16-битное целое число, PRECISION
равно 10
, следовательно, получаются 2 десятичных знака, но действительна только первая цифра. Сохраните вторую цифру как перенос. Показать первую цифру плюс перенос.
-
x_0
- это целое число 2, его нельзя добавлять для последовательных итераций, очистите его.
-
Перейдите к шагу 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, указанной выше):
преобразован в фиксированную точку...
//---------------------------------------------------------------------------
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 ^ что-то, чтобы превратить десятичную часть в неотъемлемую часть. Это ваша миссия, если вы примете это :)