美文网首页
浅谈编译期间的内存指令次顺问题

浅谈编译期间的内存指令次顺问题

作者: Teech | 来源:发表于2019-07-27 17:27 被阅读0次

在cpu运行指令之前,内存操作指令都可能在遵循一定的规则下被重排,编译期间以及cpu执行期间,主要目的都是为了使得代码运行更快点。cpu执行指令看似顺序执行的,其实本质内部是并行的,只是达到的结果和顺序执行的没有区别而已。
编译器和cpu产商在内存指令次顺问题上都遵循着一条基本的规则“在单线程程序上不改变其运算的正确结果”
因为这条规则所以当编写单线程程序时,我们不需要注意内存一致性的问题。当编写多线程程序时,由于mutexes,semaphores 以及events都被设计了阻止内存指令重排在他们调用区域内。只有当无锁技术lock-free采用,内存被多个线程共享时而且相互之间没有任何互斥操作,这个时候内存重排才会被关注到。
在多核的平台上编写多线程程序且不用关心内存指令重排的问题是可以实现的,比如通过c++11的atmoics,可能稍微花费一点点额外的消耗。下面重点关注下编译器的内存指令重排。
查看如下代码

//t.c
int A,B;
void f(){
    A = B+1;
    B= 0;
}

使用如下指令去编译 gcc -S t.c 得到的汇编指令如下(略去无关紧要部分),可以观察到并没有发生指令重排。

    movl    B(%rip), %eax
    addl    $1, %eax
    movl    %eax, A(%rip)
    movl    $0, B(%rip)

当使用O2的优化去编译时 gcc -O2 -S t.c,得到汇编指令如下,观察到指令发生重排了。B=0;被排到前面去了。

    movl    B(%rip), %eax
    movl    $0, B(%rip)
    addl    $1, %eax
    movl    %eax, A(%rip)

我们观察编译O2优化后,把写入B提前了。但是内存排序的基本规则并没有打破,单线程程序不可能能发现这两者有啥区别。
当编写lock-free程序时,编译器指令重排就会照成问题了。
参考下面的例子,IsPublished标记着值有没有被写入,如果编译器把IsPublished = 1;提前的话,就算在单处理器的多线程程序中,也会有问题。一个线程执行了IsPublished=1,但是Value并没有被更新。其他线程读取IsPublished后就误认为Value已经被更新了,但实际上并没有。

int Value;
int IsPublished = 0;
void sendValue(int x)
{
    Value = x;
    IsPublished = 1;
}

显式编译Barriers

如下代码列出了编译阶段阻止内存指令重排的例子

int A, B;
void foo()
{
    A = B + 1;
    asm volatile("" ::: "memory");
    B = 0;
}

使用O2优化编译: gcc -O2 -S t.c,得到的汇编指令如下,观察到并没有被编译器重排指令了,指令顺序和c源码一致了。这个也只能保证在单cpu的是线程安全的。

    movl    B(%rip), %eax
    addl    $1, %eax
    movl    %eax, A(%rip)
    movl    $0, B(%rip)

emm,现在我们的目标假设是让这个例子可以在单核处理器中运行的没问题,代码如下,不仅在sendValue中加入编译屏障 而且在tryRecvValue中也加入编译屏障。

#define COMPILER_BARRIER() asm volatile("" ::: "memory")

int Value;
int IsPublished = 0;

void sendValue(int x){
    Value = x;
    COMPILER_BARRIER();
    IsPublished = 1;
}

int tryRecvValue(){
    if(IsPublished){
        COMPILER_BARRIER();
        return Value;
    }
    return -1;
}

上面例子中tryRecvValue我觉得没必要加入编译屏障,可以对比汇编代码得知(都是使用O2优化),仅仅去掉je和mov,用cmovne替代了,这么优化的目的主要减少分支和跳转指令,避免分支预测错误带来的惩罚。

//没加之前
    movl    IsPublished(%rip), %eax
    testl   %eax, %eax
    movl    $-1, %eax
    cmovne  Value(%rip), %eax
