Лучшие способы реализации операции по модулю (вопрос алгоритма)

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

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

result,modulus : integer (n bits) (previously defined)
shiftcount : integer (initialized to zero)
while( (modulus<result) and  (modulus(n-1) != 1) ){
     modulus = modulus << 1
     shiftcount++
}
for(i=shiftcount;i>=0;i--){
     if(modulus<result){result = result-modulus}
     if(i!=0){modulus = modulus >> 1}
}

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


person ryxxui    schedule 05.05.2010    source источник
comment
это значительно медленнее, чем умножение? не похоже, что так должно быть; у вас есть те же основные компоненты.   -  person Jason S    schedule 05.05.2010
comment
Кстати, я также разочарован тем, что статьи в Википедии все чаще пишут математики. Просто потому, что что-то может быть легко выражено с использованием расширенных концепций и обозначений, не означает, что это лучший способ объяснить это ;-) Это похоже на обсуждение Stackoverflow и Mathoverflow.   -  person phkahler    schedule 05.05.2010
comment
@Jason S, конечно, приведенный выше алгоритм намного медленнее, чем умножение. Умножение (с деревьями Уоллеса) - это O (log n), а выше - O (n).   -  person personal_cloud    schedule 20.10.2019


Ответы (5)


Я не уверен, что вы там рассчитываете, если честно. Вы говорите об операции по модулю, но обычно операция по модулю выполняется между двумя числами a и b, и ее результатом является остаток от деления a на b. Где a и b в вашем псевдокоде...?

В любом случае, может это поможет: a mod b = a - floor(a / b) * b.

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

Еще один способ ускорить вычитание — использовать бинарный поиск. Если вы хотите a mod b, вам нужно вычесть b из a до тех пор, пока a не станет меньше b. Итак, в основном вам нужно найти k такое, что:

a - k*b < b, k is min

Один из способов найти это k — линейный поиск:

k = 0;
while ( a - k*b >= b )
    ++k;

return a - k*b;

Но вы также можете выполнить бинарный поиск (только несколько тестов, но он работал на всех из них):

k = 0;
left = 0, right = a
while ( left < right )
{
    m = (left + right) / 2;
    if ( a - m*b >= b )
       left = m + 1;
    else
       right = m;
}

return a - left*b;

Я предполагаю, что решение для бинарного поиска будет самым быстрым при работе с большими числами.

Если вы хотите вычислить a mod b и только a является большим числом (вы можете хранить b в примитивном типе данных), вы можете сделать это еще быстрее:

for each digit p of a do
    mod = (mod * 10 + p) % b
return mod

