# 乱序执行与内存屏障

现在在使用高级语言编写程序后,都要经过编译汇编或解时器,最后变成机器指令才能在CPU上执行。但是,CPU上执行的指令顺序是否就一定是程序编写时的顺序呢?答案是不一定,为了提升CPU的运行效率在代码编译时CPU运行时都有可能改变指令的执行顺序。都于上层应用开发CPU乱序执行极少会影响计算结果,所以绝大多数情况下用户都不会感受到乱序执行的存在。但对于底层软件开发者,譬如做驱动开发有时就必须考虑乱序执行的问题了,需要考虑做内存屏障来防止乱序执行的发生。

# 1.编译时乱序

这个就是在代码编译时生成的汇编代码中的汇编指令顺序就已经和用户编写的顺序不同了。

// test.cpp
int test_func();
int a, b;
int main()
{
    a = test_func();
    b = 2;
    return 0;
}

使用g++ mem_barrier.cpp -S -o m.o 命令编译得到汇编代码中有:

call    _Z9test_funcv@PLT
movl    %eax, a(%rip)
movl    $2, b(%rip)
movl    $0, %eax
popq    %rbp
.cfi_def_cfa 7, 8

上面的汇编代码中是先将函数的返回值从eax内存器放到a中,再将2赋值给b,这和c++代码中的定义是一致的。再来看:

开启编译二级优化g++ mem_barrier.cpp -O2 -S -o m.o命令编译得到汇编代码中有:

call    _Z9test_funcv@PLT
movl    $2, b(%rip)
movl    %eax, a(%rip)
xorl    %eax, %eax
addq    $8, %rsp

结果会发现是先将2赋值给b,然后才是将函数的返回值赋值给a,这和我们代码中的定义就不一致了,发生了指令乱序。

至于为什么会在这里发生乱序,无外乎优化程序性能和减小可执行文件大小。

要避免编译时导致的乱序也很简单,只需要使用asm语句告诉编译器不要在此优化即可。

int test_func();
int a, b;
int main()
{
    a = test_func();
    asm("":::"memory");
    b = 2;
    return 0;
}

编译:g++ mem_barrier.cpp -O2 -S -o m.o

再看生成的汇编代码会发现没有再发生乱序了。asmc++中提供的函数,告诉编译器在此插入asm提供的汇编指令。

# 运行时乱序

cpu在执行代码的时候也有可能会选择指令的运行顺序。

看这样一个例子:

#include <thread>
#include <iostream>
#include <semaphore.h>

int v1, v2, r1, r2;
sem_t start1, start2, complete;

void func1();
void func2();

int main(int argc, char **argv)
{
    sem_init(&start1, 0, 0);
    sem_init(&start2, 0, 0);
    sem_init(&complete, 0, 0);

    std::thread t1(func1);
    std::thread t2(func2);

    constexpr const int n = 1000000;
    for(int i = 0; i < n; i++) {
        v1 = v2 = 0;
        sem_post(&start1);
        sem_post(&start2);
        sem_wait(&complete);
        sem_wait(&complete);
        if((r1 == 0) && (r2 == 0)) {
            std::cout << "instruction reorder detected " << i << std::endl;
        }
    }
    t1.detach();
    t2.detach();
    return 0;
}

void func1()
{
    while (true)
    {
        sem_wait(&start1);
        v1 = 1;
        // asm("mfence" ::: "memory");
        r1 = v2;
        sem_post(&complete);
    }
}

void func2()
{
    while (true)
    {
        sem_wait(&start2);
        v2 = 1;
        // asm("mfence" ::: "memory");
        r2 = v1;
        sem_post(&complete);
    }
}

上面代码中,有两条线程并行,并使用信号量进行同步,分析上面的代码,程序执行的结果只可能有三种结果:(r1=1, r2=0),(r1=0, r2=1),(r1=1, r2=1)绝对不会出现(r1=0, r2=0)的情况,但是实际执行的情况却是,

# 编译
g++ mem_barrier.cpp -o mb -lpthread
./mb
# instruction reorder detected 940083
# instruction reorder detected 943637
# instruction reorder detected 945191
# instruction reorder detected 945307
# instruction reorder detected 976957
# instruction reorder detected 988545

大量检测到了乱序的发生。要避免上面的情况只需要使用mfence指令即可。放开上面的asm语句,可以发现不管是在单核还是多核cpu上都没有再发生乱序的情况了。

# reference

1.https://www.bilibili.com/video/BV16C4y1e7aP/?spm_id_from=333.1073.top_right_bar_window_history.content.click (opens new window)