因为CPU的乱序执行技术虽然可以极大的提高流水线的工作效率,但是导致了实际运行次序和program的次序不一致,如果不做多余的防护措施,在逻辑次序上最后写入内存的数据未必最后写入。 也就是说,如果你期望最后写入一个标记数据表示前面的数据都已经准备好,然后在另外一个核心上依靠判断这个标记来判定一些数据就绪,这个策略并不可靠;
比如以下代码:
char* g_Data = nullptr; //processor 1 code: char* pTemp = new char[1000]; g_Data = pTemp; //processor 2 code: if(g_Data != nullptr) { //do something... }
这样的代码,在核心2上进行判断是不准确的,有可能核心1上的g_Data的值已经不是nullptr了,但是真正的1000个char还没有new出来,也就是说核心1上的g_Data保存着pTemp[0]的地址(标记位先被存到g_Data里面了),但是后面999个char还没真正开辟出来,这样,在核心2上进行访问就会造成程序crash。
也就是说,在一个核心写入内存的数据未必真的最后写入,核与核之间作为一个整体来看的话,不能保证self-consitent。
解决的办法是在标记为被写入前,强迫CPU串行化(也就是在上面demo中g_Data被赋值前强行CPU串行化):
char* g_Data = nullptr; //processor 1 code: char* pTemp = new char[1000]; MemoryBarrier();//add barrier, different platform has different API g_Data = pTemp; //processor 2 code: if(g_Data != nullptr) { //do something... }
这样,在核心1上的g_Data被赋值前pTemp的内存就会是完整开辟出来的,就不会对核心2上的代码在访问g_Data的时候产生crash隐患。
另:当CPU要访问的内存存在于cache的时候,像Interxxx(如InterLockedIncrement等)这样的原子操作命令是不会被发送到总线上的,取而代之会锁住cache。加锁,其实也是会锁住cpu的cache的,如果此时该核心又被让出去其他线程使用,那么这些原有的cache会被清掉,所以大量的锁也有这个副作用。
PS:关于CPU的推测执行技术和乱序执行技术:
推测执行技术:处理器为了提高性能,会去提前猜测接下去需要执行什么动作,然后提前执行,如果发现推测错误,则回滚至正常状态。通常应用的推测执行技术都能达到80%-90%的推测准确率。需要指出的是,推测执行技术是一个大类技术的统称,包括分支预测,预读取,推测性内存访问,缓存缺失的重叠/乱序处理(MESR)等等;
乱序执行技术:当处理器遇到需要发生停顿的时间时(例如需要装在的数据发生了混存确实,需要去高延迟的内存中查找),处理器可以越过这个停顿事件,继续超前执行指令。同样,如果超前执行中发生了错误,也需要回滚至正常状态。
乱序执行的微结构框图:
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步