深入解析定点乘法器中的部分积压缩技术——以Wallace压缩为例

1. 从加法到压缩:为什么乘法器不能“硬算”?

大家好,我是老张,在芯片设计这行摸爬滚打了十几年,从早期的ASIC到现在的各种AI加速器,乘法器这个基础单元不知道设计优化过多少遍了。今天想和大家聊聊乘法器设计里一个特别核心,但又容易被新手忽略的环节——部分积压缩。尤其是以Wallace压缩为代表的压缩技术,它可以说是高性能乘法器的“灵魂”所在。

咱们先从一个最朴素的想法开始。假如我们要计算两个32位的二进制数相乘,比如 a * b。学过数字电路的朋友都知道,乘法本质上可以分解为一系列的“移位-相加”操作。Booth算法(咱们上期聊过)能高效地生成一堆被称为“部分积”的中间结果。对于32位乘法,你可能会得到16个或17个(取决于算法变种)32位的部分积。接下来最直接的办法是什么?找个加法器,把它们一个一个加起来呗!

但问题就出在这个“一个一个加”上。你想想,如果用最基础的串行加法器,加完第一个和第二个,得到结果再去加第三个……这样一级一级串下去,总共要加15次。每一次加法都伴随着进位链的传递,这个延时是线性累积的,最终会导致乘法运算慢得无法接受,在高频CPU或者对吞吐量要求极高的DSP、AI芯片里,这种设计根本行不通。

所以,工程师们的智慧就体现在这里:我们能不能让这些加法“并行”起来?就像组织一场大合唱,如果让所有人排成一列逐个发声(串行),那唱完一首歌要很久。但如果我们把所有人分成几个声部,同时发声,最后再把几个声部的声音合并(并行),效率就高多了。部分积压缩技术,干的就是这个“并行加法”的活儿。它的目标,就是用一种树状结构,在尽可能少的层级内,将十几个、几十个部分积,快速压缩成最终的两个数(一个和,一个进位),这两个数再用一个快速的加法器(比如超前进位加法器)做最后一次相加,就得到最终乘积。

今天,咱们就深入这个“加法树”的内部,以最经典、应用最广的Wallace压缩为例,掰开揉碎了讲讲它的原理、实现,以及我当年在类似“华为杯”这样的竞赛中实际应用时,踩过的坑和总结的经验。你会发现,理解了这部分,你不仅能在竞赛中游刃有余,更能看懂很多高端芯片技术文档里关于计算单元设计的精髓。

2. Wallace压缩的核心思想:化“串”为“并”

Wallace压缩,是以其发明者C. S. Wallace的名字命名的。它的核心思想非常直观:用全加器(FA)作为基本单元,构建一个不规则的树形网络,来同时处理多个部分积的相加

这里需要先明确一个关键概念:我们处理的每一个部分积,都是一个多位数(比如32位)。但压缩过程是按比特位纵向进行的。想象一下,我们把所有部分积像搭积木一样,按照二进制位权对齐,排成一个矩阵。矩阵的每一列,就代表了相同权重(2^0, 2^1, 2^2...)上的所有比特。Wallace树要做的,就是高效地压缩每一列上的“1”的个数。

2.1 基本单元:全加器(3:2压缩器)

Wallace树的基石是全加器。一个全加器有三个1比特输入(A, B, Cin),产生两个1比特输出(Sum, Cout)。从压缩的角度看,它吃进三个比特,吐出两个比特。所以,全加器天然就是一个 3:2压缩器。它把一列中三个比特的相加问题,转化成了一个“和”位(留在本列)和一个“进位”位(进入更高一列)的问题。

表达式很简单: Sum = A ⊕ B ⊕ Cin Cout = (A & B) | (A & Cin) | (B & Cin)

框图长这样:

       A   B   Cin
        \  |  /
        [ 全加器 ]
          /   \
        Sum   Cout (去下一列)

在实际的Wallace树构造中,我们就是像玩“消消乐”一样,在一列列的部分积矩阵中,寻找三个可以送入一个全加器的比特,然后用它们的Sum和Cout去更新矩阵,不断重复,直到每一列的比特数都减少到最多2个(一个和位,一个可能的进位位)。

2.2 效率升级:4:2压缩器

虽然3:2压缩器(全加器)是基础,但在实际工程中,尤其是位宽较大时,直接用全加器构建Wallace树,结构可能会比较深(层级多),延时不够理想。于是,更高效的 4:2压缩器 被广泛采用。

4:2压缩器,顾名思义,一次能压缩四个比特。它有四个主要输入(X1, X2, X3, X4),一个来自低位的进位输入(Cin),输出两个比特(Sum, Carry)和一个向高位的进位输出(Cout)。注意,这里的Carry和Cout是不同的:Carry是本次压缩产生的、需要左移一位(即进入下一更高权重列)的进位,而Cout是直接传递给同一列在下一个4:2压缩器(如果级联的话)的进位输入。

