Объяснение арифметики произвольной точности

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

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

На связанной ноте, кажется, что очень длинный и процедурный код базы данных часто является признаком чего-то еще, и я думаю, что такой код может быть преобразован в тестируемый код, не подвергаясь хиту производительности. Теория состоит в том, что такой код часто представляет пакетные обработки, которые периодически обрабатывают большие объемы данных. Если бы эти пакетные обработки должны были быть преобразованы в меньшие блоки бизнес-логики в реальном времени, которая работает каждый раз, когда входные данные изменяются, эта логика могла быть выполнена на среднем уровне (где это может быть протестировано), не получая удар производительности (так как работа сделана в маленьких блоках в режиме реального времени). Как побочный эффект, это также устраняет длинную обратную связь обработки ошибок пакетной обработки. Конечно, этот подход не будет работать во всех случаях, но он может работать в некоторых. Кроме того, если существуют тонны такого непригодного для тестирования кода базы данных пакетной обработки в Вашей системе, дорога к спасению может быть длинной и трудной. YMMV.

89
задан Arafangion 2 June 2018 в 07:59
поделиться

6 ответов

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

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

В библиотеке произвольной точности, нет фиксированного ограничения на количество базовых типов, используемых для представления наших чисел, На самом деле мы написали библиотеки для такого рода вещей, используя максимальную степень десяти, которая может быть помещена в целое число в квадрате (чтобы предотвратить переполнение при умножении двух int вместе, например, 16-битного ] int ограничен значениями от 0 до 99 для генерации 9,801 (<32,768) в квадрате, или 32-битным int с использованием значений от 0 до 9,999 для генерации 99 980 001 (<2 147 483 648)), что значительно упростило алгоритмы.

Некоторые хитрости, на которые следует обратить внимание.

1 / При сложении или умножении чисел предварительно выделите максимальное необходимое пространство, а затем уменьшите его позже, если вы обнаружите, что это слишком много. Например, добавление двух 100-значных (где цифра - int ) числа никогда не даст вам более 101 цифры. При умножении 12-значного числа на 3-значное число никогда не будет больше 15 цифр (сложите количество цифр).

2 / Для увеличения скорости нормализуйте (уменьшите объем памяти, необходимый для) числа только в случае крайней необходимости - мои В библиотеке это было отдельным вызовом, поэтому пользователь мог выбирать между скоростью и хранением.

3 / Добавление положительного и отрицательного числа - это вычитание, а вычитание отрицательного числа - то же самое, что добавление эквивалентного положительного числа. Вы можете сэкономить довольно много кода, заставив методы сложения и вычитания вызывать друг друга после корректировки знаков.

4 / Избегайте вычитания больших чисел из маленьких, поскольку вы неизменно получаете такие числа, как:

         10
         11-
-- -- -- --
99 99 99 99 (and you still have a borrow).

Вместо этого вычитайте 10 из 11, а затем отрицать это:

11
10-
--
 1 (then negate to get -1).

Вот комментарии (преобразованные в текст) из одной из библиотек, для которой мне пришлось это сделать. Сам код: К сожалению, авторское право защищено, но вы можете выбрать достаточно информации для выполнения четырех основных операций. Предположим далее, что -a и -b представляют отрицательные числа, а a и b - нулевые или положительные числа.

Для сложения , если знаки разные, используйте вычитание отрицания:

-a +  b becomes b - a
 a + -b becomes a - b

Для вычитания , если знаки разные, используйте добавление отрицания:

 a - -b becomes   a + b
-a -  b becomes -(a + b)

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

small - big becomes -(big - small)

Умножение использует математику начального уровня следующим образом:

475(a) x 32(b) = 475 x (30 + 2)
               = 475 x 30 + 475 x 2
               = 4750 x 3 + 475 x 2
               = 4750 + 4750 + 4750 + 475 + 475

Способ, которым это достигается, включает извлечение каждой из 32 цифр по одной (в обратном порядке) затем с помощью add вычислить значение, которое будет добавлено к результату (изначально нулевое).

