天天看点

今天给大家分享一下计算机硬件底层原理知识!汇编语言的执行过程

汇编语言的执行过程

首先,计算机是个傻瓜,它所做的所有复杂运算都是基于高低电平,转成我们逻辑上的就是0和1,计算机只认识0和1。

最早的时候,有一种纸带计算机,在纸带上打孔表示1,不打孔表示0,让计算机去读。

关于纸带计算机,有一个传说:有一个人的纸带检查了很多遍都没问题,但是输出就是不对,后来发现有一个孔,被一个小虫子堵住了,虫子的英文叫bug,所以后来都把找问题叫做找bug。

所以最早的程序员编程都是使用计算机能直接识别的机器语言,也就是0和1:01001101 00110100 ...(我瞎编的)

但是这种机器语言对人来说可读性太差了,写起来很费劲,人们就给 

机器语言

起了别名,于是就有了汇编语言。

比如:

01001000

 是移动的意思(我瞎编的),人们起了别名 

mov

与之匹配;

10110011

 是做加法的意思(也是我瞎编的),人们起了别名 

add

与之匹配;

这样就方便记很多。

所以:汇编语言的本质就是机器语言的助记符。

汇编语言的执行过程:

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

CPU的组成

PC:Program Counter(程序计数器),用于记录当前指令的地址

Registers:寄存器,暂时存储CPU计算需要的数据,速度极快

ALU:Arithmetic & Logic Unit,逻辑运算单元

CU:Control Unit,控制单元

MMU:Memory Management Unit,内存管理单元

超线程

我们经常能听到四核八线程这类术语,也就是所谓的超线程,其实特别简单,也就是一个 

ALU

对应多个 

PC|Registers

这么做的好处就是:

原来CPU在做**线程切换(context switch)**的时候,需要把旧线程的数据拿出去,存到缓存里,再把新线程的数据放进来,这种线程切换需要消耗CPU资源;

现在不需要这么麻烦了,直接让ALU切换对应的 

PC|Registers

地址就可。

今天给大家分享一下计算机硬件底层原理知识!汇编语言的执行过程

缓存

为什么要加缓存:因为CPU速度太快,内存的速度太慢。CPU和内存的速度大概是100:1。

存储器的层次结构

今天给大家分享一下计算机硬件底层原理知识!汇编语言的执行过程

CPU到不同层级存储器的访问速度

存储器 访问速度
Registers < 1ns
L1 cache 约1ns
L2 cache 约3ns
L3 cache 约15ns
main memory 约80ns

多核CPU的缓存结构

今天给大家分享一下计算机硬件底层原理知识!汇编语言的执行过程

每个核心有自己的L1、L2,每颗CPU内的核心共享L3,多个CPU共享内存。

按块读取

  • 程序的局部性原理

    表现为:时间局部性和空间局部性。时间局部性是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。空间局部性是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。

按块读取是指读取某个存储单元时,会把相邻的一块存储单元一起读取,下次读取相邻存储单元的数据时,根据程序局部性原理,可以减少IO次数,提高效率。

cache line(缓存行)

缓存行越大,局部性空间利用率越高,但读取时间慢;

缓存行越小,局部性空间利用率越低,但读取时间快。

英特尔CPU经过实践得到一个折中值,64字节,其他CPU不一定是64字节。

缓存一致性协议,这里介绍一个英特尔CPU使用的缓存一执行协议,MESI:www.cnblogs.com/z00377750/p…

缓存行对齐

今天给大家分享一下计算机硬件底层原理知识!汇编语言的执行过程
  • 伪共享:如上图,相邻的两块区域x 和 y,位于同一个缓存行,两个线程分别修改x和y的值,会让对方缓存中的数据失效,重新从内存中读取数据,这就是伪共享。

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

  • 不使用缓存行对齐
/**
 * 缓存行对齐实验
 * - 未使用缓存行对齐的情况
 * arr[0]和arr[1]位于同一个缓存行内
 */
