将常数(2 的幂)除以整数的技巧

Trick to divide a constant (power of two) by an integer

注意 这是一道理论题。我对实际代码的性能感到满意。我只是好奇是否有替代方案。

是否有技巧可以将一个常量值除以一个整数变量值,而该常量值本身就是 2 的整数次幂,而无需使用实际的除法运算?

// 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);
}

我找到了几个(很多)关于除以常数的技巧的参考资料,包括整数和浮点数。例如,问题 What's the fastest way to divide an integer by 3? 有许多很好的答案,包括对其他学术和社区材料的引用。

鉴于分子是常数,并且是 2 的整数次方,是否有一个巧妙的技巧可以用来代替实际的 64 位除法;某种按位运算(移位、AND、XOR 之类的东西)或类似的?

我不希望精度损失(由于整数舍入而超过可能的半位)大于实际除法的精度,因为仪器的精度取决于此测量的精度。

"Let the compiler decide"不是答案,因为我想知道有没有技巧。

额外的上下文信息

我正在开发 16 位数据、24 位指令字微控制器的驱动程序。驱动程序对外围模块进行一些操作,以获得固定数量的信号频率脉冲的参考频率脉冲计数。所需的结果是信号脉冲与参考脉冲的比率,表示为无符号的 32 位值。该函数的算法由我正在为其开发驱动程序的设备的制造商定义,并且进一步处理结果以获得浮点实际值,但这超出了这个问题的范围。

我正在使用的微控制器有一个数字信号处理器,它有许多我可以使用的除法运算,如果有必要我不怕这样做。除了将汇编指令放在一起以使其工作之外,这种方法还需要克服一些小挑战,例如 DSP 用于在 BLDC 驱动程序 ISR 中执行 PID 功能,但没有什么是我无法管理的。

您不能使用巧妙的数学技巧来不做除法,但如果您知道引用计数的范围,您当然仍然可以使用编程技巧:

  • 就速度而言,没有什么能比得上预先计算的查找 table。
  • 有快速近似平方根算法(可能已经在您的 DSP 中),您可以通过一到两次 Newton-Raphson 迭代改进近似值。如果使用浮点数进行计算对您来说足够准确,那么您可能在速度方面(但在代码清晰度方面)优于 64 位整数除法。

您提到稍后会将结果转换为浮点数,完全不计算整数除法但使用您的浮点硬件可能会有所帮助。

我开发了一个 Matlab 版本,使用定点算法。

此方法假设log2(x)的整数版本可以有效计算,这适用于dsPIC30/33F和TI C6000,它们具有检测整数最高有效1的指令。

因此,此代码具有很强的 ISA 依赖性,不能用 portable/standard C 编写,可以使用乘加、乘移等指令进行改进,所以我不会' t 尝试将其翻译成 C.

nrdiv.m

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

test.m

test_number = (2^22-12345);
test_q      = 48;

lut_q31 = round(2^31 ./ [1,[1:1:255]]);
display(sprintf('tested 2^%d/%d, diff=%f\n',test_q, test_number,...
                 nrdiv( 39, (2^22-5), lut_q31) - 2^39/(2^22-5)));

示例输出

tested 2^48/4181959, diff=-0.156250

参考:

Newton–Raphson division

有点晚了,但这是我的解决方案。

首先是一些假设:

问题:

X=N/D 其中 N 是常数和 2 的幂。

所有 32 位 无符号 整数。

X 未知,但我们有一个很好的估计 (以前但不再准确的解决方案)。

不需要精确解。

注意:由于整数截断,这不是一个准确的算法!

迭代解决方案是可以的(每次循环都会改进)。

除法比乘法昂贵得多:

对于 Arduino UNO 的 32 位无符号整数:

'+/-'~0.75us

'*'~3.5us

'/' ~36us 4 我们寻求替换 基本上让我们从牛顿法开始:

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

其中 f(x)=0 表示我们寻求的解决方案。

解决这个我得到:

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.0us)、一个“-”(0.75us)和一个“>>”(0.75us?)或总共 8.5us(而不是 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