Алгоритм целочисленного деления - PullRequest
23 голосов
/ 24 февраля 2011

Я думал об алгоритме деления больших чисел: делении с остатком bigint C на bigint D, где мы знаем представление C в базе b, а D имеет форму b ^ k-1.Вероятно, проще всего показать это на примере.Давайте попробуем разделить C = 21979182173 на D = 999.

  • Запишем число как набор из трех цифр: 21 979 182 173
  • Мы берем суммы (по модулю 999) последовательных наборов, начиная слева: 21 001 183 356
  • Мы добавляем 1 к тем наборам, которые предшествуют тем, в которых мы «прошли 999»: 22 001 183 356

Действительно, 21979182173 /999 = 22001183 и остаток 356.

Я вычислил сложность и, если я не ошибаюсь, алгоритм должен работать в O (n), где n - количество цифр C в базовом b представлении.,Я также сделал очень грубую и неоптимизированную версию алгоритма (только для b = 10) в C ++, проверил его по общему алгоритму целочисленного деления GMP, и он действительно лучше, чем GMP.Я не мог найти ничего подобного реализованному, где бы я ни выглядел, поэтому мне пришлось прибегнуть к его проверке на предмет общего деления.

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

Таким образом, мой вопрос будет таким: есть ли статья или книга или что-то, где описан вышеупомянутый алгоритм, возможно, обсуждаются реализации?Если нет, то имеет ли смысл для меня пытаться реализовать и протестировать такой алгоритм, скажем, на C / C ++, или этот алгоритм по своей сути плох?

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

Большое спасибо!


Дальнейшее разъяснение вопросов, поднятых в комментариях / ответах:

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

Я полностью осознаю, что работа в базах2 ^ n - это, вообще говоря, наиболее эффективный способ ведения дел.Практически все библиотеки bigint используют 2 ^ 32 или что-то еще.Однако, что если (и, я подчеркиваю, это будет полезно только для этого конкретного алгоритма!) Мы реализуем bigints как массив цифр в базе b?Конечно, мы требуем, чтобы b здесь было «разумным»: b = 10, наиболее естественный случай, кажется достаточно разумным.Я знаю, что это более или менее неэффективно как с точки зрения памяти и времени, так и с точки зрения внутреннего хранения чисел, но я смог, если мои (базовые и, возможно, каким-то образом ошибочные) тесты верны, вывести результаты быстрее, чем общее деление GMP,что имело бы смысл для реализации такого алгоритма.

Замечания Ninefingers мне пришлось бы использовать в этом случае дорогостоящую операцию по модулю.Я надеюсь, что нет: я могу видеть, пересекли ли старый + новый, скажем, 999, просто посмотрев на число цифр старый + новый + 1.Если оно имеет 4 цифры, мы сделали.Более того, начиная со старого <999 и нового <= 999, мы знаем, что если у старого + нового + 1 есть 4 цифры (больше не может быть), то (старый + новый)% 999 равняется удалению самой левой цифры (старый + новый + 1), который, я полагаю, мы можем сделать дешево. </p>

Конечно, я не оспариваю очевидные ограничения этого алгоритма и не утверждаю, что его нельзя улучшить - его можно разделить только сопределенный класс чисел, и мы должны априори знать представление дивидендов в базе б.Однако для b = 10, например, последнее кажется естественным.

