用户工具


编译器重排序

  • 编译源代码时,编译器依据对上下文的分析,对指令进行重排序,使之更适合cpu运行

数字列表项目编译期重排序的典型就是通过调整指令顺序,在不改变程序语义的前提下,尽可能减少寄存器的读取、存储次数,充分复用寄存器的存储值。假设第一条指令计算一个值赋给变量A并存放在寄存器中,第二条指令与A无关但需要占用寄存器(假设它将占用A所在的那个寄存器),第三条指令使用A的值且与第二条指令无关。那么如果按照顺序一致性模型,A在第一条指令执行过后被放入寄存器,在第二条指令执行时A不再存在,第三条指令执行时A重新被读入寄存器,而这个过程中,A的值没有发生变化。通常编译器都会交换第二和第三条指令的位置,这样第一条指令结束时A存在于寄存器中,接下来可以直接从寄存器中读取A的值,降低了重复读取的开销。

cpu重排序

  • 现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。相比于先执行却未命中cpu缓存的指令,后执行的但命中cpu缓存的指令可能更先执行完

内存可见性

  • 由于每个cpu核心都有自己的读/写缓冲区(Load,Store buffer),和cache(L1,L2等独立cache)。当一个cpu对某个变量修改时,最新的值只是放在了读/写缓冲区或者cache(并没有写回内存),并根据MESI协议(cpu缓存一致性协议)将其他cpu中该变量设置为失效。当其他cpu要访问这个变量时发现自己的cache已经失效并重新从内存中读取数据(仍然不是最新的数据)。
  • 该问题其实就是内存可见性引起的,某个时刻修改的变量值没有及时写会主内存,导致其他线程不可见
  • 所以需要一种协议能及时将最新值刷新到内存中。也就是“内存屏障”。 java 中的volatile 就可以认为具有内存屏障的作用。

内存屏障的作用

内存屏障的作用就是告诉编译器2件事

  1. 有屏障的地方加上一个lock 汇编指令,将之前修改且存在于缓存中的数据全部刷新到内存
    1. 这个过程中也让其他cpu的缓存失效了
  2. 屏障后面访问变量的时候,不要适用寄存器中的值,要重新从内存中读取最新值

例子

int a=0;
a++;
a++;

volatile int b=0;
b++;
b++

变量a流程是

  • core0 从内存读取a=0,放入寄存器中
  • 对寄存器中的值+1
  • 写回缓存(并同时将其他cpu的缓存设置成失效)
    • core1 发现缓存失效从内存中从新读取,不能读到最新值
  • 对寄存器中的值+1
    • 直接使用寄存器中的值,多线程下不能读到最新值
  • 写回缓存(并同时将其他cpu的缓存设置成失效)
    • core1 发现缓存失效从内存中从新读取,不能读到最新值

变量b流程是

  • core0 从内存读取b=0,放入寄存器中
  • 对寄存器中的值+1
  • 写回内存(并同时将其他cpu的缓存设置成失效)
    • core1 发现缓存失效从内存中从新读取,能读到最新值
  • core0 从内存读取b=1,放入寄存器中
    • 直接从内存中重新读取,多线程下也能读到最新值
  • 对寄存器中的值+1
  • 写回内存(并同时将其他cpu的缓存设置成失效)
    • core1 发现缓存失效从内存中从新读取,能读到最新值