public class T01_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);
    }
}
复制代码
           
  • 使用缓存行对齐
/**
 * 缓存行对齐实验
 * - 使用缓存行对齐的情况
 * arr[0]和arr[8]位于不同的缓存行内
 */
public class T02_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);
    }
}

复制代码
           
  • JDK8中,提供了

    @Contended

    注解,用于保证修饰的字段不在同一个缓存行内,使用时需要加上虚拟机参数

    -XX:-RestrictContended

    才会生效
import sun.misc.Contended;

/**
 * jdk8开始提供的缓存行对齐注解
 *
 * 需要加上虚拟机参数 -XX:-RestrictContended
 */
public class T03_Contended {

    @Contended
    public static volatile long x;

    @Contended
    public static volatile long y;

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                x = i;
            }
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < 10000_0000L; i++) {
                y = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}
复制代码
           

乱序执行

CPU乱序执行的本质是为了提高效率。

DCL 单例为什么要加volatile

创建对象源码:

class T {
  int m = 8;
}

T t = new T();
复制代码
           

创建一个对象大概分为三步:

1: 申请一块内存,给属性赋默认值;

2: 执行构造方法;

3: 把对象的地址赋给引用。

volatile

可以禁止指令重排序,如果不加 

volatile

,那么第2、3步可能会被重排序,导致把一个未初始化的不完整的对象暴露出来。

如何禁止指令重排

CPU层面:内存屏障/总线锁

内存屏障:对某部分内存做操作时前后添加的屏障,屏障前后的操作不可以乱序执行。

Intel的CPU提供了一些原语(lfence,sfence,mfence),也可以使用总线锁来解决。

  • sfence:在sfence指令前的写操作当必须在sfence指令后的写操作之前完成
  • lfence:在lfence指令前的读操作当必须在lfence指令后的读操作之前完成
  • mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作之前完成

JVM级别:8个happens-before原则和4个内存屏障(LL,SS,SL, LS)

happens-before原则(JVM规定重排序必须遵守的规则)

  • 程序次序规则: 在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen—before(时间上)后执行的操作 (同一个线程中前面的所有写操作对后面的操作可见)
  • 管程锁定规则:一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。 (如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))
  • volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。 (如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))
  • 线程启动规则:Thread.start()方法happen—before调用用start的线程前的每一个操作。 (假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。)
  • 线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。 (线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。)
  • 线程中断规则:对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。 (线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作)
  • 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。 (对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。)
  • 传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。 (A h-b B , B h-b C 那么可以得到 A h-b C)

JSR内存屏障

  • LoadLoad屏障:

    对于这样的语句Load1;LoadLoad;Load2,

    在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

  • StoreStore屏障:

    对于这样的语句Store1;StoreStore;Store2,

    在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见。

  • LoadStore屏障:

    对于这样的语句Load1;LoadStore;Store2,

    在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

  • StoreLoad屏障:

    对于这样的语句Store1;StoreLoad;Load2,

    在Load2及后续的读操作执行前,保证Store1的写入操作对其他处理器可见。

as-if-serial : 不管硬件什么顺序,单线程执行的结果不变,看上去像是serial

合并写技术

Write Combining Buffer,一般是4个字节

由于ALU速度太快,所以在写入L1的同时,写入一个WC Buffer,满了之后,直接更新到L2。

NUMA

  • UMA:Uniform Memory Access,统一内存访问

    缺点:不易拓展,CPU数量增多后引起内存访问冲突加剧,CPU的很多资源花在争抢内存地址上面,(4颗比较合适)

  • NUMA:Non Uniform Memory Access,非统一内存访问

    在NUMA架构中,一组CPU和一些内存是放在一起的,可以理解为专属的内存,访问效率高。

计算机启动过程

今天给大家分享一下计算机硬件底层原理知识!汇编语言的执行过程
  1. 通电,BIOS,UEFI工作,通电自检,到硬盘固定位置(第一个扇区)加载bootloader;
  2. 从cmos读取可配置信息。

继续阅读