Это работает, потому что мы можем записать a как a_n*10^n + a_(n-1)*10^(n-1) + ... + a_1*10^0 = (((a_n * 10 + a_(n-1)) * 10 + a_(n-2)) * 10 + ....

Я думаю, что бинарный поиск - это то, что вы ищете.

person IVlad    schedule 05.05.2010
comment
OP в основном выполняет алгоритм деления (путем многократного вычитания, как вы выполняете деление на низком уровне). Двоичный поиск не ускорит его, если есть шаг умножения (который занимает столько же времени, сколько и деление, когда вы делаете это на низком уровне). - person Jason S; 05.05.2010
comment
@Jason S - я не совсем уверен, что делает ОП, но мне кажется, что его цикл while можно заменить двоичным поиском. - person IVlad; 05.05.2010
comment
Это очень низкоуровневая логика ворот. Переключение легкое, быстрое и простое. Бинарный поиск — нет. - person Jason S; 05.05.2010
comment
@IVlad, если ты не уверен, чего пытается добиться ОП, зачем ты вообще пытаешься ему ответить? Нет необходимости отвечать. Нерелевантные ответы только загромождают ветку, что значительно усложняет поиск релевантных ответов. - person SasQ; 26.04.2015
comment
@SasQ, потому что, когда написано первое сообщение, невозможно точно узнать, что нужно ОП, потому что его вопрос очень расплывчатый. На самом деле, нет даже настоящего вопроса, просто есть ли лучший алгоритм или достаточно ли хорош его. Попыток уточнить с его стороны также не было, судя по тому, что он не отвечал на все полученные ответы. Мой ответ может помочь людям, которые наткнутся на этот вопрос. Судя по отзывам, которые я получил, я предполагаю, что это действительно помогло некоторым людям, поэтому я не думаю, что это не имеет значения. Если бы ОП пояснил, что это ему не помогает, я бы удалил его. - person IVlad; 27.04.2015

Есть много способов сделать это за время O(log n) для n бит; вы можете сделать это с умножением, и вам не нужно повторять 1 бит за раз. Например,

a mod b = a - floor((a * r)/2^n) * b

где

r = 2^n / b

предварительно вычисляется, потому что обычно вы используете один и тот же b много раз. Если нет, используйте стандартный метод суперсходящейся полиномиальной итерации для обратного (итерация 2x - bx^2 в фиксированной точке).

Выберите n в соответствии с диапазоном, в котором вам нужен результат (для многих алгоритмов, таких как возведение в степень по модулю, это не обязательно должно быть 0..b).

(Много десятилетий назад мне казалось, что я нашел способ избежать двух умножений подряд... Обновление: я думаю, что это Умножение Монтгомери (см. Алгоритм REDC). Я беру свои слова назад, REDC выполняет ту же работу, что и более простой алгоритм, описанный выше. Не знаю, зачем вообще был изобретен REDC... Возможно, задержка немного меньше из-за использования младшего порядка результат в цепное умножение вместо результата более высокого порядка?)

Конечно, если у вас много памяти, вы можете предварительно вычислить все частичные суммы 2^n mod b для n = log2(b)..log2(a). Многие программные реализации делают это.

person personal_cloud    schedule 20.10.2019
comment
Ваш первый пример, по-видимому, называется редукцией Барретта. К тому же... ты опоздал на вечеринку на 9 с лишним лет. - person Charlie; 26.12.2019
comment
@Charlie Намного позже ... многое из этого было выяснено в 1970-х годах. Но это нормально: сам вопрос был почти таким же запоздалым. - person personal_cloud; 23.01.2020

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

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

EDIT Вот моя реализация на Python:

def mod_mul(a,b,m):
    result = 0
    a = a % m
    b = b % m
    while (b>0):
        if (b&1)!=0:
            result += a
            if result >= m: result -= m
        a = a << 1
        if a>=m: a-= m
        b = b>>1
    return result

Это просто модульное умножение (result = a*b mod m). Операции по модулю в верхней части не нужны, но служат напоминанием о том, что алгоритм предполагает, что a и b меньше m.

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

person phkahler    schedule 05.05.2010
comment
это имеет дополнительное преимущество: если каждое число, прежде чем вы сдвинете его влево на один бит, меньше модуля, то число, сдвинутое влево на один бит (что в два раза больше числа), не может быть более чем в два раза больше модуля, который означает, что вам нужно будет вычесть модуль только один раз в этих шагах. - person Noah Lavine; 09.05.2010

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

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

person Jason S    schedule 05.05.2010

Этот тест (modulus(n-1) != 1) // битовый тест?

- кажется излишним в сочетании с (modulus<result).

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

Если бы мы могли легко выполнять побитовые тесты, это могло бы быть быстро:

m=msb_of(modulus)

while( result>0 ) 
{
  r=msb_of(result) //countdown from prev msb onto result
  shift=r-m        //countdown from r onto modulus or 
                   //unroll the small subtraction 

  takeoff=(modulus<<(shift))  //or integrate this into count of shift

  result=result-takeoff;  //necessary subtraction

  if(shift!=0 && result<0)
  { result=result+(takeoff>>1); }

  } //endwhile

if(result==0) { return result }
else          { return result+takeoff }

(непроверенный код может содержать ошибки)

result многократно уменьшается на modulus, сдвигается для соответствия старшим значащим битам.

После каждого вычитания: result имеет шанс ~50/50 потерять более 1 старшего разряда. Он также имеет ~ 50/50 шанс стать отрицательным, добавление половины того, что было вычтено, всегда снова сделает его положительным. > его следует вернуть в плюс, если сдвиг не был = 0

Рабочий цикл завершается, когда result недоработано, а 'shift' равен 0.

person strainer    schedule 08.05.2010