CPU乱序发射与内存屏障

CPU乱序发射与内存屏障


在提出问题之前,先看一段简单的代码。

#include <stdio.h>
#include <pthread.h>
int x = 0, y = 0;
void* mythread1(void*)
{

    while (y == 0)
    {
    }
    printf("x=%d\n",x);
}

void* mythread2(void*)
{
    x = 1;
    y = 1;
}

int main()
{
    int ret = 0;
    pthread_t id1, id2;
    ret = pthread_create(&id1, NULL, mythread1, NULL);
    if (ret)
    {
        printf("create pthread error!\n")return -1;
    }
    ret = pthread_create(&id2, NULL, mythread2, NULL);
    if (ret)
    {
        printf("create pthread error!\n");
        return -1;
    }
    pthread_join(id1, NULL);
    pthread_join(id2, NULL);
    return 0;
}

阅读代码可知程序的输出为x=1,因为在对y赋值之前已对x赋值,所以当线程1跳出循环时x=1是不奇怪的。实际运行这段代码时,程序的输出也确实为1,即使测试了10000次,也没出现其他结果。但能出现正确结果真的是因为代码中赋值顺序吗?
实际上并不是,程序能输出正确结果仅仅是因为线程分配到的时间片过长&&线程需要执行的时间过短
对于线程2而言,执行时间大约等于访问两次内存的时间,约等于50ns,而线程分配的时间片为ms级别。所以程序并没有出现异常的结果。但是相信程序的执行顺序与代码的编写顺序一致是一种危险的观点。实际上如果两个语句之间不存在数据相关,由于编译器的乱序优化和CPU的乱序执行,语句的执行顺序是不确定。CPU乱序执行只保证局部的正确性(数据相关的语句一定按原顺序执行)

a = x + y;	//语句1
c = a + b;	//语句2
// 语句2一定在语句1之前执行
m = p + q;	//语句3
k = i + j;	//语句4
//语句3不一定在语句4之前执行

设想一个极端情况,在执行语句3时,访问变量p时cache未命中且发生缺页,此时语句4大概率在语句3之前执行完毕。为使语句执行顺序与代码顺序保持一致,有两种解决思路。
一:增加冗余的数据相关

m = p + q;	
k = i + j + m;	
k -= m;

这种方法是我瞎想的,不知道会不会被编译器优化掉,(__) 嘻嘻……
二:增加内存屏障
Barrier函数可以在代码中设置屏障,这个屏障可以阻挡编译器的优化,也可以阻挡处理器的优化。

对于编译器来说,设置任何一个屏障都可以保证:

编译器的乱序优化不会跨越屏障,即屏障前后的代码不会乱序;

在屏障后所有对变量或者地址的操作,都会重新从内存中取值(相当于刷新寄存器中的变量副本)。

而对于处理器来说,根据不同的屏障有不同的表现(以下仅仅列举3种最简单的屏障):

读屏障rmb()
处理器对读屏障前后的取数指令(LOAD)能保证有序,但是不一定能保证其他算术指令或者是写指令的有序。对于读指令的执行完成时间也不能保证,即它不能保证在屏障之前的读指令一定都执行完成,只能保证屏障之前的读指令一定能在屏障之后的读指令之前完成。

写屏障wmb()
处理器对屏障前后的写指令(STORE)能保证有序,但是不一定能保证其他算术指令或者是读指令的有序。对于写指令的执行完成时间也不能保证,即它不能保证在屏障之前的写指令一定都执行完成,只能保证屏障之前的写指令一定能在屏障之后的写指令之前完成。

通用内存屏障mb()
处理器保障只有屏障之前的访存操作(包括读写)都完成以后才会执行屏障之后的访存操作。即可以保障读写之间的有序(但是同样无法保证指令完成的时 间)。这种屏障对处理器的执行单元效率产生的负面影响要比单纯用读屏障或者写屏障来的大。比如对于PowerPC来说这种通用屏障通常是使用sync指令实现的,在这种情况下处理器会丢弃所有预取的指令并清空流水线。所以频繁使用内存屏障会降低处理器执行单元的效率。

