IEArch-NaiveTPU

实验报告

一、实验原理

1.1 项目架构

​ 相比于 lab4 只有 Bram 是在板子上,而 PL 采用软件模拟的,这次 PL 侧我们利用 FPGA 实现了,也就是真正完成了一个简单的乘法运算加速器。

在这里插入图片描述

​ PL 侧架构如图所示:

在这里插入图片描述

​ 可以看到,依然是利用 bram 进行通信。用到了在 lab3 中开发的 MAC 模块。

1.2 矩阵乘法

​ 在 lab3 的时候我们开发了 8x8 的矩阵乘法模块,可以用计算 A 8 × N ⋅ B N × 8 A_{8 \times N} \cdot B_{N \times 8} A8×NBN×8 的矩阵乘法,其中 N N N 可以是任意值。

​ 由于 FPGA 的硬件资源的限制,我们在板子上实现 2 个 8x8 的矩阵乘法模块,由矩阵乘法的性质很容易得出,2 个 8x8 的矩阵乘法模块等价于一个 8x16 的矩阵乘法模块,也就是说,可以计算 A 8 × N ⋅ B N × 16 A_{8 \times N} \cdot B_{N \times 16} A8×NBN×16 的矩阵乘法。

​ 但是我们实际的需求是计算 A M × N ⋅ B N × P A_{M \times N} \cdot B_{N \times P} AM×NBN×P ,所以需要对这两个矩阵分块,分块的方法就是 A A A 矩阵横着分(适应 8 × N 8 \times N 8×N B B B 矩阵竖着分(适应 N × 16 N \times 16 N×16 )。如图所示:

在这里插入图片描述

​ 对于不足 8 和 16 的输入矩阵,需要补齐。

1.3 对齐

​ 对齐有很多种实现方式,比较方便的就是补零操作,比如说需要进行 8 对齐,而数据宽度只有 1 位,那么就可以补充 7 位的零。本次 lab 的基础部分就是采用这个方法实现的。

​ 如果比较本质的看,对齐操作应当是与数据接受方的需求有关,本次 lab 一共有两个数据接收方,一个是沟通 PS 和 PL 的 Bram,是 32 位的,一个是 8x16 的乘法单元,是 32 位对齐和 64 位对齐的,所以补零操作发生了两次,第一次在 Matmul.py 中,如下代码,是 32 对齐的。

def send_data(self, data, block_name, offset='default'):
    """
    写入 input 或 weight 至 bram
    假设两个矩阵分别是 (m,n) x (n,p), m 和 p 的维度需要补全至 self.systolic_size 的倍数,
    并且写入时需要按照补零的方向写入,例如:
        1. 矩阵 (m, n) 是 m 补零,则 m 个 m 个写入BRAM中。(行方向补零,列方向写入)
        2. 矩阵 (n, p) 是 p 补零,则 p 个 p 个写入BRAM中。(列方向补零,行方向写入)
    Args:
        data: 要写入的数据
        block_name: input, weight
        offset: 偏移地址名称,默认为default
    """
    # padding
    if block_name == 'input':
        # M x N
        m, n = data.shape
        self.ori_m = m
        # padded m 是需要在 m 方向上补零的个数
        self.paddedm = self.systolic_size * (m // self.systolic_size + 1) - m
        padd = np.zeros((self.paddedm, n), dtype=np.uint8)
        padded = np.append(data, padd, axis=0).T
        self.paddedm += m
    else:
        assert block_name == 'weight'
        # N x P
        n, p = data.shape
        self.ori_p = p
        self.paddedp = self.systolic_size * (p // self.systolic_size + 1) - p
        padd = np.zeros((n, self.paddedp), dtype=np.int8)
        padded = np.append(data, padd, axis=1)
        self.paddedp += p
    self.n = n
    self.bram.write(padded, block_name=block_name)
    pass

​ 第二次是在 FM_reshape.vWM_reshape.v 模块,如下代码

always @(posedge clk or posedge rst) begin
    if (rst) begin
        BRAM_FM64_wrdata <= 'b0;
    end
    else if ((c_state==WORK)&&(cycle1_cnt[0]==1'b1)) begin
        BRAM_FM64_wrdata <= {BRAM_FM32_rddata,BRAM_FM64_wrdata[31:0]};
    end
    else if (c_state==WORK) begin
        BRAM_FM64_wrdata <= {32'b0,BRAM_FM32_rddata};
    end
    else begin
        BRAM_FM64_wrdata <= BRAM_FM64_wrdata;
    end
end

二、实验流程

2.1 仿真流程

​ 本次实验所需要的工程示例已提供,所以不需要再改动 Verilog 源码了。

​ 对于仿真流程,需要 Gen_matrix.py 产生输入矩阵,然后利用 MM_top_tb.v 作为 ARM 的模拟激励测试平台,然后测试即可,最后利用 compare.py 进行比对。

​ 输出为

data right

2.2 上板流程

​ 生成 bit 流后上板,需要用到 lab4 的 Matmul.py ,最后效果如图

在这里插入图片描述


思考题

1

有如下约束
M × N ≤ 8 × 4096 = 32768 N × P ≤ 16 × 8192 = 131072 M \times N \leq 8 \times 4096 = 32768\\ N \times P \leq 16 \times 8192 = 131072 M×N8×4096=32768N×P16×8192=131072
取等条件是 M % 8 == 0, P % 16 == 0 。软件 padding 是根据脉冲矩阵的大小进行的,所以是 4 补零,硬件补零是按照 $8 \times 16 $ 的乘法单元进行补零的,为了取等,不能补零,所以是上面的条件。

这是因为 BRAM 的配置如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

关于补零:

可以看做是对齐的具体实现方式,对齐应该看输入端口的需求位宽,

对于第一次补零,是向 BRAM_32 传输信号,所以是 32 bit 对齐,所以是 4 补零,这次补零由与 BRAM_32 通信的 PS 完成(也就是 python 模拟)

对于第二次补零,是向 BRAM_FM_64 和 BRAM_WM_128 传输信号,所以 64 bit 和 128 bit 对齐,所以是 8 补零和 16 补零,这次补零由与 BRAM_FM_64 和 BRAM_WM_128 通信的 reshape 模块完成。


2

均是足够的。

在这里插入图片描述

由计算可得 Feature 和 Weight 矩阵中的最大元素个数分别是(需要按照 4 补零,但是恰好是对齐的):
f e a t u r e _ m a x = 784 × 25 = 19600 w e i g t h _ m a x = 784 × 100 = 78400 feature\_max = 784 \times 25 = 19600\\ weigth\_max = 784 \times 100 = 78400 feature_max=784×25=19600weigth_max=784×100=78400
BRAM_FM32 可以容纳的最大元素个数
8 × 8192 = 65536 ≥ 19600 8 \times 8192 = 65536 \geq 19600 8×8192=6553619600
所以大小是足够的。

BRAM_WM32 可以容纳的最大元素个数
8 × 32768 = 262144 ≥ 78400 8 \times 32768 = 262144 \geq 78400 8×32768=26214478400
所以大小是足够的。

BRAM_FM_64 可以容纳的最大元素个数是 32768 ≥ 19600 32768 \geq 19600 3276819600 ,所以大小是足够的。

BRAM_WM_128 可以容纳的最大元素个数是 131072 ≥ 78400 131072 \geq 78400 13107278400 ,所以大小是足够的。


3

可以通过一个固定维数的二维数组,模拟矩阵,然后利用 for 循环,完成矩阵乘法操作。

reg [0:7]   feature[0:200][0:200];
reg [0:7]   weight[0:200][0:200];
reg signed [0:31]  result_std;
reg [0:31]  result_sim[0:20000];
reg [0:0]  right;

integer i, j, k, r;

// 初始化,生成待计算的矩阵大小以及矩阵元素
initial begin
    while(1) begin
        wait(c_state==IDLE);
        // 初始化矩阵大小
        M = 2 + {$random} % 20;
        N = 2 + {$random} % 20;
        P = 2 + {$random} % 20;
//        M = 4;
//        N = 4;
//        P = 4;
        

        FM_reg_valid = 1'b0;
        FM_reg0 = 'b0;
        FM_reg1 = 'b0;
        FM_reg2 = 'b0;
        FM_reg3 = 'b0;
        wait(c_state==WRITE_FM);
        // 生成矩阵 feature
        for(j = 0; j < N; j=j+1) begin
            for(i = 0; i < M; i=i+4) begin
                @(posedge arm_clk)
                FM_reg_valid = 1'b1;
                {FM_reg0, FM_reg1, FM_reg2, FM_reg3} = $random;

                if(i + 1 >= M) begin
                    FM_reg1 = 0;
                    FM_reg2 = 0;
                    FM_reg3 = 0;
                end 
                else if(i + 2 >= M) begin
                    FM_reg2 = 0;
                    FM_reg3 = 0;
                end 
                else if(i + 2 >= M) begin
                    FM_reg3 = 0;
                end
                feature[i][j] = FM_reg0;
                feature[i + 1][j] = FM_reg1;
                feature[i + 2][j] = FM_reg2;
                feature[i + 3][j] = FM_reg3;
                
            end
        end    
        
        WM_reg_valid = 1'b0;
        WM_reg0 = 'b0;
        WM_reg1 = 'b0;
        WM_reg2 = 'b0;
        WM_reg3 = 'b0;
        wait(c_state==WRITE_WM);
        // 生成矩阵 weight
        for(i = 0; i < N; i = i + 1) begin
            for(j = 0; j < P; j = j + 4) begin
                @(posedge arm_clk)
                WM_reg_valid = 1'b1;
                {WM_reg0, WM_reg1, WM_reg2, WM_reg3} = $random;

                if(j + 1 >= M) begin
                    WM_reg1 = 0;
                    WM_reg2 = 0;
                    WM_reg3 = 0;
                end else if(j + 2 >= M) begin
                    WM_reg2 = 0;
                    WM_reg3 = 0;
                end else if(j + 2 >= M) begin
                    WM_reg3 = 0;
                end
                weight[i][j] = WM_reg0;
                weight[i][j+1] = WM_reg1;
                weight[i][j+2] = WM_reg2;
                weight[i][j+3] = WM_reg3;
            end
        end 
        r = 0;
        // 计算并对比
        
        wait(n_state==FINISH);
        wait(r == M * P);
        arm_work = 1'b0;
        right = 1'b1;
        for(i = 0; i < M && right; i = i + 1) begin
            for(j = 0; j < P && right; j = j + 1) begin
                result_std = 0;
                for(k = 0; k < N; k = k + 1) begin
                    result_std = result_std + $signed(weight[k][j]) * $signed({8'b0,feature[i][k]});
                end
                if(result_std != result_sim[i*P + j]) begin
                    right = 1'b0;
                end
            end
        end                
        if (right)
            $display("passed test %dx%dx%d\n", M, N, P);
        else
            $display("notpass test %dx%dx%d\n", M, N, P);
        
        arm_work = 1'b1;

    end
end

同理,也可用 for 循环完成检验操作。


4

因为直接与 BRAM_FM_64 和 BRAM_WM_128 通信,所以需要在 PL 侧完成新的对齐操作(也就是补零)。之前的代码是 4 字节对齐,需要被修改为 8 对齐和 16 对齐,所以需要对 Matmul.py 进行如下修改(修改脉动矩阵的大小 systolic_size

def send_data(self, data, block_name, offset='default'):
    # padding
    if block_name == 'input':
        # M x N
        m, n = data.shape
        self.ori_m = m
        # 原为 self.paddedm = 4 * ((m - 1) // 4 + 1) - m
        self.paddedm = 8 * ((m - 1) // 8 + 1) - m
        padd = np.zeros((self.paddedm, n), dtype=np.uint8)
        padded = np.append(data, padd, axis=0).T
        self.paddedm += m
    else:
        assert block_name=='weight'
        # N x P
        n, p = data.shape
        self.ori_p = p
        # 原为 self.paddedp = 4 * ((p - 1) // 4 + 1) - p
        self.paddedp = 16 * ((p - 1) // 16 + 1) - p
        padd = np.zeros((n, self.paddedp), dtype=np.int8)
        padded = np.append(data, padd, axis=1)
        self.paddedp += p
        self.paddedn = n
        self.bram.write(padded, block_name=block_name)
    pass

然后烧录 PL 侧,运行即可得到正确结果:

在这里插入图片描述


5

补零是不必要的,因为补零的位置虽然会被运算,但是并不会作为“有效答案”。

如果不进行补 0,无效位置保留默认值,不会影响矩阵运算结果,这是因为我们通过控制信号,会忽略原先补零位置计算出的答案。

我们可以将“补零”操作改为“补一”操作验证上面的观点,修改 Matmul.py 中的 send_data 方法

# 原为 padd = np.zeros((self.paddedm, n), dtype=np.uint8)
padd = np.ones((self.paddedm, n), dtype=np.uint8)

然后运行工程,会发现依然是可以正常工作的,效果如图

在这里插入图片描述


6

如果需要“跳地址”,那么就需要人为控制写入的偏移量,可以通过修改 bram.py 文件,来为偏移量增设这个功能。然后修改 Matmul.py 文件,使得我们可以在传输数据的时候实现“跳地址”的功能。

### bram.py
def write(self, data, block_name: str, offset='default'):
    '''写入数据
        由于数据位宽32bit,因此最好以4的倍数Byte写入(还不知道以1Byte单位写进去会有什么效果)

        Args:
            data: 输入的数据
            block_name: BramConfig中配置的block_info的key值
            offset: BramConfig中配置的offset字典key值
    '''
    map_ = self.block_map[block_name]

    if isinstance(offset, str):
        offset_ = self.block_info[block_name]['offset'][offset]
    # 这里增加一种新的方式给出偏移量
    else:
        offset_ = offset
    
    map_.seek(offset_)
    if isinstance(data, np.ndarray):
        data = data.reshape(-1)
    map_.write(data)

### Matmul.py
def send_data(self, data, block_name, offset='default'):
    padd = 0
    r, c = 0, 0
    if block_name == 'input':
        # M x N
        m, n = data.shape
        self.ori_m = m
        padd = self.systolic_size * (m // self.systolic_size + 1) - m
        # padd = np.zeros((self.paddedm, n), dtype=np.uint8)
        # padded = np.append(data, padd, axis=0).T
        self.paddedm = m + padd
        toWrite = data.T
        r, c = n, self.paddedm
    else:
        assert block_name == 'weight'
        # N x P
        n, p = data.shape
       self.ori_p = p
        padd = self.systolic_size * (p // self.systolic_size + 1) - p
        # padd = np.zeros((n, self.paddedp), dtype=np.int8)
        # padded = np.append(data, padd, axis=1)
        self.paddedp = p + padd
        toWrite = data
        r, c = n, self.paddedp
    self.n = n
    # 将内存中的顺序调整为行优先
    toWrite = toWrite.copy(order='C')
    for i in range(r):
        # 手动控制偏移量
        self.bram.write(toWrite[i, :], block_name=block_name, offset=i * c)
    # self.bram.write(padded, block_name=block_name)
    # pass

需要注意的是,可能是由于前面出现了“转置”操作,所以如果直接书写代码,会报错

not C-contiguous

意思是 ndarray 在内存中的存储方式不是行优先(C-order)的。而Fortrant-order(列优先顺序)不适合 mmap 的写入。所以我们需要将数组重新变成行优先序,即如下代码:

toWrite = toWrite.copy(order='C')

7

125.000000 M H z \tt{125.000000 MHz} 125.000000MHz

在这里插入图片描述

8

结合以下源码和指导书可以画出状态转移图:

localparam IDLE   = 5'b0_0001;
localparam INFO   = 5'b0_0010;
localparam WORK   = 5'b0_0100;
localparam WAIT   = 5'b0_1000;
localparam FINISH = 5'b1_0000;

// 当前状态
reg [4:0] c_state;
// 下一状态
reg [4:0] n_state;

// 很简单的状态机
always @(posedge clk or posedge rst) begin
    if (rst) begin
        c_state <= IDLE;
    end
    else begin
        c_state <= n_state;
    end
end

always @(*) begin
    case(c_state)
    IDLE: begin
        // 运算开始
        if (submulti_start)
            n_state = INFO;
        else
            n_state = c_state;
    end
    INFO: begin
        n_state = WORK;
    end
    WORK: begin
        // 应该是数据搬运完了的意思
        if (N_cnt == N - 1'b1)
            n_state = WAIT;
        else
            n_state = c_state;
    end
    WAIT: begin
        // 坍塌运算,应该是 align_fifo_get_all 都弄完了
        if (&align_fifo_get_all)
            n_state = FINISH;
        else
            n_state = c_state;
    end
    // 重新回到空闲状态
    FINISH: begin
        n_state = IDLE;
    end
    default: n_state = IDLE;
    endcase
end

状态图:

在这里插入图片描述

寄存器功能如下表所示

寄存器功能
sub_scale_M1第 1 个矩阵乘法单元,结果矩阵的行数,输出给 Align_fifo,用于控制答案矩阵的有效值
sub_scale_P1第 1 个矩阵乘法单元,结果矩阵的列数,输出给 Align_fifo,用于控制答案矩阵的有效值
sub_scale_M2第 2 个矩阵乘法单元,结果矩阵的行数,输出给 Align_fifo,用于控制答案矩阵的有效值
sub_scale_P2第 2 个矩阵乘法单元,结果矩阵的列数,输出给 Align_fifo,用于控制答案矩阵的有效值

9

Align_fifo 模块内结构如图所示

在这里插入图片描述

也就是说,Align_fifo 内部有 8 个 FIFO IP 核,用于存储 Multi_8x8 的输出。

9.1 读过程

从代码中可以看出,读过程是并行执行的,只是需要受到检验是否入队(因为对于有些数据是无效的),代码如图

// 检验是否入队,要满足没有被读出,该位有效,还没有写完等条件
assign fifo_wr_en[0] = (~align_fifo_get_all)&&valid_get0&&(sub_scale_P>8'd0)&&(fifo_wr_cnt[0]<sub_scale_M);
assign fifo_wr_en[1] = (~align_fifo_get_all)&&valid_get1&&(sub_scale_P>8'd1)&&(fifo_wr_cnt[1]<sub_scale_M);
assign fifo_wr_en[2] = (~align_fifo_get_all)&&valid_get2&&(sub_scale_P>8'd2)&&(fifo_wr_cnt[2]<sub_scale_M);
assign fifo_wr_en[3] = (~align_fifo_get_all)&&valid_get3&&(sub_scale_P>8'd3)&&(fifo_wr_cnt[3]<sub_scale_M);
assign fifo_wr_en[4] = (~align_fifo_get_all)&&valid_get4&&(sub_scale_P>8'd4)&&(fifo_wr_cnt[4]<sub_scale_M);
assign fifo_wr_en[5] = (~align_fifo_get_all)&&valid_get5&&(sub_scale_P>8'd5)&&(fifo_wr_cnt[5]<sub_scale_M);
assign fifo_wr_en[6] = (~align_fifo_get_all)&&valid_get6&&(sub_scale_P>8'd6)&&(fifo_wr_cnt[6]<sub_scale_M);
assign fifo_wr_en[7] = (~align_fifo_get_all)&&valid_get7&&(sub_scale_P>8'd7)&&(fifo_wr_cnt[7]<sub_scale_M);

// 写入数据
assign fifo_din[0] = data_get0;
assign fifo_din[1] = data_get1;
assign fifo_din[2] = data_get2;
assign fifo_din[3] = data_get3;
assign fifo_din[4] = data_get4;
assign fifo_din[5] = data_get5;
assign fifo_din[6] = data_get6;
assign fifo_din[7] = data_get7;

仿真如图

在这里插入图片描述

9.2 写过程

写过程是一个串行过程,这可能是因为写入 BRAM_32 的时候是串行的,所以没必要在这里并行。相关代码如图

// 状态转换,根据 fifo_rd_en 进行转换
always @(*) begin
    case (c_state)
    NOW_OUT0: begin
        if (fifo_rd_en[0]&&(sub_scale_P==8'd1))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[0])
            n_state = NOW_OUT1;
        else
            n_state = c_state;
    end
    NOW_OUT1: begin
        if (fifo_rd_en[1]&&(sub_scale_P==8'd2))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[1])
            n_state = NOW_OUT2;
        else
            n_state = c_state;
    end
    NOW_OUT2: begin
        if (fifo_rd_en[2]&&(sub_scale_P==8'd3))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[2])
            n_state = NOW_OUT3;
        else
            n_state = c_state;
    end
    NOW_OUT3: begin
        if (fifo_rd_en[3]&&(sub_scale_P==8'd4))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[3])
            n_state = NOW_OUT4;
        else
            n_state = c_state;
    end
    NOW_OUT4: begin
        if (fifo_rd_en[4]&&(sub_scale_P==8'd5))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[4])
            n_state = NOW_OUT5;
        else
            n_state = c_state;
    end
    NOW_OUT5: begin
        if (fifo_rd_en[5]&&(sub_scale_P==8'd6))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[5])
            n_state = NOW_OUT6;
        else
            n_state = c_state;
    end
    NOW_OUT6: begin
        if (fifo_rd_en[6]&&(sub_scale_P==8'd7))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[6])
            n_state = NOW_OUT7;
        else
            n_state = c_state;
    end
    NOW_OUT7: begin
        if (fifo_rd_en[7]&&(sub_scale_P==8'd8))
            n_state = NOW_OUT0;
        else if (fifo_rd_en[7])
            n_state = NOW_OUT0;
        else
            n_state = c_state;
    end
    default: n_state = NOW_OUT0;
    endcase
end

// ready 转换条件,只有当当前队列为空且 out_ctrl 完成的时候,才可以进行状态转换
always @(*) begin
    fifo_rd_en = 8'b0;
    case (c_state)
    NOW_OUT0: fifo_rd_en[0] = (~fifo_empty[0])&out_ctrl_ready;
    NOW_OUT1: fifo_rd_en[1] = (~fifo_empty[1])&out_ctrl_ready;
    NOW_OUT2: fifo_rd_en[2] = (~fifo_empty[2])&out_ctrl_ready;
    NOW_OUT3: fifo_rd_en[3] = (~fifo_empty[3])&out_ctrl_ready;
    NOW_OUT4: fifo_rd_en[4] = (~fifo_empty[4])&out_ctrl_ready;
    NOW_OUT5: fifo_rd_en[5] = (~fifo_empty[5])&out_ctrl_ready;
    NOW_OUT6: fifo_rd_en[6] = (~fifo_empty[6])&out_ctrl_ready;
    NOW_OUT7: fifo_rd_en[7] = (~fifo_empty[7])&out_ctrl_ready;
    default: fifo_rd_en = 8'b0;
    endcase
end
// 根据状态,选择输出数据
always @(*) begin
    case (c_state_f1)
    NOW_OUT0: data = fifo_dout[0];
    NOW_OUT1: data = fifo_dout[1];
    NOW_OUT2: data = fifo_dout[2];
    NOW_OUT3: data = fifo_dout[3];
    NOW_OUT4: data = fifo_dout[4];
    NOW_OUT5: data = fifo_dout[5];
    NOW_OUT6: data = fifo_dout[6];
    NOW_OUT7: data = fifo_dout[7];
    default: data = 'b0;
    endcase
end

读出的时候,是一个队列一个队列的读出,最终完成串行化。

在这里插入图片描述


10

10.1 削减 MAC 资源

​ 原工程中为了确定有效数据,提供了很多的信号位(w_validf_valid 信号,对于全局还有一个 num_valid)来进行确定,但是各个信号之间的功能是有重叠部分的,也就是有冗余现象的出现。

​ 可以考虑只使用一个 num_valid 信号完成对于“有效”的控制,它充当一个类似 reset 信号的功能,重置乘加器内部的寄存器,设置内部的num_r寄存器为输入的num_r值。当输入数据有效时,再拉低num_valid信号,乘加器自动开始计数,在指定周期内完成乘法的运算要求。

​ 在整个矩阵乘法整列中,将外部的 num_valid 信号只接入左上角的第一个乘加模块,后续模块的 num_valid 信号接入左侧或者上方乘加模块的 num_valid_r 输出,且保证最终所有模块在同一个连接图内,即可。这样可以节约乘法器的四组输入与三组输出,有利于减少最终使用的资源。由于这样的整列会重复 64 次,这些细节的节约,对最终是会有比较大的影响的。

10.2 提高乘法器吞吐量

​ 原始设计中,矩阵乘法的运算是多周期的。下一次矩阵乘法运算会等待上一次运算的结果存好后再开始下一次运算。如果考虑流水线化的设计,并不需要等待上一次的计算完全完成,可以在乘法模块上并行运行两次运算,但是这就需要对与输入输出有架构性的调整。

10.3 具体实现

​ 具体实现和详细文档请看 y o f i n g e r t yofingert yofingert 的作业。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。在编写C程序时,需要注意变量的声明和定义、指针的使用、内存的分配与释放等问题。C语言中常用的数据结构包括: 1. 数组:一种存储同类型数据的结构,可以进行索引访问和修改。 2. 链表:一种存储不同类型数据的结构,每个节点包含数据和指向下一个节点的指针。 3. 栈:一种后进先出(LIFO)的数据结构,可以通过压入(push)和弹出(pop)操作进行数据的存储和取出。 4. 队列:一种先进先出(FIFO)的数据结构,可以通过入队(enqueue)和出队(dequeue)操作进行数据的存储和取出。 5. 树:一种存储具有父子关系的数据结构,可以通过中序遍历、前序遍历和后序遍历等方式进行数据的访问和修改。 6. 图:一种存储具有节点和边关系的数据结构,可以通过广度优先搜索、深度优先搜索等方式进行数据的访问和修改。 这些数据结构在C语言中都有相应的实现方式,可以应用于各种不同的场景。C语言中的各种数据结构都有其优缺点,下面列举一些常见的数据结构的优缺点: 数组: 优点:访问和修改元素的速度非常快,适用于需要频繁读取和修改数据的场合。 缺点:数组的长度是固定的,不适合存储大小不固定的动态数据,另外数组在内存中是连续分配的,当数组较大时可能会导致内存碎片化。 链表: 优点:可以方便地插入和删除元素,适用于需要频繁插入和删除数据的场合。 缺点:访问和修改元素的速度相对较慢,因为需要遍历链表找到指定的节点。 栈: 优点:后进先出(LIFO)的特性使得栈在处理递归和括号匹配等问题时非常方便。 缺点:栈的空间有限,当数据量较大时可能会导致栈溢出。 队列: 优点:先进先出(FIFO)的特性使得
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。
该资源内项目源码是个人的课程设计、毕业设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 该资源内项目源码是个人的课程设计,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! ## 项目备注 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值