Трюк для деления константы (степени двух) на целое число

Примечание это теоретический вопрос. Я доволен производительностью моего фактического кода, как он есть. Мне просто интересно, есть ли альтернатива.

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

// The fixed value of the numerator
#define SIGNAL_PULSE_COUNT 0x4000UL

// The division that could use a neat trick.
uint32_t signalToReferenceRatio(uint32_t referenceCount)
{
    // Promote the numerator to a 64 bit value, shift it left by 32 so
    // the result has an adequate number of bits of precision, and divide
    // by the numerator.
    return (uint32_t)((((uint64_t)SIGNAL_PULSE_COUNT) << 32) / referenceCount);
}

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

учитывая, что числитель постоянен, и это целочисленная сила двух, есть ли аккуратный трюк, который можно использовать вместо фактического 64-битного деления; какая-то битовая операция (сдвиги и, XOR, такого рода вещи) или аналогичная?

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

"пусть компилятор решает" не является ответом, потому что я хочу знать, есть ли трюк.

Дополнительная Контекстная Информация

Я разрабатываю драйвер на 16-битных данных, 24-битный микроконтроллер с инструкцией. Водитель делает некоторую магию с периферийными модулями для получения отсчета импульсов опорной частоты для фиксированного числа импульсов частоты сигнала. Требуемым результатом является отношение импульсов сигнала к опорному импульсу, выраженное в виде 32-разрядного значения без знака. Арифметика для функции определяется производителем устройства, для которого я разрабатываю драйвер, и результат обрабатывается далее для получения реального значения с плавающей запятой, но это выходит за рамки этого вопрос.

микроконтроллер, который я использую, имеет цифровой сигнальный процессор, который имеет ряд операций разделения, которые я мог бы использовать, и я не боюсь делать это при необходимости. При таком подходе возникнут некоторые незначительные проблемы, помимо составления инструкций по сборке, чтобы заставить его работать, например DSP, используемый для выполнения функции PID в ISR драйвера BLDC, но ничего, что я не могу управлять.

4 ответов


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

  • ничто не сравнится с предварительно вычисленной таблицей поиска с точки зрения скорости.
  • существуют быстрые приближенные алгоритмы квадратного корня (возможно, уже в вашем DSP), и вы можете улучшить аппроксимацию одной или двумя итерациями Ньютона-Рафсона. Если выполнение вычисления с числами с плавающей запятой достаточно точный для вас, вы, вероятно, можете побить 64-битное целочисленное деление с точки зрения скорости (но не ясности кода).

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


Я разработал версию Matlab, используя арифметику с фиксированной точкой.

этот метод предполагает, что целочисленная версия log2(x) можно вычислить эффективно, что верно для dsPIC30/33F и TI C6000, которые имеют инструкцию для обнаружения наиболее значимого 1 целого числа.

по этой причине этот код имеет сильную степень ISA и не может быть написан на портативном / стандартном C и может быть улучшен с помощью инструкций, таких как multiply-and-add, multiply-and-shift, поэтому я не буду пытаться переводим на С.

nrdiv.м

function [ y ] = nrdiv( q, x, lut) 
                          % assume q>31, lut = 2^31/[1,1,2,...255]
    p2 = ceil(log2(x));   % available in TI C6000, instruction LMBD
                          % available in Microchip dsPIC30F/33F, instruction FF1L 
    if p2<8
        pre_shift=0;
    else
        pre_shift=p2-8;
    end                                  % shr = (p2-8)>0?(p2-8):0;

    xn = shr(x, pre_shift);              % xn  = x>>pre_shift;
    y  = shr(lut(xn), pre_shift);        % y   = lut[xn]>pre_shift; 
    y  = shr(y * (2^32 - y*x), 30);      % basic iteration
                                         % step up from q31 to q32
    y  = shr(y * (2^33 - y*x), (64-q));  % step up from q32 to desired q
    if q>39
        y = shr(y * (2^(1+q) - y*x), (q));  % when q>40, additional 
                                            % iteration is required, 
    end                                     % no step up is performed
end
function y = shr(x, r)
    y=floor(x./2^r);             % simulate operator >>
end


немного поздно, но вот мое решение.

сначала некоторые предположения:

проблема:

X=N/D, где N-константа и степень 2.

все 32 бит без подписи целых чисел.

X неизвестно, но у нас есть хорошая оценка (предыдущее, но уже не точное решение).

точное решение не требуется.

Примечание: из-за целочисленного усечения это не точный алгоритм!

итеративное решение в порядке (улучшается с каждым циклом).

деление намного дороже, чем умножение:

для 32bit беззнаковое целое для Arduino Уно:

'+/-' ~0.75 нас

' * ' ~3.5 us

' / ' ~36us 4 мы стремимся заменить в основном давайте начнем с метода Ньютона:

Xnew=Xold-f(x)/(f`(x)

где f (x)=0 для решения, которое мы ищем.

решение этого I получить:

Xnew=XNew*(C-X*D)/N

где C=2 * N

первый трюк:

теперь, когда числитель (константа) теперь делитель (константа), то одно решение здесь (которое не требует, чтобы N было степенью 2):

Xnew=XNew*(C-X*D)*A>>M

где C=2*N, A и M-константы (ищем деление на постоянные трюки).

или (оставаясь с методом Ньютона):

Xnew=XNew*(C-X*D)>>M

где C=2>>M, где m-мощность.

Итак, у меня есть 2 ' * '(7.0 us), a' -' (0.75 us) и "> > " (0.75 us?) или 8.5 US total (а не 36us), исключая другие накладные расходы.

ограничения:

поскольку тип данных 32 бит без знака, " M " не должен превышать 15, иначе возникнут проблемы с переполнением (вы, вероятно, можете обойти это, используя 64-битный промежуточный тип данных).

N>D (иначе алгоритм взорвется! по крайней мере, с целым числом без знака)

очевидно, что алгоритм будет работать с подписанными и плавающими данными типы)

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
int main(void)
{
  unsigned long c,d,m,x;
  // x=n/d where n=1<<m
  m=15;
  c=2<<m;
  d=10;
  x=10;
  while (true)
  {
    x=x*(c-d*x)>>m;
    printf("%ld",x);
    getchar();
  }
  return(0);
}

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

/*
 * Converts the reference frequency count for a specific signal frequency
 * to a ratio.
 *   Xs = Ns * 2^32 / Nr
 *   Where:
 *   2^32 is a constant scaling so that the maximum accuracy can be achieved.
 *   Ns is the number of signal counts (fixed at 0x4000 by hardware).
 *   Nr is the number of reference counts, passed in W1:W0.
 * @param  W1:W0    The number of reference frequency pulses.
 * @return W1:W0    The scaled ratio.
 */
    .align  2
    .global _signalToReferenceRatio
    .type   _signalToReferenceRatio, @function

    ; This is the position of the most significant bit of the fixed Ns (0x4000).
    .equ    LOG2_DIVIDEND,  14
    .equ    DIVISOR_LIMIT,  LOG2_DIVIDEND+1
    .equ    WORD_SIZE,      16

_signalToReferenceRatio:
    ; Create a dividend, MSB-aligned with the divisor, in W2:W3 and place the
    ; number of iterations required for the MSW in [W14] and the LSW in [W14+2].
    LNK     #4
    MUL.UU  W2, #0, W2
    FF1L    W1, W4
    ; If MSW is zero the argument is out of range.
    BRA     C, .returnZero
    SUBR    W4, #WORD_SIZE, W4
    ; Find the number of quotient MSW loops.
    ; This is effectively 1 + log2(dividend) - log2(divisor).
    SUBR    W4, #DIVISOR_LIMIT, [W14]
    BRA     NC, .returnZero
    ; Since the SUBR above is always non-negative and the C flag set, use this
    ; to set bit W3<W5> and the dividend in W2:W3 = 2^(16+W5) = 2^log2(divisor).
    BSW.C   W3, W4
    ; Use 16 quotient LSW loops.
    MOV     #WORD_SIZE, W4
    MOV     W4, [W14+2]

    ; Set up W4:W5 to hold the divisor and W0:W1 to hold the result.
    MOV.D   W0, W4
    MUL.UU  W0, #0, W0

.checkLoopCount:
    ; While the bit count is non-negative ...
    DEC     [W14], [W14]
    BRA     NC,  .nextWord

.alignQuotient:
    ; Shift the current quotient word up by one bit.
    SL      W0, W0
    ; Subtract divisor from the current dividend part.
    SUB     W2, W4, W6
    SUBB    W3, W5, W7
    ; Check if the dividend part was less than the divisor.
    BRA     NC, .didNotDivide
    ; It did divide, so set the LSB of the quotient.
    BSET    W0, #0
    ; Shift the remainder up by one bit, with the next zero in the LSB.
    SL      W7, W3
    BTSC    W6, #15
    BSET    W3, #0
    SL      W6, W2
    BRA     .checkLoopCount
.didNotDivide:
    ; Shift the next (zero) bit of the dividend into the LSB of the remainder.
    SL      W3, W3
    BTSC    W2, #15
    BSET    W3, #0
    SL      W2, W2
    BRA     .checkLoopCount

.nextWord:
    ; Test if there are any LSW bits left to calculate.
    MOV     [++W14], W6
    SUB     W6, #WORD_SIZE, [W14--]
    BRA     NC, .returnQ
    ; Decrement the remaining bit counter before writing it back.
    DEC     W6, [W14]
    ; Move the working part of the quotient up into the MSW of the result.
    MOV     W0, W1
    BRA     .alignQuotient

.returnQ:
    ; Return the quotient in W0:W1.
    ULNK
    RETURN

.returnZero:
    MUL.UU  W0, #0, W0
    ULNK
    RETURN
.size   _signalToReferenceRatio, .-_signalToReferenceRatio