Эффективный (циклично) алгоритм для вычисления по модулю 25? - PullRequest
10 голосов
/ 11 июня 2009

У меня есть код, в котором я вычисляю x% 25. x всегда принимает положительное значение, но его динамический диапазон велик.

Я обнаружил, что этот конкретный фрагмент кода вычисления x% 25 занимает большие циклы. Мне нужно оптимизировать его.

Предварительно вычисленная таблица поиска исключена из-за возможного большого объема памяти таблицы.

В качестве второго подхода я кодировал фрагмент ниже (код C) -

mod(a, b)
{   
    int r = a;  
    while(r >= b)
    {      
        r = r - b;
    }   
    return r;
}

1.) Как я могу оптимизировать этот код для циклов (сжать его до максимума)?

2.) Есть ли какой-то совершенно другой оптимизированный способ достижения x% 25 (я знаю, что это не обычная операция, но все же, в поисках умных входов, которые люди могли бы использовать в своем опыте, которые могли бы мне помочь.) *

Спасибо.

-AD

EDIT:

Я думаю, что при использовании собственного оператора по модулю% в C внутренне используется операция деления (/), которая является дорогостоящей на процессоре, который я использую. (Нет команды div). следовательно, пытаясь понять, может ли пользовательская реализация превзойти внутренние вычисления, используя оператор%.

-AD

Ответы [ 21 ]

30 голосов
/ 11 июня 2009

Предлагаю прочитать Восторг хакера . Он описывает очень быстрые остаточные алгоритмы для постоянных делителей. Они почти наверняка побеждают общий алгоритм.

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

unsigned mod25(unsigned n)
{
    unsigned reciprocal = 1374389535; // 2^35 / 25
    unsigned div25 = ((unsigned long long)n * reciprocal) >> 35;
    return n - div25 * 25;
}
8 голосов
/ 12 июня 2009

Вот еще одно решение, которое я придумал:

int mod25(int x){
  /* 25 * (all powers of 2 <= INT_MAX), descending */
  if (x >= 1677721600) x -= 1677721600;
  if (x >=  838860800) x -=  838860800;
  if (x >=  419430400) x -=  419430400;
  if (x >=  209715200) x -=  209715200;
  if (x >=  104857600) x -=  104857600;
  if (x >=   52428800) x -=   52428800;
  if (x >=   26214400) x -=   26214400;
  if (x >=   13107200) x -=   13107200;
  if (x >=    6553600) x -=    6553600;
  if (x >=    3276800) x -=    3276800;
  if (x >=    1638400) x -=    1638400;
  if (x >=     819200) x -=     819200;
  if (x >=     409600) x -=     409600;
  if (x >=     204800) x -=     204800;
  if (x >=     102400) x -=     102400;
  if (x >=      51200) x -=      51200;
  if (x >=      25600) x -=      25600;
  if (x >=      12800) x -=      12800;
  if (x >=       6400) x -=       6400;
  if (x >=       3200) x -=       3200;
  if (x >=       1600) x -=       1600;
  if (x >=        800) x -=        800;
  if (x >=        400) x -=        400;
  if (x >=        200) x -=        200;
  if (x >=        100) x -=        100;
  if (x >=         50) x -=         50;
  if (x >=         25) x -=         25;
  return x;
}

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

Немного трудно убедить себя, что это работает, но это работает (по крайней мере, для неотрицательных значений x).

Приведенный выше код действительно является развернутой версией этого:

int mod25(int x){
  int divisor;
  for(int divisor = 1677721600; divisor >= 25; divisor >>= 1) {
    if (x >= divisor) x -= divisor;
  }
  return x;
}

Развернув его, мы избегаем сравнения циклов, а также сдвигов за счет большего кода. Вы могли бы даже частично развернуть его, используя устройство Даффа, если бы вы чувствовали такую ​​склонность, но, имея всего 27 итераций и такой крошечный кусочек кода на каждую итерацию, я был бы склонен просто развернуть его полностью.

Вот как это работает: каждое неотрицательное целое число x может быть выражено как (n * 25) + k, где n - неотрицательное целое число, а k - целое число от 0 до 24. k также является результатом мы хотим, поэтому, если бы мы могли вычислить x - (n * 25), мы получили бы наш ответ. Однако мы хотим сделать это, не зная заранее.

Подумайте о двоичном. Если бы мы могли отключить каждый из 1 бита, то получили бы 0. Один из способов сделать это - начать с больших степеней 2 и двигаться вниз, вычитая каждую степень 2, только если текущее значение n больше чем или равно этой степени 2.

Поскольку мы имеем дело с (n * 25), нам на самом деле нужны убывающие степени в 2 раза 25. Поскольку k строго меньше 25, а наименьший делитель, который мы когда-либо рассматриваем, равен 25, это работает, даже когда мы имеем дело с (n * 25) + к.

Таким образом, каждое сравнение + вычитание обнуляет один бит из n, и в конце мы получаем k, остаток.

7 голосов
/ 11 июня 2009

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

7 голосов
/ 11 июня 2009

Вот лучшее, что я мог придумать:

int mod25(int x)
{
    while((x = (x & 31) + 7 * (x >> 5)) >= 25)
        x -= 25;

    return x;
}

Это приблизительно x % 25 с x % 32 + 7 * (x/32). Значение будет превышено кратным 25, что допускает рекурсию.

Производительность кажется достаточной: для значения x = 2147483647 (он же INT_MAX) требуется 11 итераций.

7 голосов
/ 11 июня 2009

Я был вдохновлен ответом Пакса и создал более универсальный алгоритм.

