移位实现常量乘除的简单优化

移位实现常量乘除的简单优化

在一些没有硬件实现乘除法指令的CPU中,我们常用移位操作来取代常量的乘除操作,运算结果向下取整。因此,位移的次数和运算的精度成为我们关注的重点。这里来谈谈几个显而易见,但是你也许会忽略的问题。

整数乘法和小数乘法


用位移实现常量整数乘法、小数乘法,都不会遇到精度问题,这是根据二进制数本身的原理来的。运算次数则根据转换算法的不同而不同。

[算法1:二进制数展开,只用加法]
最简单的方法就是把乘数展开,逐位相加。
步骤1:乘数转换成二进制
步骤2:凡是为“1”的位置,作为展开后的一项,移位的方向和次数与“1”的位置相同
步骤3:相加
例:9 = 0000 1001.
      => (x << 3)
       + (x << 0)
例:0.625 = 0.1010
      => (x >> 1)
       + (x >> 3)
[算法2:加减法逐次逼近]
步骤1:从最高位(MSB)开始逐次递减,考察该项与乘数的距离最高位(MSB)开始逐次递减,考察该项与乘数的距离
步骤2:选取最接近当前值得位(Bit)
步骤3:如果该位大于当前值,符号位选取“+”号,否则选取“-”号
步骤4:求和
例:7 = 8 - 1(按算法1则是1 + 2 + 4)
      =>  (x << 3)
        - (x << 0)
例:0.21875 = 0.25 - 0.03125(按算法1则是0.125 + 0.0625 + 0.03125) 
      =>  (x >> 2)
        - (x >> 5)
[算法1与算法2的评价]
直观的看过去,算法2利用了四舍五入的原理,因此优于算法1。我们来验证一下:

用例1:0到65535,计算位宽uint16_t。

Better: 46736
Worse : 0
Equal : 18800

用例2:1/1到1/65535,计算位宽uint16_t。

Better: 4847
Worse : 0
Equal : 60688

整数除法和小数除法


通常认为除法是乘法的逆运算,通过取得除数的倒数与原数相乘,就相当于除法。但是这一规则并不完全适用于整数除法。单独利用算法2实现除法会遇到运算精度问题,这是由于求整数的倒数会丢失精度。

[精度问题]
例如我们要计算x / 800。利用算法2,在uin16_t下我们得到:
    y = (x >> 10) + (x >> 12)

但这不是x / 800的精确解。原因在于1 / 800的完整展开式为:

0.000000000101000111101011100001010001111010111000010100011110101110000...

这个误差是多大呢?以0-65535的uint16_t来说:

Error 0: 6752
Error 1: 27328
Error 2: 25984
Error 3: 5472
Error>3: 0

当然对于精度要求不高的场合,这也足够了。

[算法3:预先左移]
好在完成除法之后,我们只需要整数部分,所以过高的运算精度毫无意义。那么多高的精度能够满足要求呢?事实上只需要预先左移适当的位数,就能大大提高运算精度。

步骤1:除数转换为二进制
步骤2:以第1次出现1的位置作为预处理阶段左移的数量
步骤3:除数左移
步骤4:使用算法2
步骤5:运算结果右移

以x / 800为例,首次出现1是在小数点后第10位,所以选择左移9位(如果左移10位就溢出了)。然后回到算法2即可。运算结果:

    x =   (x >> 1)  + (x >> 3)  + (x >> 6)
        - (x >> 11) - (x >> 13);
    x >>= 9;

此时在0-65535以及uint16_t下的误差有所下降:

Error 0: 65524
Error 1: 12
Error>1: 0

你也许会问,预先左移是否导致被除数溢出?这个问题太幼稚回家自己想,想不出来打屁股。

[算法4:循环节优化]
我们用膝盖想也知道有理数除法 + 二进制转换不可能产生无理数。细心的你也许会发现,我上面举的1 / 800的例子并非毫无规律:

1 / 800 = 0.000000000
10100011110101110000
10100011110101110000
10100011110101110000...

[作用1:提高运算精度]
我们来试着处理uint32_t的情况。使用算法3得到:

    x =   (x >> 1)  + (x >> 3)  + (x >> 6)
        - (x >> 11) - (x >> 13) - (x >> 16)
        + (x >> 21) + (x >> 23) + (x >> 26)
        - (x >> 31);
    x >>= 9;
在0-4294967295之中的误差数目是:
Error 0: 4293047518
Error 1: 1919778(万分之4)
Error>1: 0

由于意识到循环节的存在,我们改为

    x =   (x >> 1)   + (x >> 3)  + (x >> 6)
        - (x >> 11)  - (x >> 13) - (x >> 16);
    x =   (x << 0)
        + (x >> 20);
    x >>= 9;
现在0-4294967295之中的误差数目是:
Error 0: 4294567686
Error 1: 399610(十万分之9)
Error>1: 0

看来 精度提高了4.8倍。

[作用2:缩短移位次数]
循环节越短,缩短就越明显。以1 / 3为例,在uint_16的情况下:

1 / 3 = 0.010101010...

使用算法3得到:

    x =   (x >> 1)  + (x >> 3)   + (x >> 5)  + (x >> 7)
        + (x >> 9)  + (x >> 11)  + (x >> 13) + (x >> 15);
    x >>= 1;
