Ответить на комментарий

Умножая вычеты

Август 19, 2017 — Шарахов А.П.

В известном алгоритме замены деления умножением произведение имеет двойную длину. Существует ли вариант с "коротким" произведением?

Молчит наука

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

Полезные факты

Используя операции сдвига, умножение и деление на четные числа в арифметике по модулю 2^N можно свести к умножению и делению на нечетные числа, для которых существует обратный по умножению элемент (modular multiplicative inverse).

Из того, что для взаимно обратных чисел M и W выполняется равенство M*W=W*M=1 (mod 2^N) следует, что операция квазиделения Y на M, т.е. умножения Y на W=M^-1, взаимно однозначна.

Очевидно, что если заранее известно, что Y делится на M (Y=X*M), то для нахождения частного X можно использовать квазиделение: Y*W=X*M*W=X.

В каждом умножении есть доля деления

Давайте посмотрим, как ведут себя при умножении на обратный элемент W, числа, дающие при делении на M одинаковый остаток R:
(0*M+R)*W=0*M*W+R*W=R*W+0=S(R)+0
(1*M+R)*W=1*M*W+R*W=R*W+1=S(R)+1
(2*M+R)*W=2*M*W+R*W=R*W+2=S(R)+2

Легко заметить, что произведения занимают все целые точки некоторого диапазона значений, начиная с некоторого числа S(R). Причем вследствие взаимной однозначности умножения диапазоны значений произведений, соответствующие разным значениям остатка, не пересекаются, т.е. весь дипазон [0..2^N-1] разбивается на M непересекающихся поддиапазонов примерно равной длины. Заметим также, что первый поддиапазон начинается с S(0)=0.

Назовем произведение остатка на обратный элемент следом остатка S(R)=R*W. Зная величину следа остатка S(R), мы могли бы найти частное вычитанием:
пусть Y=X*M+R, тогда Y*W=X*M*W+R*W=X+R*W=X+S(R), X=Y*W-S(R)

Гадая на остатках

Пусть длина делителя M равна B бит. Тогда, очевидно, длина следа ненулевого остатка должна превышать N-B, иначе в нулевом поддиапазоне окажется слишком мало чисел. То же самое справедливо для любого поддиапазона. Следовательно, старшие B бит любой пары следов различных остатков различны.

Это значит, что, заранее вычислив следы всех остатков, мы могли бы угадать след любого остатка по его старшим B битам. Но проблема в том, что старшие B бит результата квазиделения могут не совпадать со старшими битами следа, например, из-за переноса.

Элегантно избавиться от этой проблемы не удается, поэтому мы просто уменьшим в два раза максимальное допустимое значение делимого и будем угадывать след остатка по его старшим B+1 битам. С одной стороны, это ограничение может добавить в алгоритм деления лишнюю проверку для нечетного делителя. С другой стороны, т.к. теперь результаты квазиделения покрывают нижнюю половину каждого поддиапазона, то старшие B+1 бит могут совпасть только у результатов, соответствующих следам одинаковых остатков.

Алгоритм и примеры

Сначала давайте посмотрим примеры деления с остатком на 10 (проверка переполнения должна быть отключена):

function ShaDiv10(ADividend: cardinal): cardinal;
const
  Shift= 1;
  Inverse= $CCCCCCCD;
  ToIndex= 28;
  Stamp: array[0..15] of cardinal= ($00000000, $00000000, $00000001, $33333334,
                                    $33333334, $00000001, $66666667, $66666667,
                                    $66666667, $9999999A, $9999999A, $9999999A,
                                    $CCCCCCCD, $CCCCCCCD, $CCCCCCCD, $00000001);
begin;
  Result:=ADividend shr Shift * Inverse;
  Result:=Result - Stamp[Result shr ToIndex];
  end;
 
function ShaMod10(ADividend: cardinal): cardinal;
const
  Shift= 1;
  Inverse= $CCCCCCCD;
  ToIndex= 28;
  Rest: array[0..15] of cardinal= (0, 0, 10, 8, 8, 10, 6, 6, 6, 4, 4, 4, 2, 2, 2, 10);
begin;
  Result:=Rest[ADividend shr Shift * Inverse shr ToIndex] + ADividend and (1 shl Shift - 1);
  end;
 
function ShaDivMod10(ADividend: cardinal; var ARest: cardinal): cardinal;
const
  Shift= 1;
  Inverse= $CCCCCCCD;
  ToIndex= 28;
  Stamp: array[0..15] of cardinal= ($00000000, $00000000, $00000001, $33333334,
                                    $33333334, $00000001, $66666667, $66666667,
                                    $66666667, $9999999A, $9999999A, $9999999A,
                                    $CCCCCCCD, $CCCCCCCD, $CCCCCCCD, $00000001);
  Rest: array[0..15] of cardinal= (0, 0, 10, 8, 8, 10, 6, 6, 6, 4, 4, 4, 2, 2, 2, 10);
