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

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


person Thomas O    schedule 05.04.2011    source источник
comment
Это возможно (повторное вычитание - это деление), но вопрос в том, быстрее ли оно, чем медленное деление.   -  person David Thornley    schedule 06.04.2011
comment
@esnyder. Извините, я вас не понимаю. Вы говорите по базе 17 или 22?   -  person Thomas O    schedule 06.04.2011
comment
База большая два. Сдвиг вправо делится на 2 ^ n, что решит ваш вопрос, если под 10 вы имеете в виду 16 десятичных или 10h.   -  person tamarintech    schedule 06.04.2011
comment
Вы со мной спорите? На самом деле я пытаюсь признать, что я не упомянул, что мой ответ был не для десятичных чисел .... Может быть, это немного неясно, но это было моим намерением.   -  person tamarintech    schedule 06.04.2011
comment
@Thomas O - см. Мой комментарий. Я не замечаю голоса за ....   -  person KevinDTimm    schedule 06.04.2011
comment
@esynder, да, наверное, я спорил с вами, по поводу интерпретации 10 (база 10) как 10 (база 16). Я думаю, что такая интерпретация по умолчанию в лучшем случае необычна.   -  person Thomas O    schedule 06.04.2011
comment
По теме: Почему GCC использует умножение на странное число при реализации целочисленного деления?: Если у вас есть быстрое умножение, вы можете разделить на константы времени компиляции просто умножением и сдвигом высокой половины, получая правильный результат для каждого делимого ( в отличие от текущего принятого ответа).   -  person Peter Cordes    schedule 19.10.2017


Ответы (9)


Примечание редактора: это не, на самом деле компиляторы, а дает неправильный ответ для больших положительных целых чисел, оканчивающихся на 9, начиная с div10(1073741829) = 107374183, а не с 107374182. Это точно для меньших входных данных, хотя этого может быть достаточно для некоторых применений.

Компиляторы (включая MSVC) действительно используют мультипликативные инверсии с фиксированной точкой для постоянных делителей, но они используют другую магическую константу и сдвигают результат с высокой половиной, чтобы получить точный результат для всех возможных входов, соответствующий тому, что требуется абстрактной машине C. См. статью Гранлунда и Монтгомери об алгоритме.

См. Почему GCC использует умножение странным числом при реализации целочисленного деления? на примерах реальных x86 asm gcc, clang, MSVC, ICC и других современных компиляторов.


Это быстрое приближение, которое неточно для больших входных данных.

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

Вы можете использовать высокую половину результата умножения для деления на малые интегральные константы. Предположим, что на 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 будет желаемым значением. Визуализация (предполагается, что дивиденды передаются в 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

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

person John Källén    schedule 05.04.2011
comment
+1, и я хотел бы подчеркнуть, что компилятор сделает это за вас автоматически, когда вы напишете x / 10 - person Theran; 06.04.2011
comment
хм, а нет ли здесь числовой неточности? - person Jason S; 06.04.2011
comment
Вы всегда будете иметь числовую неточность при делении целых чисел: что вы получите, если разделите 28 на 10 с помощью целых чисел? Ответ: 2. - person John Källén; 06.04.2011
comment
В целочисленном делении нет числовой погрешности, результат указан точно. Однако приведенная выше формула точна только для некоторых делителей. Даже 10 будет неточным, если вы хотите выполнить арифметику без знака: 4294967219 / 10 = 429496721, но 4294967219 * div >> 32 = 429496722 Для больших делителей подписанная версия также будет неточной. - person Evan; 04.07.2017
comment
@Theran: Нет, компиляторы, включая MSVC, будут компилировать x/10 в мультипликативный обратный алгоритм с фиксированной точкой (и создать дополнительный код для обработки отрицательных входных данных для деления со знаком), чтобы дать правильный ответ для всех возможных 32-битных входных данных. Для беззнакового деления на 10 MSVC (и другие компиляторы) (godbolt.org/g/aAq7jx) будут умножьте на 0xcccccccd и сдвиньте верхнюю половину вправо на 3. - person Peter Cordes; 19.10.2017
comment
Я написал тестовую программу для этого, сравнив результаты с i/10. Это неправильно для больших положительных целых чисел, заканчивающихся на 9, начиная с div10(1073741829) = 107374183. Correct = 107374182. Это также неправильно для большинства (всех?) Отрицательных целых чисел, например. div10(-1) = -1. Correct = 0. @JasonS правильно, что это не реализует семантику C x / 10. - person Peter Cordes; 19.10.2017
comment
@PeterCordes есть отличное видео Мэтта Годболта, в котором говорится о том, что компилятор делает для деления; иногда используется умножение. см. youtube.com/watch?v=bSkpMdDe4g4. - person Jason S; 19.10.2017

Хотя приведенные до сих пор ответы соответствуют фактическому вопросу, они не соответствуют названию. Итак, вот решение, в значительной степени вдохновленное Hacker's 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);
}

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

