Выбор хороших первых оценок для дивизиона Гольдшмидта
Я вычисляю встречные обратные точки в Q22.10 с раздел Goldschmidt для использования в моем растеризаторе программного обеспечения на ARM.
Это делается путем простой установки числителя в 1, т.е. числитель становится скаляром на первой итерации. Честно говоря, я слепо слежу за алгоритмом Википедии. В статье говорится, что если знаменатель масштабируется в полуоткрытом диапазоне (0,5, 1,0), хорошая первая оценка может основываться только на знаменателе: пусть F - оценочный скаляр, а D - знаменатель, то F = 2 - Д.
Но при этом я теряю много точности. Скажите, хочу ли я найти обратную ссылку 512.00002f. Чтобы уменьшить число вниз, я теряю 10 бит точности во фракционной части, которая сдвинута. Итак, мои вопросы:
- Есть ли способ выбрать лучшую оценку, которая не требует нормализации? Зачем? Почему нет? Математическое доказательство того, почему это или невозможно, было бы здорово.
- Кроме того, можно предварительно вычислить первые оценки, чтобы ряд сходился быстрее? Сейчас он сходится после 4-й итерации в среднем. В ARM это примерно в ~ 50 циклов наихудшего случая и не учитывает эмуляцию clz/bsr, а также поиск в памяти. Если это возможно, я хотел бы знать, увеличивает ли это количество ошибок и насколько.
Вот мой тестовый файл. Примечание. Реализация программного обеспечения clz
в строке 13 из моего сообщения здесь. Вы можете заменить его внутренним, если хотите. clz
должен возвращать число начальных нулей и 32 для значения 0.
#include <stdio.h>
#include <stdint.h>
const unsigned int BASE = 22ULL;
static unsigned int divfp(unsigned int val, int* iter)
{
/* Numerator, denominator, estimate scalar and previous denominator */
unsigned long long N,D,F, DPREV;
int bitpos;
*iter = 1;
D = val;
/* Get the shift amount + is right-shift, - is left-shift. */
bitpos = 31 - clz(val) - BASE;
/* Normalize into the half-range (0.5, 1.0] */
if(0 < bitpos)
D >>= bitpos;
else
D <<= (-bitpos);
/* (FNi / FDi) == (FN(i+1) / FD(i+1)) */
/* F = 2 - D */
F = (2ULL<<BASE) - D;
/* N = F for the first iteration, because the numerator is simply 1.
So don't waste a 64-bit UMULL on a multiply with 1 */
N = F;
D = ((unsigned long long)D*F)>>BASE;
while(1){
DPREV = D;
F = (2<<(BASE)) - D;
D = ((unsigned long long)D*F)>>BASE;
/* Bail when we get the same value for two denominators in a row.
This means that the error is too small to make any further progress. */
if(D == DPREV)
break;
N = ((unsigned long long)N*F)>>BASE;
*iter = *iter + 1;
}
if(0 < bitpos)
N >>= bitpos;
else
N <<= (-bitpos);
return N;
}
int main(int argc, char* argv[])
{
double fv, fa;
int iter;
unsigned int D, result;
sscanf(argv[1], "%lf", &fv);
D = fv*(double)(1<<BASE);
result = divfp(D, &iter);
fa = (double)result / (double)(1UL << BASE);
printf("Value: %8.8lf 1/value: %8.8lf FP value: 0x%.8X\n", fv, fa, result);
printf("iteration: %d\n",iter);
return 0;
}
Ответы
Ответ 1
Я не мог удержаться от часа на вашу проблему...
Этот алгоритм описан в разделе 5.5.2 "Arithmetique des ordinateurs" Жан-Мишеля Мюллера (на французском). На самом деле это особый случай итераций Ньютона с 1 в качестве отправной точки. В книге дается простая формулировка алгоритма для вычисления N/D, причем D нормирована в диапазоне [1/2,1 [:
e = 1 - D
Q = N
repeat K times:
Q = Q * (1+e)
e = e*e
Количество правильных бит удваивается на каждой итерации. В случае 32 бит будет достаточно 4 итераций. Вы также можете выполнить итерацию до тех пор, пока e
не станет слишком мал, чтобы изменить Q
.
Нормализация используется, поскольку она обеспечивает максимальное количество значимых бит в результате. Также легче вычислить ошибку и количество итераций, необходимых, когда входы находятся в известном диапазоне.
Как только ваше входное значение нормализовано, вам не нужно беспокоиться о значении BASE, пока не получится обратное. Вы просто имеете 32-разрядное число X, нормированное в диапазоне от 0x80000000 до 0xFFFFFFFF, и вычисляете приближение Y = 2 ^ 64/X (Y не более 2 ^ 33).
Этот упрощенный алгоритм может быть реализован для вашего представления Q22.10 следующим образом:
// Fixed point inversion
// EB Apr 2010
#include <math.h>
#include <stdio.h>
// Number X is represented by integer I: X = I/2^BASE.
// We have (32-BASE) bits in integral part, and BASE bits in fractional part
#define BASE 22
typedef unsigned int uint32;
typedef unsigned long long int uint64;
// Convert FP to/from double (debug)
double toDouble(uint32 fp) { return fp/(double)(1<<BASE); }
uint32 toFP(double x) { return (int)floor(0.5+x*(1<<BASE)); }
// Return inverse of FP
uint32 inverse(uint32 fp)
{
if (fp == 0) return (uint32)-1; // invalid
// Shift FP to have the most significant bit set
int shl = 0; // normalization shift
uint32 nfp = fp; // normalized FP
while ( (nfp & 0x80000000) == 0 ) { nfp <<= 1; shl++; } // use "clz" instead
uint64 q = 0x100000000ULL; // 2^32
uint64 e = 0x100000000ULL - (uint64)nfp; // 2^32-NFP
int i;
for (i=0;i<4;i++) // iterate
{
// Both multiplications are actually
// 32x32 bits truncated to the 32 high bits
q += (q*e)>>(uint64)32;
e = (e*e)>>(uint64)32;
printf("Q=0x%llx E=0x%llx\n",q,e);
}
// Here, (Q/2^32) is the inverse of (NFP/2^32).
// We have 2^31<=NFP<2^32 and 2^32<Q<=2^33
return (uint32)(q>>(64-2*BASE-shl));
}
int main()
{
double x = 1.234567;
uint32 xx = toFP(x);
uint32 yy = inverse(xx);
double y = toDouble(yy);
printf("X=%f Y=%f X*Y=%f\n",x,y,x*y);
printf("XX=0x%08x YY=0x%08x XX*YY=0x%016llx\n",xx,yy,(uint64)xx*(uint64)yy);
}
Как отмечено в коде, умножения не заполнены 32x32- > 64 бит. E будет уменьшаться и уменьшаться и вначале помещается на 32 бита. Q всегда будет на 34 бита. Мы принимаем только 32 разряда продуктов.
Вывод 64-2*BASE-shl
оставлен в качестве упражнения для читателя:-). Если он становится 0 или отрицательным, результат не представляется (входное значение слишком мало).
ИЗМЕНИТЬ. В качестве продолжения моего комментария здесь представлена вторая версия с неявным 32-м битом в Q. И E, и Q теперь хранятся на 32 битах:
uint32 inverse2(uint32 fp)
{
if (fp == 0) return (uint32)-1; // invalid
// Shift FP to have the most significant bit set
int shl = 0; // normalization shift for FP
uint32 nfp = fp; // normalized FP
while ( (nfp & 0x80000000) == 0 ) { nfp <<= 1; shl++; } // use "clz" instead
int shr = 64-2*BASE-shl; // normalization shift for Q
if (shr <= 0) return (uint32)-1; // overflow
uint64 e = 1 + (0xFFFFFFFF ^ nfp); // 2^32-NFP, max value is 2^31
uint64 q = e; // 2^32 implicit bit, and implicit first iteration
int i;
for (i=0;i<3;i++) // iterate
{
e = (e*e)>>(uint64)32;
q += e + ((q*e)>>(uint64)32);
}
return (uint32)(q>>shr) + (1<<(32-shr)); // insert implicit bit
}
Ответ 2
Несколько идей для вас, хотя никто не решает вашу проблему напрямую, как указано.
- Почему этот алго для деления? Большинство разделов, которые я видел в ARM, используют некоторые переменные
adcs hi, den, hi, lsl #1
subcc hi, hi, den
adcs lo, lo, lo
повторяется n бит раз с двоичным поиском вне clz, чтобы определить, с чего начать. Это довольно быстро.
- Если точность является большой проблемой, вы не ограничены 32/64 бит для вашего представления с фиксированной точкой. Это будет немного медленнее, но вы можете добавить /adc или sub/sbc, чтобы перемещать значения в регистры. mul/mla также предназначены для такого рода работ.
Опять же, не прямые ответы для вас, но, возможно, несколько идей, чтобы идти вперед. Видеть фактический код ARM, вероятно, тоже поможет мне.
Ответ 3
Безумие, вы совсем не теряете точности. Когда вы делите 512.00002f на 2 ^ 10, вы просто уменьшаете показатель вашего числа с плавающей запятой на 10. Mantissa остается прежним. Конечно, если экспонент не достигнет своего минимального значения, но этого не должно произойти, так как вы масштабируетесь до (0,5, 1).
EDIT: Хорошо, вы используете фиксированную десятичную точку. В этом случае вы должны разрешить другое представление знаменателя в вашем алгоритме. Величина D равна (0,5, 1) не только в начале, но и во всем вычислении (легко доказать, что x * (2-x) < 1 для x < 1). Таким образом, вы должны представить знаменатель с десятичной точкой в базе = 32. Таким образом, вы будете иметь 32 бита точности все время.
EDIT: для этого вам придется изменить следующие строки вашего кода:
//bitpos = 31 - clz(val) - BASE;
bitpos = 31 - clz(val) - 31;
...
//F = (2ULL<<BASE) - D;
//N = F;
//D = ((unsigned long long)D*F)>>BASE;
F = -D;
N = F >> (31 - BASE);
D = ((unsigned long long)D*F)>>31;
...
//F = (2<<(BASE)) - D;
//D = ((unsigned long long)D*F)>>BASE;
F = -D;
D = ((unsigned long long)D*F)>>31;
...
//N = ((unsigned long long)N*F)>>BASE;
N = ((unsigned long long)N*F)>>31;
Кроме того, в конце вам придется сдвинуть N не по битпотам, а какое-то другое значение, которое мне слишком ленив, чтобы разобраться прямо сейчас:).