var
  i: cardinal;
begin;
  Result:=ADividend shr Shift * Inverse;
  i:=Result shr ToIndex;
  Result:=Result - Stamp[i];
  ARest:=Rest[i] + ADividend and (1 shl Shift - 1);
  end;

и на 100:

function ShaDivMod100(ADividend: cardinal; var ARest: cardinal): cardinal;
const
  Shift= 2;
  Inverse= $C28F5C29;
  ToIndex= 26;
  Stamp: array[0..63] of cardinal= (
    $00000000, $00000000, $0A3D70A4, $0A3D70A4, $00000001, $147AE148, $147AE148, $1EB851EC,
    $1EB851EC, $00000001, $28F5C290, $28F5C290, $33333334, $33333334, $33333334, $3D70A3D8,
    $3D70A3D8, $47AE147B, $47AE147B, $47AE147B, $51EB851F, $51EB851F, $00000001, $5C28F5C3,
    $5C28F5C3, $66666667, $66666667, $00000001, $70A3D70B, $70A3D70B, $7AE147AF, $7AE147AF,
    $7AE147AF, $851EB852, $851EB852, $8F5C28F6, $8F5C28F6, $8F5C28F6, $9999999A, $9999999A,
    $A3D70A3E, $A3D70A3E, $A3D70A3E, $AE147AE2, $AE147AE2, $00000001, $B851EB86, $B851EB86,
    $C28F5C29, $C28F5C29, $00000001, $CCCCCCCD, $CCCCCCCD, $D70A3D71, $D70A3D71, $D70A3D71,
    $E147AE15, $E147AE15, $EB851EB9, $EB851EB9, $EB851EB9, $F5C28F5D, $F5C28F5D, $00000001);
  Rest: array[0..63] of cardinal= (
     0,  0, 16, 16,100, 32, 32, 48, 48,100, 64, 64, 80, 80, 80, 96,
    96, 12, 12, 12, 28, 28,100, 44, 44, 60, 60,100, 76, 76, 92, 92,
    92,  8,  8, 24, 24, 24, 40, 40, 56, 56, 56, 72, 72,100, 88, 88,
     4,  4,100, 20, 20, 36, 36, 36, 52, 52, 68, 68, 68, 84, 84,100);
var
  i: cardinal;
begin;
  Result:=ADividend shr Shift * Inverse;
  i:=Result shr ToIndex;
  Result:=Result - Stamp[i];
  ARest:=Rest[i] + ADividend and (1 shl Shift - 1);
  end;

Инициализация и деление 32-битных чисел в общем виде:

type
  TSlotRecord= record
    Stamp: cardinal;
    Rest: cardinal;
    end;
  TDivisorRecord= record
    Divisor: cardinal;
    Shift: cardinal;
    Mask: cardinal;
    Inverse: cardinal;
    Quot32: cardinal;
    Num32: cardinal;
    ToIndex: cardinal;
    Slots: array of TSlotRecord;
    end;
  PDivisorRecord= ^TDivisorRecord;
 
procedure InitDivisor(p: PDivisorRecord; ADivisor: cardinal);
const
  cMaxDividend= $7FFFFFFF;
  cMaxDivisor=  $000FFFFF;
var
  i, j: integer;
  c, m: cardinal;
begin;
  if (ADivisor<=0) or (ADivisor>cMaxDivisor) then ADivisor:=ADivisor div (ADivisor-ADivisor);
  with p^ do begin;
    Divisor:=ADivisor;
    Shift:=0; while Divisor and (1 shl Shift)=0 do inc(Shift);
    Mask:=1 shl Shift - 1;
    m:=Divisor shr Shift;
    Inverse:=1; c:=m;
    while c<>1 do begin;
      Inverse:=Inverse*c;
      c:=c*c;
      end;
    Quot32:=cMaxDividend div m + 1;
    Num32:=Quot32 * m;
    j:=1; while m>=1 shl j do inc(j);
    ToIndex:=SizeOf(Inverse)*8-j-1;
    SetLength(Slots, 1 shl (j+1));
    for j:=0 to Length(Slots)-1 do with Slots[j] do begin;
      Stamp:=1;
      Rest:=Divisor;
      end;
    for i:=0 to m-1 do begin;
      c:=Inverse*cardinal(i);
      for j:=c shr ToIndex to (c+Quot32-1) shr ToIndex do with Slots[j] do begin;
        Stamp:=c;
        Rest:=i shl Shift;
        end;
      end;
    if Divisor and 1=0 then begin;
      Quot32:=0;
      Num32:=0;
      end;
    end;
  end;
 