person realtime    schedule 29.09.2013
comment
pdf больше не доступен - person Alexis; 27.05.2021
comment
как мы можем адаптировать его для 10 ^ N? - person Alexis; 27.05.2021
comment
Исходный сайт мертв, ссылка теперь указывает на заархивированную версию в Wayback Machine. В связанном PDF-файле вы найдете код для деления на 100 и 1000. Имейте в виду, что они все еще содержат операцию умножения, которую необходимо заменить сдвигами и сложениями. Кроме того, код divu100 и divu1000 содержит много сдвигов, которые не кратны 8, поэтому, если вы работаете в архитектуре, в которой нет ни сдвигающего устройства, ни инструкции Muliply, вам может быть лучше, применяя divu10 несколько раз. - person realtime; 28.05.2021
comment
Спасибо! Это для FPGA / RTL, я адаптирую его в зависимости от времени, которое я смогу получить. Я просто нашел ссылку на этот pdf буквально везде, где задают такой вопрос. Без возможности найти сам файл. Еще раз спасибо! - person Alexis; 28.05.2021

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

temp = (ms * 205) >> 11;  // 205/2048 is nearly the same as /10
person Alois Kraus    schedule 05.04.2011
comment
Вы должны знать, что промежуточное значение (ms * 205) может переполняться. - person Paul R; 06.04.2011
comment
Если вы сделаете int ms = 205 * (i ›› 11); вы получите неправильные значения, если числа маленькие. Вам нужен набор тестов, чтобы убедиться, что в заданном диапазоне значений результаты верны. - person Alois Kraus; 06.04.2011
comment
это верно для ms = 0..1028 - person robert king; 18.10.2014
comment
Почему именно 205? - person ernesto; 26.11.2017
comment
@ernesto ›› 11 - это деление 2048. Если вы хотите разделить на десять, вам нужно разделить это на 2048/10, что составляет 204,8 или 205 как ближайшее целое число. - person Alois Kraus; 26.11.2017
comment
Мне просто нравится простота, это действительно отличная идея - person e271p314; 11.05.2018
comment
То же самое для 0 ‹= ms‹ 1029. - person gary; 19.06.2019
comment
А для 0 ‹= ms‹ 179 это можно сделать даже с 10 вместо 11 смен: temp = (ms * 103) >> 10; - person dionoid; 11.06.2020

чтобы немного расширить ответ Алоиса, мы можем расширить предлагаемый 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. если мы построим график ошибок, мы получим следующее:

ошибки

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

person Sam Mason    schedule 20.05.2019
comment
Что ms в вашем тесте? - person Alexis; 27.05.2021
comment
@Alexis Я позаимствовал это имя из ответа Алоиса, это просто значение, которое вы хотите разделить. может это сокращение от multiply shift? - person Sam Mason; 27.05.2021
comment
Я понимаю, но каково значение комментария в каждой строке? - person Alexis; 27.05.2021
comment
@Alexis не уверен, что могу объяснить что-то лучше, чем абзац под блоком ... это первое значение ms, которое даст неправильный ответ, т.е. параметры работают для любого значения ‹комментарий - person Sam Mason; 27.05.2021
comment
жаль, что я не понял его при первом чтении. Спасибо! - person Alexis; 27.05.2021