对于驱动开发者来说,一些对设备寄存器的操作,通常是必须保证有序的。在绝大部分情况下,一般都是写操作。对于有序的写操作,必须设置写屏障(wmb)。

当程序出现随机性的问题时,在随机的部分找原因

参考博客:CPU乱序执行

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 内存屏障是一种CPU指令,用于保证内存访问的有序性。它可以分为编译屏障和CPU执行乱序屏障。 编译屏障是在代码编译时插入的指令,用于告诉编译器在这个位置需要生成一条内存屏障指令。它可以保证在编译后生成的机器码中,这个位置的内存访问会被正确地序列化。 CPU执行乱序屏障是在CPU级别执行的指令,用于告诉CPU在这个位置需要保证内存访问的顺序性。在现代CPU中,由于硬件优化和多核并发等原因,CPU会对指令进行乱序执行,而乱序屏障可以保证在这个位置之前的所有内存访问都已经完成,从而避免了乱序执行带来的问题。 总的来说,内存屏障是一种非常重要的机制,可以保证多线程程序中的内存访问顺序性,避免出现数据竞争等问题。 ### 回答2: 内存屏障是指用于控制内存访问顺序的指令或者指令序列。根据其功能和作用方式的不同, 内存屏障可以分为编译屏障和CPU执行乱序屏障。 编译屏障是在编译器层面进行优化控制的屏障。编译器在进行程序优化时,可能会对代码进行重排,以提高执行效率。然而,有些代码的执行顺序是有严格要求的,此时就需要使用编译屏障来保证指令的顺序。编译屏障可以用于控制指令的插入位置,确保指令的执行顺序符合预期。编译屏障通常是通过特殊的指令或者关键字来实现的,例如在C语言中的"__asm__ __volatile__"关键字。 CPU执行乱序屏障是在CPU层面进行指令乱序执行时的控制屏障。现代处理器在执行指令时会使用乱序执行技术,乱序执行可以提高指令级并行度从而提高处理器的性能。然而,在某些情况下,由于指令之间存在依赖关系,需要保证指令的执行顺序,此时就需要使用乱序屏障。乱序屏障可以阻止指令乱序执行的同时也确保了数据的一致性。乱序屏障一般是通过特殊的指令来实现的,例如在x86架构中的"mfence"指令。 总的来说,编译屏障主要是用于控制编译器对代码的优化,保证指令的执行顺序;而CPU执行乱序屏障主要是用于控制CPU对指令的乱序执行,保证指令的执行顺序。两者在不同的层面上起到了优化和控制的作用,都是为了保证程序的正确执行和数据的一致性。 ### 回答3: 内存屏障是一种在并发编程中用来确保内存操作有序性的机制。内存屏障分为编译屏障和CPU执行乱序屏障两种类型。 编译屏障是在编译器层面上插入的指令,用于告诉编译器在指定位置之前的所有内存访问操作必须完成,并且在指定位置之后的所有内存访问操作必须等待。编译屏障可以通过优化和重排指令来提高程序执行效率,但是在多线程环境下可能会导致并发访问数据的顺序问题。因此,通过插入编译屏障来限制指令重排,确保内存操作按照预期的顺序进行。 CPU执行乱序屏障是在指令执行层面上插入的机制,用于告诉CPU在指定位置之前的所有内存访问操作必须完成,并且在指定位置之后的所有内存访问操作必须等待。CPU执行乱序屏障主要解决CPU乱序执行指令的问题,确保内存操作的顺序性。在现代处理器中,由于乱序执行可以提高指令执行效率,但可能导致结果和预期不符。因此,通过插入CPU执行乱序屏障来确保内存操作的有序性。 总结起来,编译屏障和CPU执行乱序屏障是为了解决并发编程中的内存操作顺序问题而设计的机制。编译屏障在编译器层面上限制指令重排,确保内存操作有序进行;CPU执行乱序屏障在指令执行层面上限制指令乱序执行,保证内存操作的有序性。这两种屏障在不同的层面上发挥作用,共同保证程序的正确执行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

最佳损友1020

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值