它的表达式可以理解为: Total = X1 + X2 + X3 + X4 + Cin 这个Total是一个0到5之间的值。然后我们把它表示为: Total = Sum + 2 * Carry + 2 * Cout 其中,Sum是留在本位的和(0或1),Carry是输出到下一列的进位(0或1),Cout是传递给同列下一级压缩器的进位(0或1)。

框图示意:

       X1  X2  X3  X4  Cin
        \   \  /   /   /
        [     4:2 压缩器    ]
            /       |      \
          Sum     Carry    Cout
          (本位)   (左移一位) (给同列下一级)

4:2压缩器的优势在于,它内部通常经过精心优化,用几个异或门、选择器实现,其从输入到Sum/Carry的延时,比两个全加器级联的延时更短。更重要的是,它更容易构建规则、平衡的压缩树。比如,对于8个部分积,用4:2压缩器只需要两级(第一级将8压成4,第二级将4压成2),结构非常规整。而用3:2压缩器,可能需要更多不规则的层级。

在我做“华为杯”赛题那个乘法器时,Booth算法生成的部分积正好是8个。用4:2压缩器,三个并行工作的4:2压缩器就能把8个压成4个,然后再用一级压缩把4个压成2个,思路清晰,布线也相对规整。如果全用3:2压缩,树形结构会复杂一些,深度也可能增加,这在追求极限时序的场合是不利的。

3. 动手实现:从1比特模块到32比特压缩树

理论说再多,不如动手写段代码来得实在。咱们就以32位乘法、生成8个部分积为例,看看怎么用硬件描述语言(比如Verilog)来实现这个基于4:2压缩器的压缩树。

3.1 首先,打造一个1比特的4:2压缩器模块

这是最基本的积木块。它的功能就是上面说的,五个1比特输入,三个1比特输出。

module compressor_4to2_1bit (
    input  wire x1, x2, x3, x4, cin,
    output wire sum, carry, cout
);
    // 内部信号
    wire s1, c1, s2, c2;

    // 第一级:用一个全加器压缩x1, x2, x3
    // 当然,实际优化电路可能不是直接用两个全加器,这里为理解方便
    assign s1 = x1 ^ x2 ^ x3;
    assign c1 = (x1 & x2) | (x1 & x3) | (x2 & x3);

    // 第二级:用另一个全加器压缩s1, x4, cin
    assign sum = s1 ^ x4 ^ cin; // 最终的本位和
    assign c2 = (s1 & x4) | (s1 & cin) | (x4 & cin);

    // 最终的进位输出和向高位的进位
    // 注意:carry是c1和c2的“或”关系的一种简化理解,实际优化电路会合并
    // 这里给出一个常见的逻辑实现:
    assign carry = c1 | c2; // 简化表达,实际电路可能更优
    assign cout = c2; // 通常cout就是第二级产生的进位

endmodule

当然,上面代码是为了清晰展示两阶段计算。在实际的ASIC标准单元库或FPGA中,综合工具可能会用一个更优化的门级电路来实现同样的功能,其关键路径延时比两个全加器串联要短。

3.2 然后,级联成32比特宽的压缩器

我们需要压缩的是32位宽的部分积,所以需要将32个1比特的4:2压缩器“肩并肩”连起来,形成一个32比特宽的压缩器模块。关键点在于处理进位链:上一个比特的cout要连接到下一个比特的cin

module compressor_4to2_32bit (
    input  wire [31:0] pp_a, pp_b, pp_c, pp_d, // 四个32位部分积输入
    input  wire         cin_lsb,                 // 整个32位压缩器的最低位进位输入(通常为0)
    output wire [31:0] sum_out, carry_out,      // 两个32位输出
    output wire        cout_msb                 // 最高位的cout,可能用于级联或处理溢出
);
    wire [32:0] cin_chain; // 进位链,比位宽多1位,用于连接
    assign cin_chain[0] = cin_lsb; // 最低位进位输入

    genvar i;
    generate
        for (i=0; i<32; i=i+1) begin: bit_slice
            compressor_4to2_1bit comp_inst (
                .x1   (pp_a[i]),
                .x2   (pp_b[i]),
                .x3   (pp_c[i]),
                .x4   (pp_d[i]),
                .cin  (cin_chain[i]),
                .sum  (sum_out[i]),
                .carry(carry_out[i]),
                .cout (cin_chain[i+1])
            );
        end
    endgenerate

    assign cout_msb = cin_chain[32]; // 最高位产生的cout
endmodule

注意carry_out这个信号,它代表的是本次压缩产生的、需要左移一位再与sum_out相加的进位。所以在后续处理时,我们需要做{carry_out, 1‘b0}(即左移一位)的操作。

3.3 最后,搭建完整的8到2压缩树

假设我们有8个32位的部分积 PP[0]PP[7]