На архитектурах, которые могут сдвигать только одно место за раз, серия явных сравнений с уменьшающейся степенью двойки, умноженной на 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;
}
person Kuba hasn't forgotten Monica    schedule 14.12.2015
comment
Ваш код выполняет умножение 16 на 10. Как вы думаете, почему ваш код быстрее, чем радость хакера? - person chmike; 07.10.2017
comment
Неважно, что я думаю. Важно то, будет ли это на соответствующей платформе быстрее. Попробуйте сами! Здесь вообще нет универсального самого быстрого решения. Каждое решение имеет в виду определенную платформу и будет работать лучше всего на этой платформе, возможно, лучше, чем любое другое решение. - person Kuba hasn't forgotten Monica; 18.10.2017
comment
Я не заметил, что n * 10 постоянно. Таким образом, он будет предварительно вычислен компилятором. В ответ я предложил альтернативный алгоритм. Наши алгоритмы эквивалентны, за исключением одного отличия. Вы вычитаете b * 10 из v, а я прибавляю его к x * 10. Вашему алгоритму не нужно отслеживать x * 10, который сохраняет переменную. Код, который вы показываете, разворачивает цикл my while. - person chmike; 18.10.2017
comment
@chmike: На машине без аппаратного умножения n*10 все еще дешево: (n<<3) + (n<<1). Эти ответы с небольшим сдвигом могут быть полезны на машинах с медленным или отсутствующим HW умножением и только сдвигом на 1. В противном случае обратная фиксированная точка намного лучше для делителей констант времени компиляции (как современные компиляторы делают для x/10) . - person Peter Cordes; 19.10.2017
comment
Это отличное решение, особенно полезное для процессоров, у которых нет правого сдвига (например, LC-3). - person Erik Eidt; 28.02.2020
comment
@ErikEidt: ответ njuffa на Как я могу умножать и делить, используя только битовый сдвиг и сложение? также не требует сдвига вправо , только сравнение adc или без знака для обнаружения переноса вручную и для фактического условного. - person Peter Cordes; 02.12.2020

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

Допустим, нам нужно найти x, где x = v / 10.

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

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

Мы можем избежать умножения на 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

Изменить: чтобы получить алгоритм Куба Обера, который позволяет избежать использования переменной 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 могут быть предварительно вычислены как константы.

person chmike    schedule 18.10.2017
comment
Э ... это просто деление в столбик (да, это то, чему вы научились в начальной школе) для двоичного, а не для десятичного числа. - person liyang; 05.03.2021
comment
Я не знаю, что вы называете длинным делением. В чем я уверен, так это в том, что я не учился этому в школе. В школе я учусь по другому методу. - person chmike; 05.03.2021
comment
Я имею в виду en.wikipedia.org/wiki/Long_division#Method, но где метод просит вас «получить наибольшее целое число, кратное делителю», просто имейте в виду, что кратное может быть только 1 или 0 при работе с основанием-2. Ваш тест на b10 <= v просто проверяет, является ли указанное кратное 1. В любом случае, несколько лет назад я преподавал долгое деление для курса архитектуры компьютерных систем. Какой метод десятичного деления в столбик вы изучали в школе? - person liyang; 16.03.2021
comment
В качестве примечания: это объективно проще, чем десятичное деление в столбик, поскольку вы никогда не спросите себя, например, «Сколько раз 3 делит 8?» - в основании 2 он либо делает ровно один раз без остатка, либо не делает его совсем. Единственное, что делает это менее интуитивно понятным, - это наше относительное знакомство с базой-10, в отличие от работы с базой-2. - person liyang; 16.03.2021

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

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

person tvanfosson    schedule 05.04.2011

Я разработал новый метод сборки AVR, только с lsr / ror и sub / sbc. Он делится на 8, затем сутрактирует число, разделенное на 64 и 128, затем вычитает 1024-е и 2048-е, и так далее и так далее. Работает очень надежно (с точным округлением) и быстро (370 микросекунд на 1 МГц). Исходный код для 16-битных чисел находится здесь: http://www.avr-asm-tutorial.net/avr_en/beginner/DIV10/div10_16rd.asm Страница с комментариями к этому исходному коду находится здесь: http://www.avr-asm-tutorial.net/avr_en/beginner/DIV10/DIV10.html Я надеюсь что это помогает, хотя этому вопросу уже десять лет. brgs, gsc

person Gerhard    schedule 20.06.2021

Код комментариев elemakil можно найти здесь: https://doc.lagout.org/security/Hackers%20Delight.pdf стр. 233. Беззнаковое деление на 10 [и 11.]

person milkpirate    schedule 12.06.2020
comment
Ответы только по ссылкам - это не то, о чем идет речь. Если это касается метода, описанного в другом ответе, вы можете оставить комментарий или сделать предлагаемую штольню. Но этого недостаточно, чтобы быть самостоятельным ответом. В качестве альтернативы вы можете процитировать или обобщить некоторые из того, что в нем говорится, и выделить ключевые части, если это даст минимальный ответ, даже если ссылка разорвется. - person Peter Cordes; 13.06.2020