ShiftLeft и ShiftRight используются для быстрого умножения или деления LongInt на значение переноса (10 для «реальных» математических операций). В приведенном выше примере мы прибавляем 475 к нулю 2 раза (последняя цифра 32), чтобы получить 950 (результат = 0 + 950 = 950).

Затем мы сдвигаем влево 475, чтобы получить 4750, и сдвиг вправо 32, чтобы получить 3 . Добавьте 4750 к нулю 3 раза, чтобы получить 14250, затем прибавьте к результату 950, чтобы получить 15200.

Сдвиг влево 4750, чтобы получить 47500, сдвиг вправо 3, чтобы получить 0. Поскольку смещение вправо 32 теперь равно нулю, мы закончили и, на самом деле, 475 x 32 равно 15200.

Деление также сложно, но основано на ранней арифметике (метод «газинты» для «входит в»). Рассмотрим следующее длинное деление для 12345/27 :

       457
   +-------
27 | 12345    27 is larger than 1 or 12 so we first use 123.
     108      27 goes into 123 4 times, 4 x 27 = 108, 123 - 108 = 15.
     ---
      154     Bring down 4.
      135     27 goes into 154 5 times, 5 x 27 = 135, 154 - 135 = 19.
      ---
       195    Bring down 5.
       189    27 goes into 195 7 times, 7 x 27 = 189, 195 - 189 = 6.
       ---
         6    Nothing more to bring down, so stop.

Следовательно, 12345/27 равно 457 с остатком 6 . Проверить:

  457 x 27 + 6
= 12339    + 6
= 12345

Это реализуется с помощью переменной спада (изначально равной нулю) для уменьшения сегментов 12345 по одному, пока она не станет больше или равной 27.

Затем мы просто вычитаем 27 из этого числа, пока не будем получить меньше 27 - количество вычитаний - это сегмент, добавленный к верхней строке.

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


Имейте в виду, что это довольно простые алгоритмы. Есть гораздо лучшие способы выполнять сложные арифметические операции, если ваши числа будут особенно большими. Вы можете посмотреть что-то вроде GNU Multiple Precision Arithmetic Library - это значительно лучше и быстрее, чем мои собственные библиотеки.

157
ответ дан 24 November 2019 в 07:16
поделиться

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

Например, умножение. Наивно, вы могли бы подумать о методе «школьника», то есть написать одно число над другим, а затем проделать долгое умножение, как вы учили в школе. пример:

      123
    x  34
    -----
      492
+    3690
---------
     4182

, но этот метод очень медленный (O (n ^ 2), n - количество цифр). Вместо, современные пакеты bignum используют либо дискретное преобразование Фурье, либо числовое преобразование, чтобы превратить это по существу в операцию O (n ln (n)).

И это только для целых чисел. Когда вы переходите к более сложным функциям на каком-либо типе реального представления числа (log, sqrt, exp и т. Д.), Все становится еще сложнее.

Если вам нужны теоретические знания, я настоятельно рекомендую прочитать первую главу книги Япа «Фундаментальные проблемы алгоритмической алгебры» . Как уже упоминалось, библиотека gmp bignum - отличная библиотека. Для вещественных чисел я использовал mpfr, и он мне понравился.

sqrt, exp и т. д.) все становится еще сложнее.

Если вам нужны теоретические знания, я настоятельно рекомендую прочитать первую главу книги Япа «Фундаментальные проблемы алгоритмической алгебры» . Как уже упоминалось, библиотека gmp bignum - отличная библиотека. Для реальных чисел я использовал mpfr, и он мне понравился.

sqrt, exp и т. д.) все становится еще сложнее.

Если вам нужны теоретические знания, я настоятельно рекомендую прочитать первую главу книги Япа «Фундаментальные проблемы алгоритмической алгебры» . Как уже упоминалось, библиотека gmp bignum - отличная библиотека. Для реальных чисел я использовал mpfr, и он мне понравился.

8
ответ дан 24 November 2019 в 07:16
поделиться