//加入之后
    movl    IsPublished(%rip), %eax
    testl   %eax, %eax
    je  .L4
    movl    Value(%rip), %eax

要想既可以运行在单核机器上,还可以运行在多核的机器上,仅仅有编译屏障是不够的,还需要有cpu屏障指令。linux提供宏定义比如smb_rmb。

隐式编译Barriers

cpu fence指令同样也作为编译barriers来使用。下列我们定义cpu fence指令

#define  RELEASE_FENCE asm volatile("lwsync" ::: "memory")

当我们把RELEASE_FENCE 加到代码中,不仅会阻止处理器的内存指令重排,当然了编译阶段也会阻止指令重排。这个就是多核平台下多线程安全的代码了。

void sendValue(int x){
    Value = x;
    RELEASE_FENCE ();
    IsPublished = 1;
}

在C++11中,提供一些原子库,每个non-relaxed原子操作都被同样被当成内存屏障

int Value;
std::atomic<int> IsPublished(0);
void SendValue(int x){
  Value = x;
  IsPublished.store(1,std::memory_order_release);
}

就如我们想象的一样,每个含有内存屏障的函数,自身都会被当做是一个内存屏障,即使这个函数是个内联函数(可能早期的VC++编译器并不是这样的)

void doSomeStuff(Foo* foo)
{
    foo->bar = 5;
    sendValue(123);       // prevents reordering of neighboring assignments
    foo->bar2 = foo->bar;
}

实际上大多数函数调用都被当成内存屏障,不论函数本身是否包含内存屏障,内联除外。外部调用的函数更会被当做内存屏障了。因为编译阶段无法对函数产生的影响做任何假设。
这个也容易理解的,编译器无法假设sendValue是否会会修改foo->bar的值,如果修改了,如果重排了指令,这样违背了“在单线程程序上不改变其运算的正确结果”这条最基本的规则了。
编译器会有一些优化的准则以及禁止优化的准则的。volatile关键字修饰后,虽然可以阻止编译器重排指令,但是不能阻止cpu重排指令,所以volatile在编写线程安全代码时,并没啥作用。

相关文章

  • 浅谈编译期间的内存指令次顺问题

    在cpu运行指令之前,内存操作指令都可能在遵循一定的规则下被重排,编译期间以及cpu执行期间,主要目的都是为了使得...

  • 浅谈运行期间cpu指令重排

    上篇谈到了编译器会进行内存操作指令的重排,这篇来谈谈运行期间cpu进行内存操作指令重排。当且仅当lock-free...

  • iOS中synthesize与dynamic

    @synthesize 指令:告诉编译器在编译期间产生 getter/setter 方法。 @dynamic 指令...

  • leveldb源码学习--skiplist

    Skiplist原理 内存屏障 内存屏障,也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器...

  • 浅谈编译过程

    浅谈编译过程浅谈编译过程

  • 多线程安全问题:可见性、原子性、有序性

    引言 CPU缓存与内存产生的一致性问题 CPU时间片切换产生的原子性问题 CPU指令编译优化产生的有序性问题 并发...

  • 原子操作、内存屏障、锁

    技术缘由 多核多线程下同时操作相同内存地址产生的竞态问题 CPU结构 乱序执行技术 1.编译器指令重排现代编译器为...

  • voilate 原理

    voilate 变量编译成汇编代码时会在之后增加一行lock 前缀的指令,这个lock指令会将数据写入内存,写入后...

  • 2017年10月19日学校总结

    今天学习了预处理指令,预处理指令包括宏定义,条件编译,文件包含 宏定义,不占用内存空间。 #define p 3....

  • JSP的7个动作指令

    1、动作指令与编译指令的区别:编译指令是通知Servlet引擎的处理消息,而动作指令只是运行时的动作。编译指令在将...

网友评论

      本文标题:浅谈编译期间的内存指令次顺问题

      本文链接:https://www.haomeiwen.com/subject/njosrctx.html