function ShaDivMod(p: PDivisorRecord; ADividend: cardinal; var ARest: cardinal): cardinal;
var
  c: cardinal;
begin;
  with p^ do begin;
    Result:=0;
    //need for big divisor only
    if Divisor shr (SizeOf(Divisor)*8-1)<>0 then begin;
      ARest:=ADividend;
      if ADividend>=Divisor then begin;
        dec(ARest, Divisor);
        inc(Result);
        end;
      exit;
      end;
    //need for odd divisor only
    if (Divisor and 1<>0) and (ADividend>=Num32) then begin;
      ADividend:=ADividend-Num32;
      Result:=Quot32;
      end;
    c:=ADividend shr Shift * Inverse;
    with Slots[c shr ToIndex] do begin;
      Result:=Result + c - Stamp;
      ARest:=Rest + ADividend and Mask;
      end;
    end;
  end;

Экспериментальная проверка корректности алгоритма была выполнена для типов byte, word, cardinal, uint64. В двух последних случаях проверка выполнялась только для делителей в диапазоне 1..2^20:

function Validate(TypeSize, MaxDivisor: integer): string;
var
  b, e, i, j, m, mlast, len: integer;
  n, d, x, umask, smask: int64;
  a: array of integer;
label
  err1, err2;
begin;
  if (TypeSize<>1) and (TypeSize<>2) and (TypeSize<>4) and (TypeSize<>8) then begin;
    Result:=Format('invalid type size = %d', [TypeSize]);
    exit;
    end;
  TypeSize:=TypeSize*8;
  if MaxDivisor=0 then MaxDivisor:=1 shl (TypeSize-1) - 1;
  if (MaxDivisor<3) or (MaxDivisor>1024*1024) then begin;
    Result:=Format('invalid max divisor = %d', [MaxDivisor]);
    exit;
    end;
 
  e:=0;
  umask:=int64(-1) shr (64-TypeSize);
  smask:=umask shr 1;
  mlast:=smask and MaxInt; if mlast>MaxDivisor then mlast:=MaxDivisor;
  len:=1; while len<MaxDivisor do len:=len*2;
  len:=len*2; SetLength(a, len);
 
  m:=3;
  repeat;
    d:=smask div m * m;
    n:=1; x:=m;
    while x<>1 do begin;
      n:=n*x and umask;
      x:=x*x and umask;
      end;
    b:=1; while m>=1 shl b do inc(b);
    inc(b);
    for i:=0 to 1 shl b-1 do a[i]:=m;
    for i:=0 to m-1 do begin;
      j:=n*i and umask shr (TypeSize-b);
      if a[j]<>m then goto err1;
      a[j]:=i;
      j:=n*(d+i) and umask shr (TypeSize-b);
      if (a[j]<>i) and (a[j]<>m) then goto err2;
      a[j]:=i;
      end;
    inc(m,2);
    until m>=mlast;
  Result:='done';
  exit;
err2: inc(e);
err1: inc(e);
  Result:=Format('error%d on divisor %d',[e, m]);
  end;
 
procedure TForm1.bValidate8Click(Sender: TObject);
begin;
  Memo1.Lines.Add(Validate(SizeOf(byte), 0));
  end;
 
procedure TForm1.bValidate16Click(Sender: TObject);
begin;
  Memo1.Lines.Add(Validate(SizeOf(word), 0));
  end;
 
procedure TForm1.bValidate32Click(Sender: TObject);
begin;
  Memo1.Lines.Add(Validate(SizeOf(cardinal), 64*1024));  //1024*1024
  end;
 
procedure TForm1.bValidate64Click(Sender: TObject);
begin;
  Memo1.Lines.Add(Validate(SizeOf(int64), 64*1024));     //1024*1024
  end;

Описанный алгоритм может применяться в функциях перевода числа из одной системы счисления в другую и им подобных. Например, в функции IntToStr 64-битное число, превышающее MaxInt, удобно разрезать на два фрагмента, длина младшего из которых составляет 10 десятичных цифр, а после в цикле применить квазиделение на 100.

на главную

Ответить

  • Адреса страниц и электронной почты автоматически преобразуются в ссылки.
  • Доступны HTML теги: <h1> <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Строки и параграфы переносятся автоматически.
  • You can enable syntax highlighting of source code with the following tags: <pre>, <code>, <asm>, <c>, <cpp>, <delphi>, <drupal5>, <drupal6>, <java>, <javascript>, <php>, <python>, <ruby>, <mytext>. Beside the tag style "<foo>" it is also possible to use "[foo]".

Подробнее о форматировании

CAPTCHA
Ведите текст с изображения. (вводить еще раз после предпросмотра а то не добавится комментарий)
Image CAPTCHA
Copy the characters (respecting upper/lower case) from the image.