首页 > 其他分享 >CPU高速缓存行对齐和代码优化

CPU高速缓存行对齐和代码优化

时间:2022-10-03 14:05:49浏览次数:54  
标签:缓存 CacheLine 代码优化 64 线程 CPU 高速缓存


CacheLine
众所周知,计算机将数据从主存读入Cache时,是把要读取数据附近的一部分数据都读取进来这样一次读取的一组数据就叫做CacheLine,每一级缓存中都能放很多的CacheLine

两种方法查看:

1.cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size

2. cat /proc/cpuinfo

3.lscpu

多核CUP

L1、L2、L3指一级缓存,二级缓存,三级缓存。其中一级缓存分指令缓存和数据缓存,通过lscpu命令,可以看到l1d和l1i。
CUP中的每个核均可单独处理一个线程
每个核公用L3

超线程
一个核中有多套PC和Register,他们公用一个ALU,这样一个核可以处理多个线程
如四核八线程就由此而来

Volatile的可见性

1、x被标记了volatile
2、两个线程运算时是将缓存中要被运算数所在的整条CacheLine复制到线程自己的存储,并进行运算,运算之后写回缓存
3、假设线程1修改了x并写回,但是线程2中的x还是未修改的x
4、由于x被标记了volatile,在线程1写回x缓存时,线程1会通知线程2重新读取缓存中的x

伪共享

1、线程1、2公共使用同一个CacheLine
2、x、y在同一个CacheLine
3、x、y都是volatile(x和y不是线程安全的,如果不是volatile,数据会不同步)
4、如果线程1不断修改x,线程2不断修改y,那么修改的时候线程1就要不断通知线程2更新x、线程2就要不断通知线程1更新y
5、这样的不断通知不断重新读取很浪费性能
6、这就叫伪共享

CacheLine对齐
多线程会有上面的伪共享的问题,如果在缓存读取数据到CacheLine时,两个volatile的数被读取到不同的CacheLine中的话,就不需要一直通知另一个线程更新数据了,因为另一个线程根本没有这个数据

那么如何让两个数据一定在不同的CacheLine呢,方法就是Cache Line对齐

一般一个CacheLine是64字节,也就是8个long,我们可以把x定义为long,并同时定义7个没有用的long变量,这样这8个数就在同一个CacheLine中
之后再定义y,y自然也就在下一个CacheLine中了
这就叫CacheLine对齐,这样两线程就不会出现伪共享的现象了

       CPU的高速缓存一般分为一级缓存、二级缓存和三级缓存。CPU在运行时首先从一级缓存读取数据,如果读取失败则会从二级缓存读取数据,如果仍然失败则再从三级缓存,再失败最后从内存中存读取数据。而CPU从缓存或主内存中最终读取到数据所耗费的时钟周期差距是非常之大的。因此高速缓存的容量和速度直接影响到CPU的工作性能。一级缓存都内置在CPU内部并与CPU同速运行,可以有效的提高CPU的运行效率。一级缓存越大,CPU的运行效率往往越高。

缓存对齐如何编码
 一级缓存又分为数据缓存和指令缓存,他们都由高速缓存行组成,对于X86_64架构的CPU来说,高速缓存行一般是64个字节,CPU大约只有512行高速缓存行,也就是说约32k的一级缓存。二级缓存一般有1-2MB,三级缓存可以达到33MB-64MB.

 查看cpu0 的一级缓存中的有多少组

$ cat /sys/devices/system/cpu/cpu0/cache/index0/number_of_sets

$64

查看cpu0的一级缓存中一组中的行数

$cat /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity

$8

        当CPU需要读取一个变量时,该变量所在的以64字节分组的内存数据将被一同读入高速缓存行,所以,对于性能要求严格的程序来说,充分利用高速缓存行的优势非常重要。一次性将访问频繁的64字节数据对齐后读入高速缓存中,减少CPU高级缓存与低级缓存、内存的数据交换。

       但是对于多CPU的计算机,情况却又不一样了。例如:

1、      CPU1 读取了一个字节,和它相邻的字节被读入 CPU1 的高速缓存。

2、      CPU2 做了上面同样的工作。这样 CPU1 , CPU2 的高速缓存拥有同样的数据。

3、      CPU1 修改了那个字节,被修改后,那个字节被放回 CPU1 的高速缓存行。但是该信息并没有被写入RAM 。

4、      CPU2 访问该字节,但由于 CPU1 并未将数据写入 RAM ,导致了数据不同步。

        当一个 CPU 修改高速缓存行中的字节时,计算机中的其它 CPU会被通知,它们的高速缓存将视为无效。于是,在上面的情况下, CPU2 发现自己的高速缓存中数据已无效, CPU1 将立即把自己的数据写回 RAM ,然后 CPU2 重新读取该数据。 可以看出,高速缓存行在多处理器上会导致一些不利。

         从上面的情况可以看出,在设计数据结构的时候,应该尽量将只读数据与读写数据分开,并具尽量将同一时间访问的数据组合在一起。这样 CPU 能一次将需要的数据读入。

如:

struct __a
{
    int id; // 不易变
    int factor;// 易变
    char name[64];// 不易变
    int value;// 易变
};
这样的数据结构就很不利。

在 X86_64 下,可以试着修改和调整它

struct __a
{
    char name[64];// 不易变
    int id; // 不易变
    char __Align[64 – sizeof(int)+sizeof(name)*sizeof(name[0])%64]
    int factor;// 易变
    int value;// 易变
    char __Align2[64 – 2* sizeof(int)%64]
};
64 – sizeof(int)+sizeof(name)*sizeof(name[0])%64

64表示 X86_64 架构缓存中,高速缓存行为64字节 大小。 __Align 用于显式对齐。

再来一个有利于高速缓存行的例子:

struct CUSTINFO
{
    DWORD dwCustomerID; //Mostly read-only
    int nBalanceDue; //Read-write
    char szName[100]; //Mostly read-only
    FILETIME ftLastOrderDate; //Read-write
};
改版后的结构定义 :

// Determine the cache line size for the host CPU.
//为各种CPU定义告诉缓存行大小
#ifdef _X86_
#define CACHE_ALIGN 32
#endif
 
#ifdef _X86_64
#define CACHE_ALIGN 64
#endif
 
#ifdef _ALPHA_
#define CACHE_ALIGN 64
#endif
 
#ifdef _IA64_
#define CACHE_ALIGN ??
#endif
 
 
#define CACHE_PAD(Name, BytesSoFar) \
BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]
 
struct CUSTINFO
{
    DWORD dwCustomerID; // Mostly read-only
    char szName[100]; // Mostly read-only
 
    //Force the following members to be in a different cache line.
    //这句很关键用一个算出来的Byte来填充空闲的告诉缓存行
    //如果指定了告诉缓存行的大小可以简写成这样
    //假设sizeof(DWORD) + 100 = 108;告诉缓存行大小为32
    //BYTE[12];
    //作用呢就是强制下面的数据内容与上面数据内容不在同一高速缓存行中。
    CACHE_PAD(bPad1, sizeof(DWORD) + 100);
    int nBalanceDue; // Read-write
 
    FILETIME ftLastOrderDate; // Read-write
 
    //Force the following structure to be in a different cache line.
    CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));
};

    高速缓存控制器是针对数据块,而不是字节进行操作的。从程序设计的角度讲,高速缓存其实就是一组称之为缓存行(cache line)的固定大小的数据块,其大小是以突发读或者突发写周期的大小为基础的。

    每个高速缓存行完全是在一个突发读操作周期中进行填充或者下载的。即使处理器只存取一个字节的存储器,高速缓存控制器也启动整个存取器访问周期并请求整个数据块。缓存行第一个字节的地址总是突发周期尺寸的倍数。缓存行的起始位置总是与突发周期的开头保持一致。

    现代处理器有专门的功能单元来执行加载和存储操作。加载单元每个时钟周期只有启动一条加载操作;与加载操作一样,在大多数情况下,存储操作能够在完整流水线化的模式中工作,每个周期开始一条新的存储。

       

标签:缓存,CacheLine,代码优化,64,线程,CPU,高速缓存
From: https://blog.51cto.com/u_14006572/5730189

相关文章

  • [架构之路-16]:目标系统 - 硬件平台 - CPU主要物理性能指标
    目录​​第1章多核CPU的架构​​​​第2章 CPU主要的物理性能指标​​​​2.1CPU频率​​​​2.2CPU的位和字长​​​​2.3CPU的缓存容量与性能​​​​2.4CPU工作电......
  • [架构之路-17]:目标系统 - 硬件平台 - ARM CPU架构与系列选型
    目录​​第1章 ARM概述​​​​1.1ARM的过去​​​​1.2 ARM内核架构版本与ARMCPU家族的对应关系​​​​1.3 ARMCPU家族系列​​​​1.4ARM cortex系列​​​​第......
  • CPU-数的表示-码
    1.原码表示法: +符号位0,-符号位1;2.补码表示法:{原码->补码,+原码==补码,-原码符号位不变,数据位取反加一},{补码->原码-原码符号位不变};3.反码表示法:-取反不加一;4.......
  • CPU--ALU逻辑单元--快速进位链
    加法器,电路逻辑实现,每一位对应相加,再加上低位来的进位;进位是运算速度的瓶颈;进位链:传送进位的链路;串行进位链:串行进位,与非门实现,从后往前,电路简单;......
  • CPU-计算机数的运算--浮点运算
    1.浮点运算的加减法;对阶,尾数求和,规格化,舍入,溢出判断;对阶:小->大尾数求和:补码    舍入:在对阶和右移的过程中,可能出现尾数末位丢弃,需要舍入......
  • CPU-计算机数的运算--定点运算
    1.定点表示;2.浮点表示; 3.1定点运算--算数移位,逻辑移位;3.2定点运算--加减; 溢出判断:一位符号位,数据最高进位和符号位进位采用异或电路判断;:二位符号位,运......
  • CPU--结构与功能简介2--中断系统
    中断请求标记:INTR中断判优逻辑:(硬件)排队器实现,(软件)程序查询。中断服务程序入口地址查找:硬件向量法,软件查询法;中断响应:响应中断的条件,EINT置1,响应中断的时间,执行周期结束......
  • CPU--结构和功能简介1
    1.结构和功能简介 功能决定结构; 2.指令周期:取址周期,间址周期,执行周期,中断周期;3.指令流水指令流水:各级部件最大时间执行,没有停顿。 ......
  • CPU--指令系统
    1.机器的指令的一般格式:操作码字段,地址码字段; 2.数据在存储器中的存放方式:a,从任意位置开始--不浪费空间,读写控制比较复杂; :b,从一个存储......
  • Jstack排查线上CPU100%
    Jstack排查线上CPU100%介绍jstack是JVM自带的Java堆栈跟踪工具,用于生成java虚拟机当前时刻的线程快照,来帮助定位线程出现长时间停顿的原因,例如死锁、死循环等。语法j......