Теперь, скажем, мы внедрили bignums, как я обрисовал выше. Скажите C = (a_1a_2 ... a_n) в основании b и D = b ^ k-1. Алгоритм (который, возможно, может быть гораздо более оптимизирован) будет выглядеть следующим образом. Надеюсь, опечаток не так много.

  • если k> n, мы, очевидно, закончили
  • добавить ноль (то есть a_0 = 0) в начале C (на всякий случай, если мы попытаемся разделить, скажем, 9999 с 99)
  • l = n% k (мод для "обычных" целых чисел - не должен быть слишком дорогим)
  • old = (a_0 ... a_l) (первый набор цифр, возможно, с числом цифр меньше k)
  • для (i = l + 1; i (У нас будет пол (n / k) или около того итераций)
    • новый = (a_i ... а_ (я + к-1))
    • new = new + old (это сложение bigint, то есть O (k))
    • aux = new + 1 (опять же, добавление bigint - O (k) - что меня не устраивает)
    • если в aux больше k цифр
      • удалить первую цифру aux
      • old = old + 1 (добавление bigint еще раз)
      • заполнить старый нулями в начале, чтобы в нем было столько цифр, сколько нужно
      • (a_ (ik) ... a_ (i-1)) = старый (если i = l + 1, (a _ 0 ... a _ л) = старый)
      • новый = Окс
    • заполнить новые нулями в начале, чтобы в них было столько цифр, сколько нужно
    • (a_i ... а_ (я + к-1) = новый
  • Quot = (A_0 ... а_ (п-к + 1))
  • бэр = новый

Спасибо, что обсудили это со мной - как я уже сказал, мне кажется, что это интересный алгоритм "особого случая", который я пытаюсь реализовать, протестировать и обсудить, если никто не увидит в нем каких-либо фатальных недостатков. Если это пока что не очень широко обсуждается, даже лучше. Пожалуйста, дайте мне знать, что вы думаете. Извините за длинный пост.

Также, еще несколько личных комментариев:

@ Ninefingers: На самом деле у меня есть некоторые (очень базовые!) Знания о том, как работает GMP, что он делает, и об общих алгоритмах деления Bigint, поэтому я смог понять большую часть вашего аргумента. Я также знаю, что GMP высоко оптимизирован и в некотором роде настраивает себя под разные платформы, поэтому я, конечно, не пытаюсь «побить его» в целом - это кажется столь же плодотворным, как атака на танк остроконечной палкой. Однако идея этого алгоритма не в этом - он работает в очень особых случаях (которые GMP, по-видимому, не охватывает). На несвязанной ноте, вы уверены, что общие деления делаются в O (n)? Максимум, что я видел, это M (n). (И это, если я правильно понимаю, на практике (Шёнхаге-Штрассен и т. Д.) Не достигает O (n). Алгоритм Фюрера, который все еще не достигает O (n), является, если я прав, почти чисто теоретический.)

@ Ави Бергер: На самом деле это не то же самое, что «разгон девяток», хотя идея похожа. Однако вышеупомянутый алгоритм должен работать постоянно, если я не ошибаюсь.

Ответы [ 3 ]

12 голосов
/ 24 февраля 2011

Ваш алгоритм представляет собой разновидность алгоритма из базовых 10, известного как «раздача девяток».Ваш пример использует базу 1000 и «изгоняет» 999 (на единицу меньше, чем база).Раньше этому учили в начальной школе как способ быстрой проверки расчетов на руках.У меня был учитель математики в старшей школе, который был в ужасе, узнав, что его больше не учат, и наполнил нас им.

Изгнание 999-х на базе 1000 не сработает как алгоритм общего деления.Он будет генерировать значения, которые соответствуют модулю 999 фактическому фактору и остатку, а не фактическим значениям.Ваш алгоритм немного отличается, и я не проверял, работает ли он, но он основан на эффективном использовании базы 1000, а делитель на 1 меньше, чем база.Если вы хотите попробовать его для деления на 47, вам придется сначала перейти на систему счисления с основанием 48.

Google "отбрасывает девятки" для получения дополнительной информации.

Редактировать: Первоначально я прочитал ваш пост слишком быстро, и вы знаете об этом как работающий алгоритм.Как @Ninefingers и @Karl Bielefeldt более четко, чем я, указали в своих комментариях, что вы не учитываете в своей оценке производительности, так это преобразование в базу, подходящую для конкретного рассматриваемого делителя.

5 голосов
/ 24 февраля 2011

Я чувствую необходимость добавить это на основе моего комментария. Это не ответ, а объяснение фона.

Библиотека bignum использует так называемые конечности - ищите mp_limb_t в источнике gmp, которые обычно являются целочисленным полем фиксированного размера.

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

doublelimb r = limb_a + limb_b + carryfrompreviousiteration

Этот конечный элемент двойного размера ловит переполнение limb_a + limb_b в случае, если сумма больше, чем размер конечности. Так что, если сумма больше, чем 2 ^ 32, если мы используем uint32_t в качестве размера конечности, переполнение может быть обнаружено.

Зачем нам это нужно? Ну, что вы обычно делаете, это перебираете все конечности - вы делали это сами, деля свое целое число на части и проходя каждую из них - но мы делаем это сначала LSL (поэтому сначала наименьшая конечность), как вы делали арифметику от руки.

Это может показаться неэффективным, но это всего лишь способ C делать вещи. Чтобы действительно разбить большие пушки, x86 имеет adc в качестве инструкции - добавьте с переносом. Это делает арифметику и для ваших полей и устанавливает бит переноса, если арифметика превышает размер регистра. В следующий раз, когда вы сделаете add или adc, процессор также учитывает бит переноса. В вычитании это называется флагом заимствования.

Это также относится к операциям смены. Таким образом, эта особенность процессора имеет решающее значение для быстрой работы Bignums. Итак, дело в том, что в микросхеме для этого есть электронная схема - выполнение в программном обеспечении всегда будет медленнее.

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

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

Я также хотел бы добавить, что, хотя то, что у вас есть, вероятно, быстро для этого случая, имейте в виду, что, как библиотека bignum, gmp выполняет для вас немало работы, например, управление памятью. Если вы используете mpz_, то для начала вы используете абстракцию выше того, что я описал здесь. Наконец, gmp использует сборку, оптимизированную вручную, с развернутыми циклами практически для каждой платформы, о которой вы когда-либо слышали, и даже больше. Есть очень веская причина, по которой он поставляется с Mathematica, Maple et al.

Теперь, просто для справки, некоторые материалы для чтения.

  • Современная компьютерная арифметика - это кнутовидная работа для библиотек произвольной точности.
  • Дональд Кнут, Полу-численные алгоритмы (Искусство компьютерного программирования, том II).
  • Блог Уильяма Харта о реализации алгоритмов для bsdnt , в котором он обсуждает различные алгоритмы деления. Если вы заинтересованы в библиотеках bignum, это отличный ресурс. Я считал себя хорошим программистом до тех пор, пока не начал следить за этим ...

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


Редактировать: Хорошо, не все методы O (n).Большинство методов, называемых div1 (деление на что-то, не превышающее конечности, это O (n). Когда вы становитесь больше, вы сталкиваетесь со сложностью O (n ^ 2); этого трудно избежать.

ТеперьМогли бы вы реализовать bigints как массив цифр? Ну да, конечно, вы могли бы. Однако рассмотрим идею только при добавлении

/* you wouldn't do this just before add, it's just to 
   show you the declaration.
 */
uint32_t* x = malloc(num_limbs*sizeof(uint32_t));
uint32_t* y = malloc(num_limbs*sizeof(uint32_t));
uint32_t* a = malloc(num_limbs*sizeof(uint32_t));
uint32_t m;

for ( i = 0; i < num_limbs; i++ )
{
    m = 0;
    uint64_t t = x[i] + y[i] + m;
    /* now we need to work out if that overflowed at all */
    if ( (t/somebase) >= 1 ) /* expensive division */
    {
        m = t % somebase; /* get the overflow */
    }
}

/* frees somewhere */

Это грубый набросок того, что вы ищете для добавления черезваша схема. Таким образом, у вас есть для запуска преобразования между базами. Поэтому вам понадобится преобразование в ваше представление для базы, а затем обратно, когда вы закончите, потому чтоэта форма просто очень медленная везде . Мы не говорим здесь о разнице между O (n) и O (n ^ 2), но мы говорим о дорогостоящей инструкции деления законечность или дорогостоящее преобразование каждый раз, когда вы хотите разделить . Посмотрите это .

Далее, как вы расширяете свойделение для общего деления дел?n, если вы хотите разделить эти два числа x и y из приведенного выше кода.Вы не можете, является ответом, не прибегая к средствам на основе bignum, которые дорогиСмотри Кнут.Взятие по модулю числа больше вашего размера не работает.

Позвольте мне объяснить.Попробуйте 21979182173 мод 1099. Давайте для простоты предположим, что поле наибольшего размера, которое мы можем иметь, состоит из трех цифр .Это надуманный пример, но самый большой размер поля, который я знаю, использует 128 бит с использованием расширений gcc.В любом случае, дело в том, что вы:

21 979 182 173

Разделите свой номер на конечности.Затем вы берете по модулю и сумме:

21 1000 1182 1355

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

Так в чем же решение?Разделите ваш номер на серию подходящих по размеру бигнумов?И начать использовать функции bignum для расчета всего, что вам нужно?Это будет намного медленнее, чем любой существующий способ манипулирования полями напрямую.

Теперь, возможно, вы предлагаете этот случай только для деления на конечность, а не на бигнум, и в этом случае он может работать, но деление по Хензелю и предварительно вычисленные обратные значения и т. Д. Делают без требования преобразования .Я понятия не имею, будет ли этот алгоритм быстрее, чем, скажем, деление Хензеля;это было бы интересное сравнение;проблема возникает с общим представлением в библиотеке bignum .Представление, выбранное в существующих библиотеках bignum, объясняется причинами, которые я расширил, - это имеет смысл на уровне сборки, где это было впервые сделано.

В качестве примечания;Вам не нужно использовать uint32_t для представления своих конечностей.Вы в идеале используете размер, равный размеру регистров системы (скажем, uint64_t), чтобы вы могли воспользоваться версиями, оптимизированными для сборки.Таким образом, в 64-битной системе adc rax, rbx устанавливает переполнение (CF), только если результат переполняет 2 ^ 64 бита.

tl; dr версия: проблема не в вашем алгоритмеили идея;это проблема преобразования между базами, так как представление, которое вам нужно для вашего алгоритма, не самый эффективный способ сделать это в add / sub / mul и т. д. Перефразировать кнут: Это показывает разницу между математической элегантностью и вычислительной эффективностью.

0 голосов
/ 17 ноября 2017

Если вам нужно часто делить на один и тот же делитель, используя it (или его степень), поскольку ваша база делает деление таким же дешевым, как смещение бит для двоичных целых чисел с основанием 2.

Вы можете использовать базу 999, если хотите; нет ничего особенного в использовании базы степени 10, кроме того, что это делает преобразование в десятичное целое очень дешевым. (Вы можете работать по одной конечности за раз вместо того, чтобы делать полное деление по целому целому. Это похоже на разницу между преобразованием двоичного целого числа в десятичное и превращением каждых 4 битов в шестнадцатеричную цифру. Двоичный код -> шестнадцатеричное можно начать с наиболее значимыми битами, но преобразование в базы не-степени-2 должно быть сначала LSB с использованием деления.)


Например, чтобы вычислить первые 1000 десятичных цифр Фибоначчи (10 9 ) для вопроса о код-гольфе с требованием к производительности, мои 105 байтов машинного кода x86 отвечают использовал тот же алгоритм, что и этот ответ Python : обычная a+=b; b+=a итерация Фибоначчи, но делится на (степень) 10 каждый раз, когда a становится слишком большим.

Фибоначчи растет быстрее, чем распространяется перенос, поэтому отбрасывание младших десятичных цифр иногда не меняет долгосрочные большие цифры. (Вы оставляете немного больше точности, которую хотите).

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

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


Я хранил конечности base-10 9 в 32-битных целочисленных элементах. Деление на 10 9 тривиально дешево: просто приращение указателя, чтобы пропустить нижнюю конечность. Вместо того, чтобы фактически делать memmove, я просто смещаю указатель, используемый следующей итерацией добавления.

Я думаю, что деление на степень 10, отличную от 10 ^ 9, было бы несколько дешево, но потребовало бы фактического деления на каждой конечности и распространения остатка на следующую конечность.

В этом случае сложение с расширенной точностью несколько дороже , чем с бинарными конечностями, потому что мне нужно сгенерировать выполнение вручную с помощью сравнения: sum[i] = a[i] + b[i]; carry = sum < a; (сравнение без знака). А также вручную обернуть до 10 ^ 9 на основе этого сравнения с помощью инструкции условного перемещения. Но я смог использовать этот улов в качестве ввода для adc (инструкция x86 для добавления с переносом).

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

Это тратит чуть более 2 бит каждого 32-битного лимба: 10 ^ 9 вместо 2^32 = 4.29... * 10^9. Хранение 10-значных базовых цифр по одному на байт будет значительно менее эффективным с точки зрения занимаемой площади, а очень значительно ухудшит производительность, поскольку 8-разрядное двоичное добавление стоит столько же, сколько 64-разрядное двоичное добавление на современных 64-разрядных. бит процессора.

Я стремился к размеру кода: для чистой производительности я бы использовал 64-битные конечности, содержащие base-10 ^ 19 «цифр». (2^64 = 1.84... * 10^19, так что это тратит меньше 1 бита на 64.) Это позволяет вам удвоить объем работы, выполняемой с каждой аппаратной add инструкцией. Хм, на самом деле это может быть проблемой: сумма двух частей может обернуть 64-битное целое число, поэтому просто проверка на > 10^19 уже не достаточна. Вы можете работать в базе 5*10^18 или в базе 10^18 или выполнять более сложное обнаружение выноса, которое проверяет двоичный перенос, а также перенос вручную.

Хранение упакованного BCD с одной цифрой на 4-битный клев было бы еще хуже для производительности, потому что нет аппаратной поддержки для блокировки переноса одного куска в следующий в байте.


В целом, моя версия работала примерно в 10 раз быстрее, чем версия с расширенной точностью Python на том же оборудовании (но она имела место для значительной оптимизации скорости, делив реже). (70 секунд или 80 секунд против 12 минут)

Тем не менее, я думаю, что для этой конкретной реализации этого алгоритма (где мне нужно было только сложение и деление, а деление происходило после каждых нескольких сложений), выбор конечностей base-10 ^ 9 был очень хорошим , Существуют гораздо более эффективные алгоритмы для числа N-го числа Фибоначчи, которым не нужно делать 1 миллиард сложений с увеличенной точностью.

...