现在利用算法4可以二分展开:
    x = (x >> 1) + (x >> 3);
    x = x + (x >> 4);
    x = x + (x >> 8);
    x >>= 1;
在这个例子中,算法4的精度同样高于算法3,但我想这应该是运气因素。如果你问我为什么不直接(x >> 2) + (x >> 4),那么上一节你显然没看。

[关于人肉优化的必要性]


由于人肉优化不具有一般性,不是这篇文章讨论的范畴。但是考虑到但凡需要使用移位来模拟乘除的场合,往往是性能关键的部分,所以还是很有必要的。

<think>嗯,用户问的是如何用Verilog写小数的乘除运算。首先,我需要明确用户所说的“小数”指的是什么。通常,在数字电路中处理小数有两种方法:定点数和浮点数。浮点数比较复杂,而定点数则是通过固定小数点位置来表示小数,可能更适合Verilog初学者或者资源受限的设计。 接下来,我应该考虑用户可能的背景。可能是学生或者刚开始学习数字电路设计的工程师,想要实现基本的运算,但不确定如何着手。用户可能没有太多硬件设计的经验,所以需要给出一个简单易懂的例子,同时解释清楚原理。 需要解释定点数的表示方法,比如Q格式,例如Q8.8表示8位整数和8位小数部分。然后,乘法的话,两个Qm.n格式的数相乘,结果会是Q(2m).(2n),所以需要截断或舍入来保持位数一致。除法的话更复杂,可能需要使用牛顿-拉夫逊方法或查找表,但简单的例子可能用移位和减法来实现。 用户可能需要具体的代码示例,所以给出乘法器的例子,说明如何调整小数点的位置。对于除法,可能指出其复杂性,建议使用现有IP核或者算法,但也可以给出一个简化的定点数除法例子,比如将被除数左移再除以除数,然后调整结果的小数点位置。 还要注意提醒用户关于资源消耗和精度的问题,以及测试的重要性。可能用户在实际应用中会遇到溢出或精度损失的问题,所以需要强调测试和验证的必要性。 最后,总结一下,提供乘法和除法的简单实现方法,并建议进一步学习资料或方法,帮助用户扩展知识。</think>在数字电路中,小数的运算通常通过**定点数(Fixed-Point)**或**浮点数(Floating-Point)**实现。Verilog 更擅长定点数运算(硬件开销小),而浮点数需要复杂硬件支持(如 IEEE 754 标准)。以下是一个基于 **Q 格式定点数** 的实现思路和示例: --- ### 一、定点数表示(Q 格式) 假设用 **Qm.n** 表示: - `m`:整数部分位宽 - `n`:小数部分位宽 例如 `Q8.8` 表示 8 位整数 + 8 位小数(总 16 位)。 --- ### 二、乘法运算 #### 1. 核心原理 - 两个 `Qm.n` 数相乘后,结果为 `Q(2m).2n` 格式。 - 需截断/舍入回 `Qm.n` 格式以保持位宽一致。 #### 2. Verilog 示例(Q8.8 乘法) ```verilog module fixed_mult ( input signed [15:0] a, // Q8.8 input signed [15:0] b, // Q8.8 output signed [15:0] y // Q8.8 ); // 中间结果:Q16.16(32 位) wire signed [31:0] product = a * b; // 截断到 Q8.8:取中间 16 位(高位可能溢出需注意!) assign y = product[23:8]; // 取 24~8 位(含小数部分) endmodule ``` --- ### 三、除法运算 #### 1. 核心难点 - 除法硬件开销大,需迭代算法(如牛顿-拉夫逊法)或查找表。 - 简单实现:将被除数左移扩展小数位,再执行整数除法。 #### 2. Verilog 示例(Q8.8 除法) ```verilog module fixed_div ( input signed [15:0] dividend, // Q8.8 input signed [15:0] divisor, // Q8.8 output signed [15:0] quotient // Q8.8 ); // 扩展被除数到 32 位(左移 16 位) wire signed [31:0] ext_dividend = dividend << 16; // 执行整数除法(结果格式 Q24.8) reg signed [31:0] div_result; always @(*) begin if (divisor != 0) div_result = ext_dividend / divisor; else div_result = 0; // 处理除零错误 end // 截断到 Q8.8 assign quotient = div_result[15:0]; // 取低 16 位 endmodule ``` --- ### 四、关键注意事项 1. **溢出处理**:定点数乘法可能导致溢出,需设计饱和逻辑或增加保护位。 2. **精度损失**:截断操作会损失精度,可根据需求使用四舍五入。 3. **符号处理**:使用 `signed` 类型自动处理符号位。 4. **资源消耗**:除法器硬件开销大,实际工程中常用 IP 核或算法优化。 5. **测试验证**:必须通过仿真测试验证边界条件(如最大值、最小值、除零)。 --- ### 五、扩展建议 - **浮点数实现**:需实现 IEEE 754 标准的加减乘除模块(复杂度高)。 - **使用 IP 核**:Xilinx/Altera 提供优化过的乘除 IP 核。 - **SystemVerilog 增强**:可用 `real` 类型仿真小数,但不可综合。 建议先用仿真工具(如 ModelSim)验证逻辑,再综合到 FPGA 测试实际时序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值