花里胡哨但没什么卵用系列又来辣
昨天一个朋友问了我关于重排序的问题我整理了一下作为冷知识分享

重排序分类

  • 指令重排序其实分两种:
    第1种是编译器重排序/或者说编译期重排序(此处的编译器是指翻译成汇编语言指令的编译器如GCC/CLang),是为了更适合CPU的流水执行(关于CPU Pipeline可以康康《Digital Design and Computer Architecture》第7.5章,讲的还不戳);
  • 第2种是CPU重排序/或者说运行期重排序,是运行中动态分析,减少流水执行过程中中断指令出现的次数;

防止重排序

为防止第一种重排序,很简单我们加一个volatile就可以了,(在C/C++中volatile会自动插入编译器屏障,Java的volatile功能更加强大),当然如果看高级语言的具体实现如Java的Hotspot(基于OpenJDK11)所谓的编译器屏障,那其实就是在C++代码中内嵌了一段汇编:__asm__ volatile ("" : : : "memory")

static inline void compiler_barrier() {
    __asm__ volatile ("" : : : "memory");
}

注意此时的volatile是底层编译器(如GCC)的宏定义,他的意思就是直接向编译器声明禁止汇编内联优化重排(关于__asm__ volatile ("" : : : "memory")或者写作__asm__ __volatile__ ("" : : : "memory")可以查看__asm__ __volatile__内嵌汇编用法简述),memory告知编译器汇编代码执行内存读取和写入操作,编译器可能需要在执行汇编前将一些指定的寄存器刷入内存(不要将变量缓存到寄存器,因为这段代码可能会用到内存变量,而这些内存变量会以不可预知的方式发生改变,因此GCC插入必要的代码先将缓存到寄存器的变量值写回内存,如果后面又访问这些变量,需要重新访问内存)。

对于第二种重排序,那可就麻烦了。不同的CPU体系架构有不同的处理方式,就拿x86和ARM来说,x86是个内存模型比较强的体系:在x86中Load、Load, Load、Store, Store、Store这三种是不允许发生重排的,只有Store、Load这种被允许重排成Load、Store(即Stores after loads);而ARM就一言难尽了,ARM的内存模型很弱(硬件层面上减少对cache的一致性保护能省不少晶体管,真有你的,不过考虑到ARM设计的初衷就是为了低功耗来看倒是情有可原),在ARM体系(v7、v8....)Load、Load, Load、Store,Store、Load, Store、Store这四种情况都是可以重排序的。

具体到Java实现中,如果是处理x86的情况,我们会发现在loadload()、storestore()、loadstore()三个操作时只是统一调用了compiler_barrier()来禁止编译器做优化(OrderAccess_linux_x86.hpp):

inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  { fence();            }

inline void OrderAccess::fence() {
   // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
}

CPU方面不需要做单独的处理,而剩下的storeload()操作他也没有调用x86自身指令集中的内存屏障指令(Ifence、sfence...),只调用了lock指令前缀来同样达到内存屏障的效果,有时候还比mfence等指令开销小(注释:always use locked addl since mfence is sometimes expensive 神了,这点性能都不放过)。

如果是ARM的情况,那这四种情况只能分别进行处理了(要分别内嵌不同的汇编ARM指令dmb来进行处理)(OrderAccess_linux_arm.hpp):

inline void OrderAccess::loadload()   { dmb_ld(); }
inline void OrderAccess::loadstore()  { dmb_ld(); }
inline void OrderAccess::storestore() { dmb_st(); }
inline void OrderAccess::storeload()  { dmb_sy(); }

// Load-Load/Store barrier
inline static void dmb_ld() {
#ifdef AARCH64
   if (!os::is_MP()) {
     return;
   }
   __asm__ __volatile__ ("dmb ld" : : : "memory");
#else
   dmb_sy();
#endif
}

inline static void dmb_sy() {
   if (!os::is_MP()) {
     return;
   }
#ifdef AARCH64
   __asm__ __volatile__ ("dmb sy" : : : "memory");
#else
   if (VM_Version::arm_arch() >= 7) {
#ifdef __thumb__
     __asm__ volatile (
     "dmb sy": : : "memory");
#else
     __asm__ volatile (
     ".word 0xF57FF050 | 0xf" : : : "memory");
#endif
   } else {
     intptr_t zero = 0;
     __asm__ volatile (
       "mcr p15, 0, %0, c7, c10, 5"
       : : "r" (zero) : "memory");
   }
#endif
}

inline static void dmb_st() {
   if (!os::is_MP()) {
     return;
   }
#ifdef AARCH64
   __asm__ __volatile__ ("dmb st" : : : "memory");
#else
   if (VM_Version::arm_arch() >= 7) {
#ifdef __thumb__
     __asm__ volatile (
     "dmb st": : : "memory");
#else
     __asm__ volatile (
     ".word 0xF57FF050 | 0xe" : : : "memory");
#endif
   } else {
     intptr_t zero = 0;
     __asm__ volatile (
       "mcr p15, 0, %0, c7, c10, 5"
       : : "r" (zero) : "memory");
   }
#endif
}

这种硬件上不做处理只能开发人员自己插入屏障的行为只能说见仁见智了。

参考资料:《深入解析Java虚拟机HotSpot》

Last modification:June 30th, 2021 at 07:47 am
大家一起分享知识,分享快乐