CPU高速缓存SRAM命中问题的总结与实验

1,SRAM高速缓存的结构

获取本机CPU的SRAM缓存信息

我使用的是一个叫cpuinfo_x86的小程序,可以获取x86架构的cpu相关信息。下载地址:http://osxbook.com/book/bonus/misc/cpuinfo_x86/cpuinfo_x86.c
gcc编译后运行,以下是当前测试环境的SRAM信息:

L1 Instruction Cache //L1指令缓存
Size : 32K
Line Size : 64B
Sharing : shared between 2 processor threads
Sets : 64
Partitions : 1
Associativity : 8

L1 Data Cache //L1数据缓存
Size : 32K
Line Size : 64B
Sharing : shared between 2 processor threads
Sets : 64
Partitions : 1
Associativity : 8

L2 Unified Cache //L2指令数据缓存
Size : 256K
Line Size : 64B
Sharing : shared between 2 processor threads
Sets : 512
Partitions : 1
Associativity : 8

L3 Unified Cache //L3指令数据缓存
Size : 3M
Line Size : 64B
Sharing : shared between 16 processor threads
Sets : 4096
Partitions : 1
Associativity : 12

Size是该缓存所有数据块的合计大小,Sets是总组数,Associativity是每组的行数,LineSize是每行的数据块大小。指令和数据缓存只用来缓存指令或内存数据,有利于CPU并行取指和访存提速,Unified Cache可以同时缓存两者,L1L2为每核心独享,L3为多核共享,整体结构大概如下图:
在这里插入图片描述
(图1)

内存数据在缓存中的定位与传输

假设有指令要从L1中读一个字节,内存地址为0000000000000000000000000000000000000000000000000000000000000000
由于L1缓存块大小64B, 64 = 2 6 64=2^6 64=26,所以最后六位用于计算数据块中的字节偏移,称为 b 位 \color{orange}b位 b
0000000000000000000000000000000000000000000000000000000000 000000 0000000000000000000000000000000000000000000000000000000000\color{orange}000000 0000000000000000000000000000000000000000000000000000000000000000
组数量也为64,那么可得出组的index为接下来的6位,称为 s 位 \color{red}s位 s
0000000000000000000000000000000000000000000000000000 000000 000000 0000000000000000000000000000000000000000000000000000\color{red}000000\color{orange}000000 0000000000000000000000000000000000000000000000000000000000000000
剩下的位用来寻找该组中存有该数据的行,称为tag标签, t 位 \color{blue}t位 t:
0000000000000000000000000000000000000000000000000000 000000 000000 \color{blue}0000000000000000000000000000000000000000000000000000\color{red}000000\color{orange}000000 0000000000000000000000000000000000000000000000000000000000000000
缓存的控制逻辑硬件解析内存地址后会先用s位定位组,然后并行的用t位定位行。如果找到了组和行既是缓存命中,会用b位来寻找数据位置(或数据起始位置)并返回数据。否则就是缓存不命中,会向下一级缓存读取数据。
每行都有一个数据块,数据块的基本单位为字节,例如L1的数据块为64字节,缓存着内存中0000000000000000000000000000000000000000000000000000000000至 0000000000000000000000000000000000000000000000000000111111的数据。每字节index与b位对应,例如上面要寻找的数据既是在下图行1中的数据块的字节0中:
在这里插入图片描述
(图2)

通过这种机制将内存数据分布在SRAM缓存中有点类似于编程中的散列表。内存中每连续n个字节相当于要存储的value,内存地址的s位相当于key,缓存的组相当于桶,每个行相当于node。与散列表不同的是node数量有限制,如果超出数量限制,将会进行不同策略的行替换,另外每个node之间也没有链接关系,所有行用t位进行标记,由硬件进行并行搜索。
s,b,t位由缓存的结构决定,例如测试环境的L3缓存与L2,L1缓存对比:
L 1 : 0000000000000000000000000000000000000000000000000000 000000 000000 L1:\color{blue}0000000000000000000000000000000000000000000000000000\color{red}000000\color{orange}000000 L1:0000000000000000000000000000000000000000000000000000000000000000
L 2 : 0000000000000000000000000000000000000000000000000 000000000 000000 L2:\color{blue}0000000000000000000000000000000000000000000000000\color{red}000000000\color{orange}000000 L2:0000000000000000000000000000000000000000000000000000000000000000
L 3 : 0000000000000000000000000000000000000000000000 000000000000 000000 L3:\color{blue}0000000000000000000000000000000000000000000000\color{red}000000000000\color{orange}000000 L3:0000000000000000000000000000000000000000000000000000000000000000
这种设计使L1,L2,L3每级缓存之间的数据传输有以下的映射关系(组index由0改为1起始):
这里写图片描述 (图3)

由于内存很大,缓存相对很小,这种映射关系对于正常编程中的数据访问是合理的,但是由于映射关系固定,从L1的角度来看,整个内存地址中有1/64的地址段的数据(对于一个4GB内存来讲既是有64MB的数据)最终都是要缓存到它的某一个组中,如果大量反复访问该范围数据(64MB=1048576个数据块)会造成该组中大量的行替换,增加缓存不命中率,由于行替换会造成访存指令等待,进而导致CPU数据吞吐率降低。

2,缓存读不命中测试

缓存不命中有冷不命中,容量不命中与冲突不命中。缓存在空的状态下,任何读写操作都会造成冷不命中。当工作集(例如循环中要访问的内存地址)整体大小超出该缓存的最大容量时导致的不命中称为容量不命中。
冲突不命中与图3中的映射关系有关,由于内存数据向上一级传输的映射关系是固定的,反复访问内存中的某个子集内的数据会造成大量的不命中,例如根据测试环境,在Unity内进行以下测试:

using System.Collections;
using UnityEngine;

public class TestScript : MonoBehaviour
{

    int[] array1 = new int[10000000];

    // Use this for initialization
    void Start()
    {
        int pTime;
        int cTime;
        int sum = 0;

        Refresh();
        pTime = System.Environment.TickCount;
        sum = Sum1(sum);
        cTime = System.Environment.TickCount;
        Debug.Log("sum=" + sum);
        Debug.Log("以步长8192循环:Time:" + (cTime - pTime));

        Refresh();
        pTime = System.Environment.TickCount;
        sum = 0;
        sum = Sum2(sum);
        cTime = System.Environment.TickCount;
        Debug.Log("sum=" + sum);
        Debug.Log("以步长1循环:Time:" + (cTime - pTime));

        Refresh();
        pTime = System.Environment.TickCount;
        sum = 0;
        sum = Sum3(sum);
        cTime = System.Environment.TickCount;
        Debug.Log("sum=" + sum);
        Debug.Log("以步长8191循环:Time:" + (cTime - pTime));

        Refresh();
        pTime = System.Environment.TickCount;
        sum = 0;
        sum = Sum3(sum);
        cTime = System.Environment.TickCount;
        Debug.Log("sum=" + sum);
        Debug.Log("以步长8193循环:Time:" + (cTime - pTime));
    }

    void Refresh() {
        for (int i = 0; i< array1.Length; i++)
        {
            array1[i] =UnityEngine.Random.Range(1, 100000);
        }
    }

    int Sum1(int sum){
        for (int i = 0,j = 1; j < 5000000;i = 8192 * (j % 1000),j = j + 1){
            sum += array1[i];
        }
        return sum;
    }

    int Sum2(int sum)
    {
        for (int i = 0, j = 1; j < 5000000; i = 1 * (j % 8192), j = j + 1)
        {
            sum += array1[i];
        }
        return sum;
    }

    int Sum3(int sum)
    {
        for (int i = 0, j = 1; j < 5000000; i = 8191 * (j % 1000), j = j + 1)
        {
            sum += array1[i];
        }
        return sum;
    }

    int Sum4(int sum)
    {
        for (int i = 0, j = 1; j < 5000000; i = 8193 * (j % 1000), j = j + 1)
        {
            sum += array1[i];
        }
        return sum;
    }
}

对一个一千万长的int数组(38.15MB)根据不同的步长进行50万次循环,测试结果:
以步长8192循环:Time:330
以步长1循环:Time:84
以步长8191循环:Time:95
以步长8193循环:Time:85

可以看出8192对于测试机来讲是一个神奇的数字,其原因可以从该机CPU的缓存结构中找出。int长度为4个字节32位,8192*4B=32768B=32KB,32KB正好是L1缓存的大小,也既是循环中的所有数据最终都要缓存到L1的组0,CPU对L1的所有访存指令都会造成不命中(除非L1在第一次缓存满八行后只对组中同一行进行行替换,那么在1000次循环中会有7次命中),同一个循环中L2与L3相对L1不命中率会稍微低一些,但是数据从内存到L3的时间会更慢,每次不命中造成的时间惩罚相对L1会更高,对整体的时间惩罚也有相当大的影响。

以上研究的都是读不命中的问题,写不命中时有两种策略,写分配与非写分配,写分配 的行为与读类似,会加载下级缓存数据上来直到L1后修改,非写分配 不加载数据而是不断尝试更改下级缓存中的数据。修改缓存数据后对内存数据的更新又分为写回与直写两种,写回的话每行要维护一个修改位,当该行被替换时如果发现有被修改过会对下一级缓存进行更改。直写是修改数据后立刻更新下一级缓存。这些策略是由硬件决定的。大量连续的直写对总线流量(SRAM<—>DRAM)有影响。写分配比较符合局部性的思想,例如对于小步长的循环,一次写不命中后加载数据到L1会避免之后再次的写不命中。

总之,在应用层面来讲,在循环中以步长为1来对数组进行遍历是最快的,而且最新的CPU还有一个prfetching预取机制(以上测试中的CPU没有),既是在循环前尽量提前将内存中的数组数据向上级缓存传输,在循环过程中提前对缓存中的数据进行更新,这是一个针对步长为1的循环的特殊硬件优化,可以规避容量不命中以及其他一些case。另外根据不同CPU对某个 2 n 2^n 2n为步长对一个超大的数组进行大量的循环是危险的,有可能会造成大量冲突不命中。

————————————————————————————————————
参考:
深入理解计算机系统—Randal E.Bryant, David R.O’Hallaron

维护日志:
2020-2-2:精简,重构

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FPGA外部高速SRAM是一种高速随机存储器,用于与可编程逻辑器件(FPGA)进行外部数据存储与读取操作。它具有以下的特点和优势。 首先,高速SRAM具有快速的存取速度和低延迟。相较于常规的存储器,高速SRAM具有更快的数据读取和写入速度,可以满足FPGA在高性能、实时数据处理和存储方面的要求。 其次,高速SRAM拥有较大的存储容量。它可以提供大容量的存储空间,以满足复杂的FPGA设计所需的数据存储需求,同时还能够支持高带宽的数据传输。 另外,高速SRAM与FPGA的接口简单,易于配置和控制。它可以通过标准的总线接口(如外部存储控制器等)与FPGA相连,方便地实现对存储器的访问和数据读写操作。 此外,高速SRAM还具有较低的功耗和抗干扰能力。由于其内部电路的优化设计,它能够在较低的工作电压下实现高速访问,从而降低功耗。同时,高速SRAM还具有抗干扰能力强的特点,能够在复杂的电磁环境下稳定可靠地工作。 最后,高速SRAM的可编程特性使得它能够灵活应用于不同的FPGA设计。用户可以通过配置高速SRAM的接口和操作模式,适应各种不同的应用需求和数据处理方式。 总结而言,FPGA外部高速SRAM是一种功能强大、性能优越的外部存储器。它通过提供高速的、可编程的数据存储空间,为FPGA设计提供了更大的灵活性和可扩展性,满足了复杂的数据处理和存储需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值