伪LRU设计

LRU

最近最少使用(LRU,Least Recently Used)替换策略,用于在缓存满时决定应该替换哪个缓存路(way)中的内容

工作原理

LRU 替换策略概述

  • LRU策略的基本思想是:在缓存需要替换一个路(way)时,选择最近最少使用的路。
  • 通过记录每次访问的路的顺序或频率,可以实现这种策略。在实现上,本模块采用了一种伪LRU(pseudo-LRU)算法,它通过树结构来记录和判断哪个路是最少使用的。

LRU树结构的表示

  • 在4路组相联缓存中,LRU状态可以用3位二进制表示。三个内部节点a、b、c的状态可以用来确定每个路是MRU(Most Recently Used,最近最常使用)还是LRU。
  • 比如,在4路情况下,00?表示路0和1之间选择,?10表示路2和3之间选择,具体的0或1决定了是哪条路径(路)是LRU
    //              d   
    //         /        \
    //        b          c
    //      /   \       /  \
    //     a     c     d    e
    //    / \   / \   / \  / \
    //   0   1 2   3 4   5 6  7   

在该结构中,0表示左子树优先,1表示右子树优先。

某一次访问前,{a,b,c,d,e,f,g} = 7'b0,访问way1,则对于子树a而言,way1被最近访问了,则a=0;对于子树b而言,左子树被最近访问了,则b=1;对于树d而言,左子树被最近访问了,则d=1.因此此时{a,b,c,d,e,f,g} = {0,1,c,1,e,f,g}.把访问前的c,e,f,g代入,得到新的状态值。

SRAM模块lru_data

  • 该模块使用SRAM存储LRU标志位,通过read_en、read_addr信号来读取当前的LRU状态。
  • 每当一个集合中的某一条路被访问或替换时,LRU标志位会更新,以反映新的最常使用路

状态更新

  • 当发生替换或访问时,标志位会根据伪LRU规则进行更新,确保LRU策略的正确性。
  • update_flags根据当前的LRU标志位和新访问的路生成新的LRU状态,这个状态会被写回SRAM。

module cache_lru
    #(parameter NUM_SETS = 1,
    parameter NUM_WAYS = 4,    // Must be 1, 2, 4, or 8
    parameter SET_INDEX_WIDTH = $clog2(NUM_SETS),
    parameter WAY_INDEX_WIDTH = $clog2(NUM_WAYS))
    (input                                clk,
    input                                 reset,

    // Fill interface. Used to request LRU to replace when filling.
    input                                 fill_en,
    input [SET_INDEX_WIDTH - 1:0]         fill_set,
    output logic[WAY_INDEX_WIDTH - 1:0]   fill_way,

    // Access interface. Used to move a way to the MRU position when
    // it has been accessed.
    input                                 access_en,
    input [SET_INDEX_WIDTH - 1:0]         access_set,
    input                                 update_en,
    input [WAY_INDEX_WIDTH - 1:0]         update_way);

    localparam LRU_FLAG_BITS =
        NUM_WAYS == 1 ? 1 :
        NUM_WAYS == 2 ? 1 :
        NUM_WAYS == 4 ? 3 :
        7;    // NUM_WAYS = 8

    logic[LRU_FLAG_BITS - 1:0] lru_flags;
    logic update_lru_en;
    logic [SET_INDEX_WIDTH - 1:0] update_set;
    logic[LRU_FLAG_BITS - 1:0] update_flags;
    logic [SET_INDEX_WIDTH - 1:0] read_set;
    logic read_en;
    logic was_fill;
    logic[WAY_INDEX_WIDTH - 1:0] new_mru;
`ifdef SIMULATION
    logic was_access;
`endif

    assign read_en = access_en || fill_en;
    assign read_set = fill_en ? fill_set : access_set;
    assign new_mru = was_fill ? fill_way : update_way;
    assign update_lru_en = was_fill || update_en;

    initial
    begin
        // Must be 1, 2, 4, or 8
        assert(NUM_WAYS <= 8 && (NUM_WAYS & (NUM_WAYS - 1)) == 0);
    end

    // This uses a pseudo-LRU algorithm
    // Assuming four sets, the current state of each set is represented by
    // three bits. Imagine a tree:
    //
    //        b
    //      /   \
    //     a     c
    //    / \   / \
    //   0   1 2   3
    //
    // The leaves 0-3 represent ways, and the letters a, b, and c represent the 3 bits
    // which indicate a path to the *least recently used* way. A 0 stored in a interior
    // node indicates the  left node and a 1 the right. Each time an element is moved
    // to the MRU, the bits along its path are set to the opposite direction. This means
    // it will take at least two cycles for a node in the MRU to move to the LRU and
    // be evicted. A strict LRU would take three cycles for the node to move to the
    // LRU. So, this is close enough to LRU to work well, but much simpler to implement.
    //
    sram_1r1w #(
        .DATA_WIDTH(LRU_FLAG_BITS),
        .SIZE(NUM_SETS),
        .READ_DURING_WRITE("NEW_DATA")
    ) lru_data(
        // Fetch existing flags
        .read_en(read_en),
        .read_addr(read_set),
        .read_data(lru_flags),

        // Update LRU (from next stage)
        .write_en(update_lru_en),
        .write_addr(update_set),
        .write_data(update_flags),
        .*);

    // XXX I bet there's a way to programmatically create update_flags
    // and fill_way with a generate loop instead of hard-coding like
    // I've done here.
    generate
        case (NUM_WAYS)
            1:
            begin
                assign fill_way = 0;
                assign update_flags = 0;
            end

            2:
            begin
                assign fill_way = !lru_flags[0];
                assign update_flags[0] = !new_mru;
            end

            4:
            begin
                always_comb
                begin
                    casez (lru_flags)
                        3'b00?: fill_way = 0;
                        3'b10?: fill_way = 1;
                        3'b?10: fill_way = 2;
                        3'b?11: fill_way = 3;
                        default: fill_way = '0;
                    endcase
                end

                always_comb
                begin
                    case (new_mru)
                        2'd0: update_flags = {2'b11, lru_flags[0]};
                        2'd1: update_flags = {2'b01, lru_flags[0]};
                        2'd2: update_flags = {lru_flags[2], 2'b01};
                        2'd3: update_flags = {lru_flags[2], 2'b00};
                        default: update_flags = '0;
                    endcase
                end
            end

            8:
            begin
                always_comb
                begin
                    casez (lru_flags)
                        7'b00?0???: fill_way = 0;
                        7'b10?0???: fill_way = 1;
                        7'b?100???: fill_way = 2;
                        7'b?110???: fill_way = 3;
                        7'b???100?: fill_way = 4;
                        7'b???110?: fill_way = 5;
                        7'b???1?10: fill_way = 6;
                        7'b???1?11: fill_way = 7;
                        default: fill_way = '0;
                    endcase
                end

                always_comb
                begin
                    case (new_mru)
                        3'd0: update_flags = {2'b11, lru_flags[5], 1'b1, lru_flags[2:0]};
                        3'd1: update_flags = {2'b01, lru_flags[5], 1'b1, lru_flags[2:0]};
                        3'd2: update_flags = {lru_flags[6], 3'b011, lru_flags[2:0]};
                        3'd3: update_flags = {lru_flags[6], 3'b001, lru_flags[2:0]};
                        3'd4: update_flags = {lru_flags[6:4], 3'b011, lru_flags[0]};
                        3'd5: update_flags = {lru_flags[6:4], 3'b001, lru_flags[0]};
                        3'd6: update_flags = {lru_flags[6:4], 1'b0, lru_flags[2], 2'b01};
                        3'd7: update_flags = {lru_flags[6:4], 1'b0, lru_flags[2], 2'b00};
                        default: update_flags = '0;
                    endcase
                end
            end

            default:
            begin
                initial
                begin
                    $display("%m invalid number of ways");
                    $finish;
                end
            end
        endcase
    endgenerate

    always_ff @(posedge clk)
    begin
        update_set <= read_set;
        was_fill <= fill_en;
    end

`ifdef SIMULATION
    always_ff @(posedge clk, posedge reset)
    begin
        if (reset)
            was_access <= 0;
        else
        begin
            // Can't update when the last cycle didn't perform an access.
            assert(!(update_en && !was_access));
            was_access <= access_en;    // Debug only
        end
    end
`endif
endmodule

接口信号

  • clk:时钟信号。
  • reset:重置信号。
  • fill_en:填充使能信号,表示需要替换一个缓存路。
  • fill_set:指定填充的集合索引。
  • fill_way:输出信号,表示在fill_set中选择的路索引,这个路将被替换。
  • access_en:访问使能信号,表示缓存中的某个路被访问了。
  • access_set:访问的集合索引。
  • update_en:更新使能信号,表示需要更新LRU状态。
  • update_way:访问的路索引,表示该路刚刚被访问,需要更新其LRU状态

读写冲突分析

  • 单端口双工(1R1W)

    • 1R1W 表示该SRAM有一个读端口和一个写端口,可以在同一时钟周期内进行一次读操作和一次写操作。这意味着你可以在一个时钟周期内从某个地址读取数据的同时,将数据写入同一或不同的地址。
    • 这种设计允许缓存逻辑在更新LRU状态的同时,仍然能够读取当前的LRU标志位,这对于提高流水线性能和减少延迟非常有用。
  • SRAM操作

    • read_en 和 read_addr:这两个信号控制读操作。如果 read_en 被置为高电平,SRAM将在 read_addr 指定的地址处读取数据。
    • write_en 和 write_addr:这两个信号控制写操作。如果 write_en 被置为高电平,SRAM将在 write_addr 指定的地址处写入数据(写入的数据由 write_data 信号提供)。
    • 由于这是一个单端口双工SRAM,所以即使是在同一周期进行读写操作,也不会产生冲突,前提是读写操作访问的不是同一个地址。

对于本模块而言,本模块的适用场景位于IFETCH阶段,PC的值(指令地址)是+4或者跳转的,不可能出现循环访问同一条指令(死循环),因此,该模块可能出现的读写冲突,即循环访问同一地址,access某个set后,读和写会在一个地址上的场景不会出现。

为什么读写同一地址很少发生

由于ICACHE的设计和使用模式,读写同一地址的情况很少出现,原因如下:

  • 顺序性:处理器通常以顺序的方式读取指令。即使发生跳转(branch)或函数调用,新的指令地址也与之前的读写地址不同。
  • 缓存未命中:当发生缓存未命中时,缓存会将数据块写入空闲的缓存位置,而不会立即去读取新写入的数据。此外,写操作通常是在一个独立的时钟周期内完成的,读操作和写操作不会在同一个周期针对同一地址进行。
  • 多路组相联结构:ICACHE通常是多路组相联(set-associative)结构,不同的地址会映射到不同的缓存路,因此即使读写操作发生在同一个缓存集合中,也会操作不同的路(way)。
很抱歉,由于我是一个自然语言处理模型,无法为您编写完整的程序代码。但是,我可以为您提供一些思路和代码,希望能够帮助您完成这个任务。 思路: 1. 读取页面序列,存储到一个数组中。 2. 使用两个队列分别实现FIFO和LRU算法。 3. 遍历页面序列,对于每个页面: - 如果页面在队列中已经存在,则不需要进行任何操作。 - 如果队列未满,则直接把页面加入队列。 - 如果队列已满,则根据算法淘汰一个页面,并把新页面加入队列。 4. 统计每个算法淘汰的页面号和缺页中断率。 代码: ``` // 定义两个队列 queue<int> fifo_q; // FIFO算法队列 queue<int> lru_q; // LRU算法队列 // 读取页面序列 int pages[MAX_PAGES]; int n_pages = read_pages("page.txt", pages); // 初始化统计变量 int fifo_faults = 0; int lru_faults = 0; vector<int> fifo_evicted; vector<int> lru_evicted; // 遍历页面序列 for (int i = 0; i < n_pages; i++) { int page = pages[i]; // FIFO算法 if (fifo_q.size() < PAGE_FRAME_SIZE) { // 队列未满,直接加入页面 fifo_q.push(page); } else { // 队列已满,淘汰队首页面并加入新页面 int evicted = fifo_q.front(); fifo_q.pop(); fifo_q.push(page); fifo_evicted.push_back(evicted); fifo_faults++; } // LRU算法 if (lru_q.size() < PAGE_FRAME_SIZE) { // 队列未满,直接加入页面 lru_q.push(page); } else { // 队列已满,淘汰最久未使用的页面并加入新页面 int evicted = lru_q.front(); lru_q.pop(); while (!lru_q.empty() && lru_q.front() != evicted) { lru_q.push(lru_q.front()); lru_q.pop(); } lru_q.push(page); lru_evicted.push_back(evicted); lru_faults++; } } // 输出结果 cout << "FIFO algorithm evicted pages: "; for (int i = 0; i < fifo_evicted.size(); i++) { cout << fifo_evicted[i] << " "; } cout << endl; cout << "FIFO algorithm fault rate: " << (double)fifo_faults / n_pages << endl; cout << "LRU algorithm evicted pages: "; for (int i = 0; i < lru_evicted.size(); i++) { cout << lru_evicted[i] << " "; } cout << endl; cout << "LRU algorithm fault rate: " << (double)lru_faults / n_pages << endl; ``` 注意:上述代码仅为代码,可能存在语法错误和逻辑错误,请根据实际情况进行修改。另外,代码中使用了STL库中的queue和vector容器,请确保您的编译环境支持STL库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值