Сообщений 1    Оценка 15        Оценить  
Система Orphus

Длинная арифметика от Microsoft

Автор: Гуев Тимур Ахсарбекович
Опубликовано: 18.11.2015
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Общие понятия
BigInteger от Microsoft
Пару слов о BigInteger в Mono и Java
Производительность BigInteger
Подводя итог

Введение

Известно, что компьютер может оперировать числами, количество бит которых ограниченно. Как правило, мы привыкли работать с 32- и 64-разрядными целыми числами, которым на платформе .NET соответствуют типы Int32 (int) и Int64 (long) соответственно.

А что делать, если надо представить такое число, как, например, 29! = 8841761993739701954543616000000? Такое число не поместится ни в 64-разрядный, ни тем более в 32-разрядный тип данных. Именно для работы с такими большими числами существует длинная арифметика.

Длинная арифметика — в вычислительной технике операции (сложение, умножение, вычитание, деление, возведение в степень и т.д.) над числами, разрядность которых превышает длину машинного слова данной вычислительной машины. Эти операции реализуются не аппаратно, а программно, с использованием базовых аппаратных средства работы с числами меньших порядков.

Длинную арифметику также можно считать одним из разделов олимпиадного программирования, поскольку очень часто при решении задач разрядности стандартных типов не хватает для представления конечного результата. При выборе языка программирования для олимпиадных нужд немаловажным является встроенный в него набор средств (готовых библиотек, реализованных классов). Многие языки (Java, Ruby,Python) имеют встроенную поддержку длинной арифметики, что в разы может сократить время написания программы.

Платформа .NET вплоть до версии 4.0 не имела встроенной поддержки работы с длинными числами. В версии 4.0 .NET обзавелась поддержкой не только длинных, но и комплексных чисел. Эта функциональность доступна через сборку System.Numerics и типы BigInteger и Complex, определенные в одноимённом с названием сборки пространстве имён.

Следует сказать, что структура BigInteger должна была появиться ещё в .NET 3.5, однако на тот момент она не была полностью готова, её реализация не отвечала всем потребностям (сюда можно отнести и проблемы производительности), поэтому было принято решение отложить её выход до .NET 4.0.

Общие понятия

Идея представления длинных чисел в памяти компьютера достаточно проста. Рассмотрим число 123456789 в десятеричной системе счисления. Очевидно, его можно представить следующим образом:

12345678910 = 1*108 + 2*107 + 3*106 + 4*105 + 5*104 + 6*103 + 7*102 + 8*101 + 9*100

В общем случае, любое число можно представить в виде:

A = a n-1 β n-1 + a n-2 β n-2 +…+a 1 β + a 0

где β – основание системы счисления, в которой мы представляем число, а коэффициенты ai удовлетворяют двойному неравенству 0 ≤ ai < β.

Представление числа напоминает представление многочлена, только вместо x в соответствующей степени имеем основание β в нужной степени. Как известно, многочлен a0 + a1x + a2x2 + … + anxn удобно представлять в виде массива, элементы которого представляют коэффициенты ai, а индекс i определяет соответствующую степень x. Длинное число хранится аналогично, осталось определиться с выбором основания β.

Например, то же самое число 123456789 можно представить в десятитысячной (β = 104) системе счисления следующим образом:

12345678910 = 12345 * (104)1 + 6789 * (104)0

Представляя число 123456789 в десятитысячной системе счисления, мы получаем сразу два преимущества: во-первых, сокращаем количество потребляемой памяти, так как вместо массива из 9 чисел нам достаточно хранить массив из 2 чисел (12345 и 6789), во-вторых, значительно уменьшаем время выполнения стандартных операций над длинными числами, поскольку за раз обрабатываем 5 разрядов числа. В общем, компьютер одинаково быстро складывает одноразрядные и 32-разрядные числа, поэтому этим следует воспользоваться.

Основание системы счисления β обычно зависит от максимального размера базового типа данных на компьютере, и выбирается, исходя из следующих соображений:

  1. Основание должно подходить под один из базовых типов данных;
  2. β должно быть как можно больше, чтобы уменьшить размер представления длинного числа и увеличить скорость операций с ними, но достаточно малого размера, чтобы все операции с коэффициентами использовали базовый тип данных;
  3. Для удобства вывода и отладки можно выбрать β как степень 10, β – степень двойки позволяет проводить быстрые операции на низком уровне.

Следует также отметить, что знак числа, как правило, учитывается в отдельной переменной, то есть массив содержит модуль длинного числа, а так же что число хранится задом наперёд. Сделано это в первую очередь для удобства: не требуется обрабатывать особым образом нулевой / последний элемент массива, в котором мог бы храниться знак числа, а также все операции выполняются от младших разрядов к старшим.

BigInteger от Microsoft

