Разделить на 10 с помощью сдвигов бит?

Можно ли разделить целое число без знака на 10, используя чистые сдвиги бит, сложение, вычитание и, возможно, умножение? Использование процессора с очень ограниченными ресурсами и медленным делением.

Ответы

Ответ 1

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

int32_t div10(int32_t dividend)
{
    int64_t invDivisor = 0x1999999A;
    return (int32_t) ((invDivisor * dividend) >> 32);
}

Что здесь происходит, мы умножаемся на близкое приближение 1/10 * 2 ^ 32, а затем удаляем 2 ^ 32. Этот подход может быть адаптирован к разным делителям и разной ширине бит.

Это отлично работает для архитектуры ia32, так как его команда IMUL поместит 64-разрядный продукт в edx: eax, а значение edx будет желаемым. Viz (при условии, что дивиденд передается в eax, а фактор возвращается в eax)

div10 proc 
    mov    edx,1999999Ah    ; load 1/10 * 2^32
    imul   eax              ; edx:eax = dividend / 10 * 2 ^32
    mov    eax,edx          ; eax = dividend / 10
    ret
    endp

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

Ответ 2

Хотя ответы, полученные до сих пор, соответствуют фактическому вопросу, они не соответствуют названию. Итак, вот решение, сильно вдохновленное Hacker Delight, которое действительно использует только бит-сдвиги.

unsigned divu10(unsigned n) {
    unsigned q, r;
    q = (n >> 1) + (n >> 2);
    q = q + (q >> 4);
    q = q + (q >> 8);
    q = q + (q >> 16);
    q = q >> 3;
    r = n - (((q << 2) + q) << 1);
    return q + (r > 9);
}

Я думаю, что это лучшее решение для архитектур, которым не хватает команды multiply.

Ответ 3

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

temp = (ms * 205) >> 11;  // 205/2048 is nearly the same as /10

Ответ 4

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

Скажем, нам нужно найти x где x = v / 10.

Хорошо используйте обратную операцию v = x * 10, потому что она имеет свойство nice, когда x = a + b, затем x * 10 = a * 10 + b * 10.

Используйте x как переменную, которая наилучшим образом приближается к результату. Когда поиск заканчивается, x Будет удерживать результат. Ну, установите каждый бит b из x от самого значимого до менее значимого, один за другим, сравните (x + b) * 10 с v. Если его меньше или равно v, тогда бит b устанавливается в x. Чтобы проверить следующий бит, мы просто сдвигаем одну позицию вправо (разделим на две части).

Мы можем избежать умножения на 10, удерживая x * 10 и b * 10 в других переменных.

Это дает следующий алгоритм для деления v на 10.

uin16_t x = 0, x10 = 0, b = 0x1000, b10 = 0xA000;
while (b != 0) {
    uint16_t t = x10 + b10;
    if (t <= v) {
        x10 = t;
        x |= b;
    }
    b10 >>= 1;
    b >>= 1;
}
// x = v / 10

Изменить:, чтобы получить алгоритм Kuba Ober, который позволяет избежать необходимости переменной x10, мы можем вычесть b10 из v и v10. В этом случае x10 больше не требуется. Алгоритм становится

uin16_t x = 0, b = 0x1000, b10 = 0xA000;
while (b != 0) {
    if (b10 <= v) {
        v -= b10;
        x |= b;
    }
    b10 >>= 1;
    b >>= 1;
}
// x = v / 10

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

Ответ 5

Деление скважины является вычитанием, так что да. Сдвиг вправо на 1 (разделите на 2). Теперь вычитаем 5 из результата, подсчитывая количество вычетов, пока значение будет меньше 5. Результатом будет количество вычитаемых вычетов. О, и деление, вероятно, будет быстрее.

Гибридная стратегия сдвига справа, а затем деление на 5 с использованием нормального деления может привести к повышению производительности, если логика в делителе уже не делает этого для вас.

Ответ 6

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

uint16_t div10(uint16_t dividend) {
  uint16_t quotient = 0;
  #define div10_step(n) \
    do { if (dividend >= (n*10)) { quotient += n; dividend -= n*10; } } while (0)
  div10_step(0x1000);
  div10_step(0x0800);
  div10_step(0x0400);
  div10_step(0x0200);
  div10_step(0x0100);
  div10_step(0x0080);
  div10_step(0x0040);
  div10_step(0x0020);
  div10_step(0x0010);
  div10_step(0x0008);
  div10_step(0x0004);
  div10_step(0x0002);
  div10_step(0x0001);
  #undef div10_step
  if (dividend >= 5) ++quotient; // round the result (optional)
  return quotient;
}

Ответ 7

чтобы немного расширить ответ Алоиса, мы можем расширить предложенный y = (x * 205) >> 11 на несколько кратных/сдвигов:

y = (ms *        1) >>  3 // first error 8
y = (ms *        2) >>  4 // 8
y = (ms *        4) >>  5 // 8
y = (ms *        7) >>  6 // 19
y = (ms *       13) >>  7 // 69
y = (ms *       26) >>  8 // 69
y = (ms *       52) >>  9 // 69
y = (ms *      103) >> 10 // 179
y = (ms *      205) >> 11 // 1029
y = (ms *      410) >> 12 // 1029
y = (ms *      820) >> 13 // 1029
y = (ms *     1639) >> 14 // 2739
y = (ms *     3277) >> 15 // 16389
y = (ms *     6554) >> 16 // 16389
y = (ms *    13108) >> 17 // 16389
y = (ms *    26215) >> 18 // 43699
y = (ms *    52429) >> 19 // 262149
y = (ms *   104858) >> 20 // 262149
y = (ms *   209716) >> 21 // 262149
y = (ms *   419431) >> 22 // 699059
y = (ms *   838861) >> 23 // 4194309
y = (ms *  1677722) >> 24 // 4194309
y = (ms *  3355444) >> 25 // 4194309
y = (ms *  6710887) >> 26 // 11184819
y = (ms * 13421773) >> 27 // 67108869

каждая строка представляет собой отдельный независимый расчет, и вы увидите свою первую "ошибку"/неверный результат со значением, указанным в комментарии. как правило, лучше брать наименьшее смещение для данного значения ошибки, так как это сведет к минимуму дополнительные биты, необходимые для сохранения промежуточного значения в вычислениях, например (x * 13) >> 7 "лучше", чем (x * 52) >> 9 как для него требуется на два бита меньше, в то время как оба начинают давать неправильные ответы выше 68.

если вы хотите рассчитать больше из них, можно использовать следующий (Python) код:

def mul_from_shift(shift):
    mid = 2**shift + 5.
    return int(round(mid / 10.))

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

def first_err(mul, shift):
    i = 1
    while True:
        y = (i * mul) >> shift
        if y != i // 10:
            return i
        i += 1

(обратите внимание, что // используется для "целочисленного" деления, т.е. оно усекает/округляет до нуля)

причина шаблона "3/1" в ошибках (то есть 8 повторений 3 раза, а затем 9), по-видимому, связана с изменением баз, то есть log2(10) составляет ~ 3,32. если мы отобразим ошибки, мы получим следующее:

errors

где относительная погрешность определяется как: mul_from_shift(shift)/(1<<shift) - 0.1