Не изобретайте велосипед: он может оказаться квадратным!

Используйте стороннюю библиотеку, такую ​​как GNU MP , которая проверена и протестирована.

6
ответ дан 24 November 2019 в 07:16
поделиться

Вы делаете это в основном так же, как с карандашом и бумагой ...

  • Число должно быть представлено в буфере (массиве), способном принимать произвольный размер ( что означает использование malloc и realloc ) по мере необходимости
  • вы реализуете базовую арифметику в максимально возможной степени с использованием структур, поддерживаемых языком, и имеете дело с переносами и перемещением точки счисления вручную
  • вы просматриваете тексты числового анализа, чтобы найти эффективные аргументы для работы с более сложной функцией
  • вы реализуете только столько, сколько вам нужно.

Обычно вы будете использовать в качестве базовой единицы вычисления

  • байтов, содержащих 0-99 или 0-255
  • 16-битные слова, содержащие либо 0-9999, либо 0-65536
  • 32-битные слова, содержащие ...
  • ...

в соответствии с вашей архитектурой.

Выбор двоичной или десятичной основы зависит от ваших желаний для максимальной эффективности использования пространства, удобочитаемости и отсутствия поддержки математики в двоично-десятичном формате (BCD) на вашем чипе.

4
ответ дан 24 November 2019 в 07:16
поделиться

Assuming that you wish to write a big integer code yourself, this can be surprisingly simple to do, spoken as someone who did it recently (though in MATLAB.) Here are a few of the tricks I used:

  • I stored each individual decimal digit as a double number. This makes many operations simple, especially output. While it does take up more storage than you might wish, memory is cheap here, and it makes multiplication very efficient if you can convolve a pair of vectors efficiently. Alternatively, you can store several decimal digits in a double, but beware then that convolution to do the multiplication can cause numerical problems on very large numbers.

  • Store a sign bit separately.

  • Addition of two numbers is mainly a matter of adding the digits, then check for a carry at each step.

  • Multiplication of a pair of numbers is best done as convolution followed by a carry step, at least if you have a fast convolution code on tap.

  • Even when you store the numbers as a string of individual decimal digits, division (also mod/rem ops) can be done to gain roughly 13 decimal digits at a time in the result. This is much more efficient than a divide that works on only 1 decimal digit at a time.

  • To compute an integer power of an integer, compute the binary representation of the exponent. Then use repeated squaring operations to compute the powers as needed.

  • Many operations (factoring, primality tests, etc.) will benefit from a powermod operation. That is, when you compute mod(a^p,N), reduce the result mod N at each step of the exponentiation where p has been expressed in a binary form. Do not compute a^p first, and then try to reduce it mod N.

1
ответ дан 24 November 2019 в 07:16
поделиться

Вы можете сделать это со старшим школьным уровнем математики. Хотя в реальности используются более продвинутые алгоритмы. Так, например, чтобы добавить два 1024-байтовых числа:

unsigned char first[1024], second[1024], result[1025];
unsigned char carry = 0;
unsigned int  sum   = 0;

for(size_t i = 0; i < 1024; i++)
{
    sum = first[i] + second[i] + carry;
    carry = sum - 255;
}

результат должен быть больше на одно место в случае сложения, чтобы обеспечить максимальные значения. Посмотрите на это:

9
   +
9
----
18

TTMath - отличная библиотека, если вы хотите учиться. Он построен с использованием C ++. Вышеупомянутый пример был глупым, но именно так в целом выполняется сложение и вычитание!

Хорошим справочным материалом по этому вопросу является Вычислительная сложность математических операций . Он сообщает вам, сколько места требуется для каждой операции, которую вы хотите реализовать. Например, если у вас есть два N-значных числа, тогда вам потребуется 2N цифр для сохранения результата умножения.

Как сказал Митч , это далеко не простая задача! Я рекомендую вам взглянуть на TTMath, если вы знаете C ++.

3
ответ дан 24 November 2019 в 07:16
поделиться
Другие вопросы по тегам:

Похожие вопросы: