CPU=和=内=存=等=硬=件=知=识

相关书籍推荐

读书的原则:不求甚解,观其大略

  • 《编码:隐藏在计算机软件背后的语言》
  • 《深入理解计算机操作系统》
    数据结构与算法
  • 《Java数据结构与算法》
  • 《算法》(红皮,经典,¥99)
  • 《算法导论》
  • 《计算机程序设计艺术》
    操作系统
  • 《Linux内核源码解析》
  • 《30天自制操作系统》
    网络
  • 机工《TCP/IP详解》卷一
  • 编译原理:机工 红书 《编程语言实现模式》《编译原理》
    数据库
  • SQLite 源码 Derby-JDK自带数据库

硬件基础知识

汇编语言(机器语言)的执行过程

汇编语言的本质:机器语言的助记符(帮助人们理解机器语言),它本身其实就是机器语言。

计算机通电 -> CPU读取内存中的程序(电信号输入)-> 时钟发生器不断震荡通断电 -> 推动CPU一步一步执行(执行多少次取决于指令需要的时钟周期)-> 计算完成 -> 写回(电信号)-> 写给显卡输出(sout,或者图形)

CPU的基本组成

  • PC:Program Counter程序计数器(记录当前指令的地址)简单理解就是保存当前指令的地址+长度。CPU怎么直到下一条指令在哪儿(PC加1?)不是,因为指令长度不是固定的,如果是固定的,简单加1就可以了,所以得看当前指令占了多少字节,如果占了3个就加个3,占了5个就加个5。

  • Registers:寄存器,暂时存储CPU计算需要用到的数据。寄存器的数量非常多,几十个寄存器,现在最多有上百个寄存器,如果你是干汇编的,需要了解每一个寄存器是干什么的。相较于内存,寄存器的读写速度更快,因为它在CPU内部,所以CPU在计算的时候,会先把内存中的数据拷贝到寄存器。现在的一般一个寄存器是64位的。

  • ALU:Arithmetic& Logic Unit运算逻辑单元,CPU执行计算用的,例如计算2+3。

  • CU:Control Unit 控制单元。对计算过程中的各种信号做控制。

  • MMU:Memory Management Unit 内存管理单元。最早的内存管理都是操作系统软件实现的,现在都是硬件+操作系统实现的。

  • cache:建议看JVM中的内存管理那一节,但是以下内容为最新内容。

  • 超线程:一个ALU对应多个PC | Registers 所谓的四核八线程。
    上下文切换:平时我们在运行一个线程的时候,这个线程相关的数据和指令,存在寄存器(数据)和PC(指令地址)里面,另外一个线程要执行,需要把寄存器和PC中的值拿出来保存到内存,再把当前线程的数据和指令存进去。这个就叫线程的上下文切换。这样效率比较低。而超线程就是说,一下子CPU里可以装两线程甚至更多,线程切换的时候,不需要上下文切换那么复杂(一个线程踢出CPU,再装另一个线程)。
    在这里插入图片描述

存储器的层次结构

在这里插入图片描述

  • 为什么要有缓存?
    因为CPU访问不同的部件,速度是不一样的:
    Registers:<1ns
    L1 cache:约1ns
    L2 cache:约3ns
    L3 cache:约15ns
    main memory:约80ns
  • 多核CPU
    在这里插入图片描述
    如图:一颗CPU里有两个核,每一个核有自己的Registers、L1、L2,两个核共享L3。多颗CPU共享内存。

cache line的概念 缓存对其 伪共享

在这里插入图片描述

  • 按块(cache line)读取

    • 程序局部性原理,可以提高效率
    • 充分发挥总线 CPU 针脚的作用
  • cache line其实就是程序块
    举一个多核数据同步问题的例子。如图,对于CPU中的一个核,如果它想读取数据x,必须把x所在的整个cache line(包括y)都读进来,同样另一个核,如果想读取数据y,也必须把y所在的整个cache line(包括x)都读进来。这样如果其中一个核要修改cache line中的数据,如修改x,那么必须保证另一个核所对应的x也被同步到。这个过程用到了缓存一致性协议。

  • 缓存一致性协议
    一个核修改了数据,要通知另一个核去内存中重新读取改过后的新数据。这里涉及到多核数据同步问题。
    Intel的内存一致性协议
    Intel的内存一致性协议叫MESI Cache 一致性协议

    • CPU每个cache line标记四种状态(额外两位)
      • Modified:在一个核内,数据被修改,在该核内的状态就是Modified
      • InValid:此时另一个核内的相同数据的状态就是InValid。这个核在读取InValid数据的时候需要从内存里面重新读取一遍进来
      • 缓存一致性协议 有时被成为缓存锁,缓存锁不够使的情况下,最终的解决方案需要锁总线。什么是锁总线,一个核去访问内存数据的时候,需要把总线锁了,只有当我访问完了以后,其他核才能去访问。这么做数据永远都会一致。当然和缓存锁相比,锁总线的效率比较低。所以在底层能有MESI的情况下,就不用总线锁。总线锁是最终武器,只有在没办法的时候才会使用的最终方案。
  • 缓存行

    • 缓存行越大,局部性空间效率越高,但读取时间慢
    • 缓存行越小,局部性空间效率越低,但读取时间快
    • 取一个折中值,目前多用:64字节(Intel工业测试后得到的值),相当于8个long或double
package com.mashibing.juc.c_028_FalseSharing;public class T03_CacheLinePadding {public static volatile long[] arr = new long[2];public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0] = i;
            }
        });
​
        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[1] = i;
            }
        });final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}
package com.mashibing.juc.c_028_FalseSharing;public class T04_CacheLinePadding {public static volatile long[] arr = new long[16];public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[0] = i;
            }
        });
​
        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                arr[8] = i;
            }
        });final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

由此诞生了一种新的编程模式:

  • 缓存行对齐 对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存航对齐的编程方式。

  • 缓存行对齐,可以使得该cache line上的数据被线程独占(cache line的状态为E),而在独占的状态下,CPU不需要开辟另外的资源来监控其他线程(如果是S状态的缓存行,CPU需要监控所有拥有该cache line的线程,以保持一致性)。
    在这里插入图片描述

  • JDK7中,很多采用long padding提高效率。这么做可以保证无论从哪个p(i)开始,在64个字节(8个long)以内,cursor所指向的数据独占整个cache line。

  • JDK8,加入了@Contended注解(实验)需要加上:JVM -XX:-RestrictContended
    在这里插入图片描述

    • 据说JDK8里面,可以保证加了@Contended注解的成员变量,可以不与其他的成员变量位于同一个缓存行

乱序执行(指令重排)

老外的相关文章
多核CPU为了提高运行效率,在不影响结果的情况下会对一些指令进行重排序执行。乱序执行的本质是同时执行。

在这里插入图片描述

CPU层面如何禁止指令重排

答案:内存屏障、总线锁

内存屏障(硬件)

对某部分内存进行操作时前后添加的屏障。这个屏障是通过CPU原语来实现的。当然也可以使用总线锁来解决,但这样效率就低了。
在这里插入图片描述
Intel:

  • lfence:读屏障
  • sfence:写屏障
  • mfence:mixed读写屏障

在这里插入图片描述

lock 汇编指令

在这里插入图片描述

  • volatile 的实现就是通过 lock addl 指令,用的总线锁。

JSR 内存屏障(JVM)

JSR 内存屏障,是JVM规范提出的内存屏障实现,是JVM层面的实现,其底层用的就是CPU原语或者总线锁lock指令。

在这里插入图片描述

happens-before 原则

刚才说过,那些东西可以进行重排序,哪些东西不能进行重拍序,在 CPU 这个级别,CPU 的规则和你 JVM 的规则是不一样的。在 CPU 看来,我管你是哪条指令呢,只要前后没有依赖关系,我就可以乱序执行。JVM 的规则是这样的,它定义了8条 happens-before 原则。

  • happens-before 原则作用:规定在某些情况下特殊情况下,你不可以重拍序。
    在这里插入图片描述

as-if-serial

直译:像是顺序执行的。不管如何重拍序,单线程的执行结果不会改变。看上去就像顺序执行一样。

NUMA(Non Uniform Memory Access)

UMA

UMA 统一访问内存,意思就是多个CPU共享同一块内存。

NUMA

在这里插入图片描述

主板的结构上会分成很多组不同的插槽。每一组插槽上有一组CPU,以及和这组CPU里的特别近的内存。每个CPU访问自己这组插槽上的内存,要比访问其他组插槽上的内存,速度要快的多,这叫 NUMA,非统一访问内存。

UMA vs NUMA

UMA 就是所有CPU,大家访问的权限是相同的,没有谁优先访问谁延后访问的这个内存。
而 NUMA 对于自己那组插槽上的内存是有优先级的。

为什么现在很多服务器架构都是NUMA?

是因为 UMA 不容易扩展,据说做了测试CPU数量增多以后会引起内存访问冲突加剧,CPU的很多资源花在争抢内存地址上面,4颗左右比较合适。所以就产生了 NUMA。现在的程序可以做到NUMA aware,也就是能感知到和离CPU最近的那个内存,然后把生成的对象放到那个内存里。

在这里插入图片描述

发布了40 篇原创文章 · 获赞 0 · 访问量 386

猜你喜欢

转载自blog.csdn.net/weixin_43780400/article/details/105342266