Если посмотреть на структуру BigInteger через Reflector или dotPeek, мы увидим следующие поля:

      private
      static
      readonly BigInteger s_bnMinInt = new BigInteger(-1, newuint[1]{ (uint) int.MinValue });
    privatestaticreadonly BigInteger s_bnOneInt = new BigInteger(1);
    privatestaticreadonly BigInteger s_bnZeroInt = new BigInteger(0);
    privatestaticreadonly BigInteger s_bnMinusOneInt = new BigInteger(-1);
    internalint _sign;
    internaluint[] _bits;
    privateconstint knMaskHighBit = -2147483648;
    privateconstuint kuMaskHighBit = 2147483648U;
    privateconstint kcbitUint = 32;
    privateconstint kcbitUlong = 64;
    privateconstint DecimalScaleFactorMask = 16711680;
    privateconstint DecimalSignMask = -2147483648;

Структура содержит всего два экземплярных поля (_sign и _bits), остальные поля представляют собой константы и статические поля для чтения, представляющие значения структуры для чисел -1, 0 и 1.

Можно предположить, что в переменной _sign хранится знак числа, а массив _bits содержит коэффициенты ai. Учитывая, что массив _bits имеет тип uint[], можно также предположить, что в качестве основания β взята степень двойки 232 (поскольку uint – 32-разрядное беззнаковое число).

Итак, попробуем подтвердить или опровергнуть наши предположения.

Конструктор, принимающий int, в качестве аргумента выглядит так:

      public BigInteger(int value)
    {
      if (value == int.MinValue)
      {
        this = BigInteger.s_bnMinInt;
      }
      else
      {
        this._sign = value;
        this._bits = (uint[]) null;
      }
    }

Его реализация может рассказать немного больше о назначении переменной _sign. Как видно, если длинное число помещается в int-диапазон (от -231 до 231 - 1), то оно хранится в переменной _sign, а массив _bits при этом не используется, он равен null. Эта оптимизация должна ускорить работу типа BigInteger, когда он на самом деле не является большим числом.

Идем дальше.

Конструктор, принимающий uint в качестве аргумента, выглядит следующим образом:

      public BigInteger(uintvalue)
    {
      if (value <= (uint) int.MaxValue)
      {
        this._sign = (int) value;
        this._bits = (uint[]) null;
      }
      else
      {
        this._sign = 1;
        this._bits = newuint[1];
        this._bits[0] = value;
      }
    }

В зависимости от того, умещается ли число в int-диапазон, оно записывается либо в переменную _sign, либо в массив _bits.

Следующий конструктор, принимающий 64-разрядное число со знаком (long), поможет ответить на вопрос о выборе основания системы счисления:

      public BigInteger(longvalue)
    {
      if ((long) int.MinValue <= value && value <= (long) int.MaxValue)
      {
        if (value == (long) int.MinValue)
        {
          this = BigInteger.s_bnMinInt;
        }
        else
        {
          this._sign = (int) value;
          this._bits = (uint[]) null;
        }
      }
      else
      {
        ulong num;
        if (value < 0L)
        {
          num = (ulong) -value;
          this._sign = -1;
        }
        else
        {
          num = (ulong) value;
          this._sign = 1;
        }
        this._bits = newuint[2];
        this._bits[0] = (uint) num;
        this._bits[1] = (uint) (num >> 32);
      }
    }

Если число не помещается в int-диапазон, то, как мы видим, переменная _sign содержит знак числа (-1 – для отрицательного и 1 – для положительного), а массив _bits содержит те самые коэффициенты ai и заполняется следующим образом:

      this._bits = newuint[2];
        this._bits[0] = (uint) num;
        this._bits[1] = (uint) (num >> 32);

В данном случае 64-разрядное число num разбивается на два 32-разрядных: (uint)num и (uint)(num >> 32). Первое число представляет собой последние 32 бита числа num, в то время как второе – первые 32 бита (смещение вправо на n бит равносильно целочисленному делению на 2n).

Давайте определим, как будет храниться число long.MaxValue = 263-1 = 9223372036854775807 в структуре BigInteger. Для этого поделим его на 232:


Фактически, (uint)long.MaxValue = 4294967295, (uint)(long.MaxValue >> 32) = 2147483647.

Значит, 9223372036854775807 = 2147483647*(232)1 + 4294967295*(232)0, и BigInteger будет представлен парой:

_sign = 1

_bits = {4294967295, 2147483647} // вспоминаем, что число храниться задом наперёд

Для длинного числа -1234567891011121314151617181920 имеем:


То есть число раскладывается по степеням 232 следующим образом:

1234567891011121314151617181920 = 15*(232)3 + 2501550035*(232)2 + 3243814879*(232)1 + 4035623136*(232)0

Значит, BigInteger будет представлен парой:

_sign = -1 // знак числа

_bits = {4035623136, 3243814879, 2501550035, 15}

Число, помещающееся в int-диапазон, скажем, 17, будет храниться следующим образом:

_sign = 17

_bits = null

Исследовав конструкторы структуры BigInteger можно сделать вывод:

  1. Если число помещается в int-диапазон, то оно хранится в переменной _sign;
  2. Если число не помещается в int-диапазон, то его знак хранится в переменной _sign (-1 – для отрицательного и 1 – для положительного), а массив _bits содержит коэффициенты ai разложения длинного числа с основанием 232.

Основание β = 232 является неплохим выбором, поскольку со степенью двойки легче работать на низком уровне, а также за раз будут обрабатываться целых 32 разряда числа, что позволяет достаточно быстро совершать операции над ними.

В общем, структура BigInteger является полноценной реализацией длинной арифметики на платформе .NET. При этом Microsoft постаралась максимально близко приблизить её к примитивным числовым типам: экземпляр BigInteger можно использовать точно так же, как и любой другой целочисленный тип. BigInteger перегружает стандартные числовые операторы для выполнения основных математических операций, таких как сложение, вычитание, деление, умножение, вычитания, отрицание и унарное отрицание. Можно также использовать стандартные числовые операторы для сравнения двух значений BigInteger друг с другом. Как и другие типы целого числа, BigInteger поддерживает битовые операторы And, Or, XOR, сдвиг влево и сдвиг вправо.

Для языков, не поддерживающих пользовательские операторы, структура BigInteger также предоставляет эквивалентные методы для выполнения математических операций. Это относится к методам Add, Divide, Multiply, Negate, Subtract и некоторым другим. Точно так же Microsoft поступило в реализации структуры Decimal.

Многие члены структуры BigInteger напрямую соответствуют членам других целых типов. Кроме того, BigInteger добавляет такие элементы как:

Пару слов о BigInteger в Mono и Java

Следует отметить, что Mono также поддерживает длинную арифметику. Реализация структуры BigInteger в Mono не отличается от реализации Microsoft ничем, кроме того, что в ней нет оптимизации для чисел, представимых типом int.

То есть число 17 в Mono будет представлено парой:

_sign = 1

_bits = {17}

Аналогичная реализация BigInteger представлена в Java:

      public
      class BigInteger extends Number implements Comparable<BigInteger> 
  {
    
     int signum;

     int[] mag;

     privateint bitCount = -1;

     privateint bitLength = -1;

     privateint lowestSetBit = -2;
    
     privateint firstNonzeroByteNum = -2;

     privateint firstNonzeroIntNum = -2;
    
     privatefinalstaticlong LONG_MASK = 0xffffffffL;

   }

Поскольку в Java отсутствуют беззнаковые типы, то массив mag имеет тип int[]. Соответственно представления длинного числа в Java и .NET будут отличаться. В .NET представление будет немного эффективнее, поскольку тип uint охватывает больший диапазон:

      private BigInteger(long val) 
{

         if (val < 0) {
            signum = -1;
     val = -val;
         } 
        else {
             signum = 1;
         }

         int highWord = (int)(val >>> 32);

         if (highWord == 0) {
             mag = newint[1];
             mag[0] = (int)val;
         } 
        else {
             mag = newint[2];
             mag[0] = highWord;
             mag[1] = (int)val;
         }
     }

В Java, так же как и в Mono нет оптимизации для чисел, представимых типом int.

Производительность BigInteger

Работая с длинным числом BigInteger, необходимо помнить о возможных проблемах, связанных с производительностью. Например, казалось бы, безобидный оператор ++ может оказать существенное влияние на производительность:

      int length = 1000000;
BigInteger num = BigInteger.Parse("12345678910111213141516171819");
for (int i = 0; i < length; i++)
 {
   if (IsPrime(num))
      num++;
 }
Console.WriteLine(num); 

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

В данном примере можно поступить следующим образом: выполнить промежуточные операции, используя обычные числовые типы, а затем использовать BigInteger:

      int length = 1000000;
BigInteger num = BigInteger.Parse("12345678910111213141516171819");
ulong temp = 0;
for (int i = 0; i < length; i++)
 {
   if (IsPrime(num))
      temp++;
 }
 num += temp;
 Console.WriteLine(num);

Другие числовые типы .NET Framework также являются неизменяемыми. Однако поскольку тип BigInteger не имеет верхней или нижней границы, его значения могут расти до очень больших величин и иметь измеримое влияние на производительность.

Подводя итог

Подводя итог, можно сказать, что платформа .NET, начиная с 4 версии, обзавелась полноценной реализацией целочисленной длинной арифметики. Возможно, для полного счастья осталось реализовать класс BigDecimal, но это уже совсем другая история.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 1    Оценка 15        Оценить