int mod(int a, int b) {
    int s = b;
    while (s <= a) {
        s <<= 1;
    }
    int r = a;
    while (r >= b) {
        s >>= 1;
        if (s <= r) {    
            r -= s;
        }
    }
    return r;
}

Это вычитает мощность двух кратных b от a до тех пор, пока не будет найден результат.

РЕДАКТИРОВАТЬ: добавлено условие if, чтобы оно работало должным образом.

Например, если это выполняется на 100% 7, сначала получается, что 7 * 2 * 2 * 2 * 2 = 112. Затем он делит 112 (s) на 2 и вычитает это из 100 (r) (когда s <= r) и постоянно делает это, пока модуль не найден. Таким образом,

s = 112 / 2 = 56, r = 100 - 56 = 44
s = 56 / 2 = 28, r = 44 - 28 = 16
s = 28 / 2 = 14, r = 16 - 14 = 2

следовательно, 100% 7 = 2

6 голосов
/ 11 июня 2009

О, мое <божество выбора>. Я не могу поверить некоторым из этих ответов.

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

20 % 25

это легко и быстро, используя повторное вычитание, но:

65535 % 25

будет ужасно медленным, 600+ итераций. Это в среднем 300 итераций для 16-битных чисел. Что касается 32-битного числа, ну, даже не ходите туда.

Самый быстрый способ сделать это - использовать длинное деление. Смотрите ответ Ники.

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

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

5 голосов
/ 11 июня 2009

Проблема вашего цикла в том, что это O (n) - оно будет очень медленным при больших значениях r. Я бы предложил что-то вроде этого:

for (int s = MAX_SHIFT; s>=0; s--)
  if (r > (b<<s)) r -= (b<<s);

Но я сомневаюсь, что ваш компилятор делает что-то намного дороже, чем это.

3 голосов
/ 11 июня 2009

Если ваш компилятор C ориентирован на ЦП без инструкции деления, вы можете изменить свой код следующим образом:

mod(a, b) {
    int s = b + b + b + b;
    int r = a;
    while(r >= s) {
        r -= s;
    }
    while(r >= b) {
        r -= b;
    }
    return r;
}

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

Это должно заставить ваш код работать примерно в четыре раза быстрее (при условии, что 4*b не находится вне диапазона ваших целых чисел). Вы могли бы даже вставить больше петель (скажем, 8*b один) перед 4*b, чтобы увеличить скорость.

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

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

int mod25 (int a) {                // a has maximum value of 2^15-1 = 32767
    while (a >= 15625) a-= 15625;  // at most 2 times.
    while (a >= 625) a-= 625;      // at most 24 times.
    while (a >= 25) a-= 25;        // at most 24 times.
    return a;
}

Выполняя тест, я обнаружил, что вам нужно сделать 10 миллионов итераций, прежде чем появится заметная разница между этим модулем кода и использованием оператора % (2 секунды против 0 секунд). До этого момента они оба составляли 0 секунд, хотя они выполнялись на быстрой машине (лучше для mod25) и с инструкцией a div (лучше для оператора %), так что вы ' мне нужно сравнить его на своем собственном оборудовании.

Это почти так же быстро, как вы, вероятно, получите, не делая ваш код нечитаемым (хотя даже это не должно вас останавливать, если вы хотите добавить множество комментариев, объясняющих, как он работает).

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

int mod (int n, int d) {
    /* dx is the adjusted denom, don't let it overflow though. */
    int dx = d;
    while (((dx << 1) >>1) == dx)
        dx <<= 1;

    /* This loop processes the dx values until they get too small. */
    while (dx >= d) {
        /* This loop subtracts the large dx value. */
        while (n >= dx)
            n -= dx;
        dx >>= 1;
    }
    return n;
}

На самом деле это работает наравне с оптимизированной версией mod25 выше, обеспечивая более общее решение.

3 голосов
/ 11 июня 2009

На многих процессорах целочисленное умножение быстрее, чем целочисленное деление. В этом блоге показано, как заменить постоянное целочисленное деление на постоянное целочисленное умножение. Немного переставив математику, вы можете получить остаток вместо отношения. Однако обратите внимание, что если вы используете умеренно сложный компилятор, то это уже сделано для вас. Вы просто пишете x % 25, а остальное компилятор решает. Вы должны проверить сгенерированный код ассемблера для своего кода, проверяя, что компилятор еще не сделал этого, прежде чем выполнять эту оптимизацию в C. Кроме того, вы должны измерить (профилировать) производительность до и после, чтобы убедиться, что вы действительно делаете вещи быстрее. .

Цикл будет намного медленнее, чем деление с использованием нативной инструкции для достаточно больших операндов.

Редактировать: см. Также этот документ .

2 голосов
/ 16 июля 2014

пожалуйста, включите здравый смысл.

Если бы вы могли написать код на C, который вычислял x% 25 быстрее, чем может компилятор, то компилятор использовал бы этот более быстрый метод.

Оригинальный постер сделал это фантастическое предположение, что компилятор будет использовать деление. Ни один компилятор, который я использовал за последние десять лет, не сделал бы этого. Это умножение на константу, близкую к (2 ^ 32/25), плюс немного биты, которые вы не сможете улучшить вручную.

Существует удаленная возможность, что вы можете создавать более быстрый код, чем компилятор, чтобы выяснить, х% 25 == 0, потому что вам на самом деле не нужен код, который будет правильно вычислять х% 25, ​​только код, который вычисляет х% 25 правильно, если оно равно 0 и не выдает 0, если x% 25! = 0. Экономия, вероятно, будет меньше наносекунды.

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

...