ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Booth乘法器基2与基4对比:Verilog实现与FPGA综合性能实测

Booth乘法器基2与基4对比:Verilog实现与FPGA综合性能实测 我前阵子把用了两年的16位基2 Booth乘法器换成基4版本综合完扫了一眼report_utilization差点想把以前的代码全部删掉。同样完成16位补码乘法基4版本LUT少了近三成关键路径短了将近两成七。这不是我调参数调出来的差距而是算法结构从“一次挪一位”变成“一次挪两位”之后天然产生的红利。这篇文章就用Verilog实现、仿真和综合实测把基2和基4两种Booth乘法器放在一起跑分看看Radix-4到底赢在哪以及为什么你在现代芯片的数据通路里几乎找不到纯基2的实现。如果你正在学数字IC设计或者刚写完一个Booth乘法器觉得“性能还行”这篇会比较解气如果你还没开始写那正好直接从Radix-4起步省得走我走过的弯路。1. 从一次综合实验开始学校的Booth乘法器为何没法直接上芯片1.1 最初基于2的版本与综合噩梦本科教材讲补码乘法时几乎清一色先讲基2 Booth算法理由很好理解它只需要看相邻两位的差值编码规则简单推导过程也容易画成状态图。于是我当时很自然地在RTL里写了一个基2版本仿真全对测试向量也过特别高兴。等我把这个模块放到Vivado里做真实综合问题就出来了。16位乘法器部分积有16行我用generate语句把每一行都生成出来再用一个大的always (*)循环累加。工具确实能综合但报告里的关键路径惨不忍睹纯组合逻辑延迟接近10ns。放到100MHz时钟下根本收不了时序必须拆成两拍甚至三拍流水而这样一来整个数据通路的面积和寄存器开销又上去了。后来才意识到问题所在我写的循环累加在综合器眼里是一条长长的加法链不是并行加法树。部分积16行每行都是一个16位以上的有符号数把它们串行加在一起延迟自然按加法器数量线性累积。这个问题跟Booth算法本身无关是我把“算法级概念”直接翻译成“RTL级实现”时踩的典型坑。1.2 跑分的触发点——同样功能面积时序差一截有个做AI加速器的同事跟我说他们IP里的整数乘法器基本不用基2全部是Radix-4加Wallace树。我不太信觉得编码逻辑多了一倍面积应该更差才对。但实测结果打脸了。我把基4版本写出来后在同样的器件、同样的约束和同样的测试激励下跑了一遍。结果让我把那版基2代码的迭代记录翻出来反复确认了两遍16位乘法器部分积行数从16行降到8行LUT少了30%以上关键路径从接近10ns掉到7ns左右。关键是部分积行数减半这件事直接让后面的压缩树少了一层这才是Radix-4真正值钱的地方。所以这篇文章里所有“跑分”指的都是RTL在真实FPGA工具链里的综合结果不是iverilog仿真时长也不是某个功能仿真里冒出来的波形。Verilog跑分这种事跑不出门级真实延迟就没什么参考价值。2. 基2与基4的编码差异从一次挪一位到一次挪两位2.1 Booth算法的本质用差分类比消除连续1Booth算法的出发点是补码表示里连续1串可以替换成一次减法加一次加法。比如乘数B 8‘b01110十进制14如果直接按位做乘法需要处理4个非零位但14等于16 - 2也就是10000 - 00010。用减法代替连续加法可以把一连串的1折叠成两个操作。基2 Booth编码就是把这个思想变成硬件的判断规则每一轮同时观察乘数当前位Yi和低一位Yi-1。如果这两位不同说明正好处于连续1串的起点或终点做一次加法或减法如果相同说明仍在连续区域中间跳过即可。于是基2的编码表只有四行当前位Yi低位Yi-1操作00部分积为001加被乘数A10减被乘数A11部分积为0每次判断完乘数右移一位部分积相对于前一行左移一位。W位的乘法就要生成W行部分积。这就是基2 Booth“一次挪一位”的含义。2.2 改进Booth编码表三位一组决定部分积基4 Modified Booth编码更进一步每次观察乘数的三位当前两位加上低一位辅助位。因为一次判断2位乘数所以移位步长也变成2位部分积行数直接变成W/2。编码表长这样乘数三比特操作0000001A010A0112A100-2A101-A110-A1110这里最关键的收益是部分积从“0、A、-A”三种选择变成“0、±A、±2A”五种选择但2A只需要把A左移一位硬件成本就是一根移位连线加符号处理几乎可以忽略。为什么这五种就够了因为任意连续两位的乘数组合最多只需要两种幅度的被乘数做加减。如果要继续提速可以用基8或更高阶但对选择集的要求立刻变高后面第5章会专门说为什么不划算。2.3 一个8位算例手推两种算法纸上谈兵不行我们拿真实数走一遍。设A 8’b11111001-7B 8‘b000001106结果应该是 -42。先处理B的辅助位B_ext {B, 1’b0} 9‘b000001100初始最低位是0这是Booth算法约好的“低位哨兵”。基2的运算过程是从最低位开始逐位取两位判断最终会生成8行部分积部分积之间相邻左移1位然后把8行全部加起来。基4的过程就简单很多B_ext仍然是9’b000001100从低位开始每次看三位第0轮取 bits[2:0] 100查表得 -2A。A -7-2A 14放在第0位移第1轮取 bits[4:2] 011查表得 2A。2A -14左移2位后是 -56第2轮取 bits[6:4] 000查表得 0左移4位第3轮取 bits[8:6] 000查表得 0左移6位。最终结果 14 (-56) 0 0 -42完全正确。基2要做8轮判断基4只需要4轮这还只是8位乘法。到了16位、32位部分积行数差的是8行和16行压缩树的高度直接差出一整层。3. 两个Verilog实现结构差距究竟在哪里3.1 基2版RTL的循环移位累加写法先放一个我早期写的基2核心逻辑伪代码它很直观但综合效果一般reg [W:0] b_ext; reg [2*W-1:0] acc; integer i; always (*) begin acc 0; b_ext {b, 1b0}; for (i 0; i W; i i 1) begin case ({b_ext[i1], b_ext[i]}) 2b01: acc acc (signed_a i); 2b10: acc acc - (signed_a i); default: acc acc; endcase end product acc; end这段代码仿真完全没有问题跑随机向量也能对。问题在综合时Vivado大概率会把它映射成一条很长的加法链而不是并行压缩树。因为循环里的acc acc ...存在明显的串行依赖工具很难自动识别成Wallace树。如果非要保留基2结构正确做法是把每一行部分积独立生成放成一个数组再用压缩树把它们并行相加。伪代码长这样wire [2*W-1:0] pp [0:W-1]; genvar i; generate for (i 0; i W; i i 1) begin: pp_gen wire op b_ext[i1] ^ b_ext[i]; assign pp[i] op ? ((b_ext[i1] 1b1 ? -signed_a : signed_a) i) : d0; end endgenerate但即使改成这种并行部分积版本基2依然有W行部分积压缩树层数依然比基4多。3.2 基4版RTL的部分积生成与符号扩展折叠基4版本的核心区别在于部分积生成模块。我通常用一个generate循环每个循环体生成一行部分积wire [2*W-1:0] pp [0:W/2-1]; genvar i; generate for (i 0; i W/2; i i 1) begin: pp_gen wire [2:0] sel {b_ext[2*i2], b_ext[2*i1], b_ext[2*i]}; wire [2*W-1:0] pp_shifted; always (*) begin case (sel) 3b000, 3b111: pp[i] d0; 3b001, 3b010: pp[i] signed_a; 3b011: pp[i] signed_a 1; 3b100: pp[i] -(signed_a 1); 3b101, 3b110: pp[i] -signed_a; default: pp[i] d0; endcase end assign pp_shifted {{(2*W-2*i-1){pp[i][2*W-1]}}, pp[i], {2*i{1b0}}}; end endgenerate这里有个隐藏细节signed_a 1是算术左移对负数来说低位补0符号位保持不变结果刚好是2倍被乘数。如果用逻辑左移符号位会被移出去负数直接就错了。部分积行数变成W/2后后续的累加可以简单写成reg [2*W-1:0] sum; integer j; always (*) begin sum 0; for (j 0; j W/2; j j 1) sum sum pp_shifted[j]; product sum; end这种写法在综合时已经比基2那种串行累加好了不少因为编译器面对的是8个独立部分积相加更容易识别成平衡的加法树。再往下走就是把这段累加替换成4-2压缩器或者Wallace树这也是纯工程优化跟Booth编码本身关系不大。3.3 符号扩展折叠比想象中更能省资源上面代码里我把每个部分积都显式扩展成2W位再相加这个写法最安全但不够省。16位乘法时每一行要扩展出很大一片符号位8行的符号扩展加起来就是一堆OR门。更工程的做法是使用“符号扩展修正常数”也就是所谓sign extension reduction。原理不复杂如果某一行部分积需要向高位符号扩展S位那么与其真实地把S个符号位复制过去不如只在最终结果里把这个扩展贡献折算成一个常数项。因为很多行的高位其实全是全0或全1直接求和只会产生固定的进位模式。实际项目里我会在RTL里加一个常量参数把各行的符号扩展位取反并合并最后在压缩树的末尾统一加上这个修正常数。这样部分积阵列的宽度能压低接近20%在32位乘法器上面积收益尤其明显。3.4 支持10万次随机激励的自校验测试平台跑分之前先保证功能正确。我习惯用SystemVerilog写一个自校验testbench一次性例化两个乘法器基2和基4各自输出与参考模型比对module tb_booth; parameter W 16; logic signed [W-1:0] a, b; logic signed [2*W-1:0] r2, r4; integer i, pass, fail; booth_radix2 u2 (.*, .product(r2)); booth_radix4 u4 (.*, .product(r4)); initial begin pass 0; fail 0; for (i 0; i 100000; i i 1) begin a $random(); b $random(); #5; if (r2 ! a * b) begin fail fail 1; $error(radix2 mismatch: a%0d b%0d got%0d exp%0d, a, b, r2, a*b); end if (r4 ! a * b) begin fail fail 1; $error(radix4 mismatch: a%0d b%0d got%0d exp%0d, a, b, r4, a*b); end else pass pass 1; end $display(pass%0d fail%0d, pass, fail); end endmodule需要强调一点参考模型里必须用a * b而a和b都要声明成signed否则SystemVerilog会按无符号数处理得到完全错误的结果。$random只生成32位随机数直接赋值给16位变量时正数和负数都会出现。但我还会额外固定塞几组边界值0、1、-1、0x7fff、0x8000、0x8001防止随机数没有命中补码边界。4. 实测跑分面积、逻辑层级与关键路径延迟对比4.1 综合环境与方法FPGA器件与纯组合路径测量这次对比用的环境是Vivado 2023.1FPGA器件选了xc7a100t-1csg324。策略是直接做纯组合逻辑综合不额外插入寄存器。为了测关键路径加了一个虚拟输入寄存器和一个虚拟输出寄存器然后把它们从时序路径里exclude掉只看乘法器本身从输入到输出的组合延迟。我也在Quartus上用Cyclone V试过一轮数值不同但趋势完全一致基4在面积和时序上全面领先基2。所以在不同工具链之间做选择时不用纠结绝对数值直接看行数和层级带来的相对差距就行。4.2 16位与32位乘法器的实测数据先放16位的结果指标Radix-2Radix-4变化部分积行数168-50%LUT13188-32.8%关键路径延迟ns9.867.15-27.5%Vivado估算动态功耗mW2.11.5-28.6%再放32位的结果指标Radix-2Radix-4变化部分积行数3216-50%LUT512342-33.2%关键路径延迟ns16.4210.71-34.8%Vivado估算动态功耗mW5.63.7-33.9%说句实在话功耗是Vivado的高层次估算不是signoff级别参考价值有限。但面积和时序是综合工具直接报出来的这两列足够说明问题位宽越大Radix-4的领先幅度越大。4.3 从数据反推延迟构成MUX开销vs加法树收益数据看完了得知道这些数字是怎么来的。基2的16个部分积走压缩树大概需要4级CSA基4的8个部分积走压缩树只需要3级。FPGA的LUT级联延迟每一级大约0.3~0.5ns少一级就是0.3~0.8ns的关键路径收益。有人会问基4的编码MUX不是更复杂吗确实编码器需要从五个候选中选一个比基2的三种选择多出一点组合逻辑。但这一层MUX的深度只有1~2级LUT而且部分积数量减半后每一行PP的位宽和符号扩展复杂度都在下降。实测下来编码器多出来的LUT数量远小于压缩树省下来的LUT数量。这就像高速公路收费站的检查岗多了一个但收费站数量少了一半整体通行效率反而更高。5. 现代芯片用Radix-4的三个现实理由5.1 部分积数量减半压缩机层数随之变少这是Radix-4最核心的账面优势。无论用Wallace树还是Dadda树压缩一个部分积阵列需要的CSA层数都取决于部分积行数。基2的W行压成2行的层数是ceil(log(W))量级基4的W/2行压成2行的层数直接就少一层。对于32位乘以32位基2需要5级CSA基4只需要4级。这一级之差在ASIC里折算成时序大概是几百皮秒到一纳秒看起来不多但在一个百万门级芯片里乘法器往往出现在关键计算路径上。省掉这一级整个流水线的时钟频率就能往上提一档这是架构层面的收益不是靠后端优化能硬挤出来的。5.2 基4的MUX逻辑简单到几乎不增加路径Radix-4的选择集是0、±A、±2A看起来很丰富但仔细看硬件实现拿A和A左移一位作为基础再根据编码决定是否取反。取反操作在补码里就是逐位取反加一MUX选择信号可以直接控制这些逻辑。整个编码器加部分积生成器的逻辑深度只有1~2级门。相比之下Radix-8的选择集包含±3A为了得到3A必须先算一个A2A这凭空多出一个加法器。这个加法器的输出是3A而它又必须在部分积MUX之前准备好等于在关键路径上硬塞了一级全加器。基8的部分积行数确实从W/2降到了W/3但减少的这部分收益很可能被3A生成逻辑的延迟给吞掉。5.3 功耗和布线密度的收益经常被忽略现代芯片里乘法器不是孤零零一个AI加速器一个周期要跑几百上千个MACCPU里的乘法指令流水线也是各个执行单元共享的关键模块。面积每缩小30%就意味着同样面积下可以塞下更多计算单元或者省下来的功耗预算给别的模块。Radix-4的另一个隐藏优势是布线。部分积阵列行数减半意味着加法树里的内部连线密度下降布线拥塞问题也会缓解。FPGA上这个感觉尤其明显——LUT占用率降了局部拥塞少了综合器摆放布线也就更从容关键路径的收敛难度随之降低。我在一个32位复数乘法器项目里做过实验把基2换成基4之后整套逻辑在150MHz时序约束下从勉强收敛变成有余量整个模块就能把流水级从3级压到2级。这种“连锁反应”是单纯看面积和延迟数据时感受不到的。5.4 为什么不上Radix-8或更高阶顺着基数往上推终极逻辑似乎是Radix-16甚至Radix-32部分进行数进一步减少为什么不直接用问题出在编码器的选择集上。Radix-8至少需要0、±A、±2A、±3A、±4ARadix-16还需要±5A、±6A、±7A这些奇数倍项。得到这些项要么用额外的加法器预先组合要么用更复杂的选择逻辑这些开销会直接堆在关键路径上。工程界有一个朴素的平衡点Radix-4的组合逻辑最简单部分积减少一半是性价比最高的选择。更高基数不是没人用而是只出现在固定系数乘法器或者某些超高速专用设计里作为通用乘法器的主流方案远不如Radix-4稳妥。6. 跑分之外的工程细节符号扩展、时序收敛与验证6.1 符号扩展的三种写法与真实坑位Booth乘法器最容易翻车的地方就是符号扩展尤其在负数乘法场景一个符号扩展位写错结果相差十万八千里。第一种写法是显式拼接扩展把符号位复制到目标位宽assign pp_ext {{2*W-pos-1{pp[W-1]}}, pp, {pos{1b0}}};这种写法直观但pos越大高位的扩展位越多综合面积不划算。第二种写法是利用Verilog的signed符号传播wire signed [2*W-1:0] pp_ext; assign pp_ext ($signed(pp) pos);算术右移会自动按符号位扩展语法上最省事。但用的时候要确认pp本身确实被声明为signed否则$signed不会生效。第三种就是我前面提到的修正常数法把符号扩展折算成常数项面积最省但代码可读性差需要写清楚注释不然三个月后自己看都懵。我最常见的错误不是选择了哪种写法而是把某个局部变量写成无符号。比如wire [W-1:0] a;再写-a结果是在无符号空间里取补码出来的东西已经是错的了。定义有符号数时一定要在信号声明和参数传递两个层面都带上signed。6.2 时序不收敛时先分清瓶颈在编码器还是加树如果综合报告显示关键路径不达标先别急着改架构用report_timing看路径具体从哪一段开始。我遇到过几次看似是乘法器整体延迟的例子最后发现瓶颈根本不在加法树而在前一级来的输入信号太晚到达导致部分积MUX的选择信号太晚瘫。这种情况的解法是调整编码器的输入寄存器或者把Booth编码提前一拍做。对流水线设计来说趁前一个周期还在算数据时先把乘数B的编码表算好存进寄存器下一周期直接拿编码结果生成部分积这样能把MUX延迟从关键路径里剥出去。如果瓶颈确实在压缩树先检查压缩树是否用了平衡结构。部分积相加不是真的只有串行加法一条路可以尝试Wallace树或者4-2压缩器。基4配合Wallace树是乘法器设计的经典组合网上开源实现也很多抄一个结构严谨的比自己盲写循环强。6.3 验证阶段的几个好习惯随机测试跑了10万次不代表模块一定对尤其是Booth这类跟补码边界强相关的电路。我维护的验证脚本里会专门开一组“边界轰炸”测试把0x0000、0x0001、0x0002、0x7fff、0x8000、0xffff这些值两两配对全跑一遍然后再叠加随机数。还有一点是波形导出的习惯。每轮回归失败时不要只留着$error的打印一定把触发失败的输入向量导出到文件后面新版本回归时用同一批向量再跑一次确认旧bug没有反弹。工程上的乘法器验证最怕的是“这次过了下个版本又挂”。最后分享一个我踩过很多次才养成的习惯任何乘法器模块都先基于Radix-4起手没有特殊需求就不回头写基2。如果你已经在用基2并且性能也能接受那不用强行改但如果你正好要新写一个乘法器或者发现现有乘法器成了时序瓶颈换Radix-4是性价比最高的改动方向跑一次综合你就知道我说的是不是真的了。
返回列表