第一级压缩: 我们用三个并行的 compressor_4to2_32bit 模块。

  • 压缩器1:处理 PP[0], PP[1], PP[2], PP[3],输入cin_lsb=0,得到 Sum1, Carry1
  • 压缩器2:处理 PP[4], PP[5], PP[6], PP[7],输入cin_lsb=0,得到 Sum2, Carry2。 这样,8个部分积就变成了4个中间向量:Sum1, Carry1(左移一位后), Sum2, Carry2(左移一位后)。注意,Carry1Carry2需要先左移一位,变成 Carry1_shifted = {Carry1[30:0], 1'b0}Carry2_shifted

第二级压缩: 现在我们有4个向量:Sum1, Carry1_shifted, Sum2, Carry2_shifted。再用一个 compressor_4to2_32bit 模块对它们进行压缩。

  • 输入这4个向量,cin_lsb=0,得到最终的 Final_SumFinal_Carry(同样需要左移一位)。

至此,我们得到了最后两个部分积:Final_SumFinal_Carry_shifted。整个压缩树的工作就完成了,剩下的就是用一个大位宽的快速加法器将它们加起来,得到最终的乘积。

4. 性能权衡与竞赛实战心得

在“华为杯”这类竞赛或者实际工程项目中,设计乘法器不仅仅是功能正确就行,更要追求**面积(Area)时序(Timing)**的平衡。部分积压缩环节是优化的主战场之一。

4.1 为什么不用直接的大加法器?

有同学可能会问,既然最后都要用一个加法器,为什么不一开始就用一个支持多操作数的巨型加法器呢?原因就在于进位链。一个标准的超前进位加法器(CLA)虽然比行波进位快很多,但其延时仍然随着位宽对数增长。如果直接用它加十几个数,你需要将加法器串联很多级,或者设计更复杂的多操作数CLA,其面积和延时都会急剧增加。而Wallace树先用面积较小、结构简单的压缩器(3:2或4:2),以很小的局部延时,将操作数数量急剧减少到2个,把复杂的多操作数加法问题,转化成了多个局部压缩和一个最终加法的问题,总体延时更优。

4.2 3:2 vs 4:2:如何选择?

这是一个常见的权衡。

  • 3:2压缩器(全加器):结构简单,标准单元库中必有,布线灵活,可以构建最小深度的Wallace树(理论上最优)。但树形结构不规则,在自动布局布线(APR)时可能导致布线拥塞和时序难以预测。
  • 4:2压缩器:压缩效率更高,树形结构更规则、更平衡(尤其是部分积数量是4的倍数时),有利于物理实现和时序收敛。虽然单个4:2压缩器比一个全加器复杂,但整体树深更浅。在现代工艺和EDA工具流下,规则性往往比极致的理论最小深度更重要。因此,在大多数对性能有要求的场景,4:2压缩器是更主流的选择。

我在赛题中选择了4:2压缩,正是因为8个部分积用4:2构建的两级树非常规整。综合后查看时序报告,关键路径清晰,主要延时集中在最后的32位超前进位加法器上,压缩树本身的延时贡献很小且均匀。

4.3 超前进位加法器(CLA)的妙用

压缩树输出两个部分积后,最后的加法器性能至关重要。行波进位加法器(RCA)肯定不能用,太慢了。我采用了四级8位超前进位加法器级联构成32位加法器。为什么是8位?这是一个经验值。超前进位逻辑的复杂度随位数增加而增加,位数太多,生成进位信号的逻辑门扇入和延时也会变大。将32位分成4段8位的CLA,每段内部超前进位,段与段之间也通过超前进位逻辑连接(即组超前进位),这样能在速度和复杂度之间取得很好的平衡。

更进一步,还可以使用选择加法器。例如,我可以同时计算两段可能的和(一段假设低位进位为0,另一段假设为1),等低位进位真正确定后,用一个选择器快速选出正确的结果。这相当于用面积换速度,在关键路径上砍掉了一级进位传递的时间。这在赛题优化阶段是提升性能的利器。

4.4 面积与延时的真实数据

根据我当时用综合工具(比如Synopsys Design Compiler)在典型工艺库下综合的结果,一个基础版本的32位Booth+Wallace树乘法器,面积大约在2000-3000门等效(GE)左右。用4:2压缩树结合分段CLA,关键路径延时(从输入到输出稳定)可以做到十几纳秒(在不太先进的工艺节点下)。这离竞赛的高性能要求还有差距。

优化的方向很明确:一是优化Booth编码,生成更少的部分积(比如采用基4或基8 Booth);二是优化压缩树的结构,探索5:2、7:2等更高压缩比的压缩器,或者混合使用不同压缩器,构建更平衡的树;三是优化最终加法器,比如采用更激进的选择加法器或并行前缀加法器(如Kogge-Stone、Brent-Kung)。

纸上得来终觉浅。我强烈建议大家用Verilog或VHDL自己实现一遍,从最简单的3:2 Wallace树开始,再到4:2的规整树。用仿真工具看波形,用综合工具看面积和时序报告。你会对“为什么压缩器内部要那样设计”、“进位链怎么走”、“关键路径在哪里”有刻骨铭心的理解。当年我就是在一次次仿真失败和时序违例中,才真正搞懂了这些压缩技术背后的电路艺术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值