汇编为什么分段执行总是执行不了_iOS汇编教程(六)CPU 指令重排与内存屏障...

系列文章

  1. iOS 汇编入门教程(一)ARM64 汇编基础

  2. iOS 汇编入门教程(二)在 Xcode 工程中嵌入汇编代码

  3. iOS 汇编入门教程(三)汇编中的 Section 与数据存取

  4. iOS 汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现

  5. iOS 汇编教程(五)Objc Block 的内存布局和汇编表示

前言

具有 ARM 体系结构的机器拥有相对较弱的内存模型,这类 CPU 在读写指令重排序方面具有相当大的自由度,为了保证特定的执行顺序来获得确定结果,开发者需要在代码中插入合适的内存屏障,以防止指令重排序影响代码逻辑[1]。

本文会介绍 CPU 指令重排的意义和副作用,并通过一个实验验证指令重排对代码逻辑的影响,随后介绍基于内存屏障的解决方案,以及在 iOS 开发中有关指令重排的注意事项。

指令重排

简介

以 ARM 为体系结构的 CPU 在执行指令时,在遇到写操作时,如果未获得缓存段的独占权限,需要基于缓存一致性协议与其他核协商,等待直到获得独占权限时才能完成这条指令的执行;再或者在执行乘法指令时遇到乘法器繁忙的情况,也需要等待。在这些情况下,为了提升程序的执行速度,CPU 会优先执行一些没有前序依赖的指令。

一个例子

看下面一段简单的程序:

; void acc(int *counter, int *flag);_acc:ldr x8, [x0]add x8, x8, #1str x8, [x0]ldr x9, [x1]mov x9, #1str x9, [x1]ret

这段代码将 counter 的值 +1,并将 flag 置为 1,按照正常的代码逻辑,CPU 先从内存中读取 counter (x0) 的值累加后回写,随后读取 flag (x1) 的值置位后回写。

但是如果 x0 所在的内存未命中缓存,会带来缓存载入的等待,再或者回写时无法获取到缓存段的独占权,为了保证多核的缓存一致性,也需要等待;此时如果 x1 对应的内存有缓存段,则可以优先执行 ldr x9, [x1],同时由于对 x9 的操作和对 x1 所在内存的操作不依赖于对 x8 和 x0 所在内存的操作,后续指令也可以优先执行,因此 CPU 乱序执行的顺序可能变成如下这样:

ldr x9, [x1]mov x9, #1str x9, [x1]ldr x8, [x0]add x8, x8, #1str x8, [x0]

甚至如果写操作都需要等待,还可能将写操作都滞后:

ldr x9, [x1]mov x9, #1ldr x8, [x0]add x8, x8, #1str x9, [x1]str x8, [x0]

再或者如果加法器繁忙,又会带来全新的执行顺序,当然这一切都要建立在被重新排序的指令之间不能相互他们依赖执行的结果。

副作用

指令重排大幅度提升了 CPU 的执行速度,但凡事都有两面性,虽然在 CPU 层面重排的指令能保证运算的正确性,但在逻辑层面却可能带来错误。比如常见的自旋锁场景,我们可能设置一个 bool 类型的 flag 来自旋等待某异步任务的完成,在这种情况下,一般是在任务结束时对 flag 置位,如果置位 flag 的语句被重排到异步任务语句的中间,将会带来逻辑错误。下面我们会通过一个实验来直观展示指令重排带来的副作用。

一个实验

在下面的代码中我们设置了两个线程,一个执行运算,并在运算结束后置位 flag,另一个线程自旋等待 flag 置位后读取结果。

我们首先定义一个保存运算结果的结构体。

typedef struct FlagsCalculate {    int a;    int b;    int c;    int d;    int e;    int f;    int g;} FlagsCalculate;

为了更快的复现重排带来的错误,我们使用了多个 flag 位,存储在结构体的 e, f, g 三个成员变量中,同时 a, b, c, d 作为运算结果的存储变量:

int getCalculated(FlagsCalculate *ctx) {    while (ctx->e == 0 || ctx->f == 0 || ctx->g == 0);    return ctx->a + ctx->b + ctx->c + ctx->d;}

为了更快的触发未命中缓存,我们使用了多个全局变量;为了模拟加法器和乘法器繁忙,我们采用了密集的运算:

int mulA = 15;int mulB = 35;int divC = 2;int addD = 20;void calculate(FlagsCalculate *ctx) {    ctx->a = (20 * mulA - mulB) / divC;    ctx->b = 30 + addD;    for (NSInteger i = 0; i < 10000; i++) {        ctx->a += i * mulA - mulB;        ctx->a *= divC;        ctx->b += i * mulB / mulA - mulB;        ctx->b /= divC;    }    ctx->c = mulA + mulB * divC + 120;    ctx->d = addD + mulA + mulB + 5;    ctx->e = 1;    ctx->f = 1;    ctx->g = 1;}

接下来我们将他们封装在 pthread 线程的执行函数内:

void* getValueThread(void *arg) {    pthread_setname_np("getValueThread");    FlagsCalculate *ctx = (FlagsCalculate *)arg;    int val = getCalculated(ctx);    assert(val == -276387);    return NULL;}void* calValueThread(void *arg) {    pthread_setname_np("calValueThread");    FlagsCalculate *ctx = (FlagsCalculate *)arg;    calculate(ctx);    return NULL;}void newTest() {    FlagsCalculate *ctx = (FlagsCalculate *)calloc(1, sizeof(struct FlagsCalculate));    pthread_t get_t, cal_t;    pthread_create(&get_t, NULL, &getValueThread, (void *)ctx);    pthread_create(&cal_t, NULL, &calValueThread, (void *)ctx);    pthread_detach(get_t);    pthread_detach(cal_t);}

每次调用 newTest 即开始一轮新的实验,在 flag 置位未被乱序执行的情况下,最终的运算结果是 -276387,通过短时间内不断并发执行实验,观察是否遇到断言即可判断是否由重排引发了逻辑异常:

while (YES) {    newTest();}

笔者在一个 iOS Empty Project 中添加上述代码,并将其运行在一台 iPhone XS Max 上,约 10 分钟后,遇到了断言错误:4c4dd05e81dd3d5ac28444315cca1d38.png

显然这是由于乱序执行导致的 flag 全部被提前置位,从而导致异步线程获取到的执行结果错误,通过实验我们验证了上面的理论。

答疑解惑

看到这里你可能惊出一身冷汗,开始回忆起自己职业生涯中写过的类似逻辑,也许线上有很多正在运行,但从来没出过问题,这又是为什么呢?

在 iOS 开发中,我们常使用 GCD 作为多线程开发的框架,这类 High Level 的多线程模型本身已经提供好了天然的内存屏障来保证指令的执行顺序,因此可以大胆的去写上述逻辑而不用在意指令重排,这也是我们使用 pthread 来进行上述实验的原因。

到这里你也应该意识到,如果采用 Low Level 的多线程模型来进行开发时,一定要注意指令重排带来的副作用,下面我们将介绍如何通过内存屏障来避免指令重排对逻辑的影响。

内存屏障

简介

内存屏障是一条指令,它能够明确地保证屏障之前的所有内存操作均已完成(可见)后,才执行屏障后的操作,但是它不会影响其他指令(非内存操作指令)的执行顺序[3]。

因此我们只要在 flag 置位前放置内存屏障,即可保证运算结果全部写入内存后才置位 flag,进而也就保证了逻辑的正确性。

放置内存屏障

我们可以通过内联汇编的形式插入一个内存屏障:

void calculate(FlagsCalculate *ctx) {    ctx->a = (20 * mulA - mulB) / divC;    ctx->b = 30 + addD;    for (NSInteger i = 0; i < 10000; i++) {        ctx->a += i * mulA - mulB;        ctx->a *= divC;        ctx->b += i * mulB / mulA - mulB;        ctx->b /= divC;    }    ctx->c = mulA + mulB * divC + 120;    ctx->d = addD + mulA + mulB + 5;    __asm__ __volatile__("dmb sy");    ctx->e = 1;    ctx->f = 1;    ctx->g = 1;}

随后继续刚才的试验可以发现,断言不会再触发异常,内存屏障限制了 CPU 乱序执行对正常逻辑的影响。

volatile 与内存屏障

我们常常听说 volatile 是一个内存屏障,那么它的屏障作用是否与上述 DMB 指令一致呢,我们可以试着用 volatile 修饰 3 个 flag,再做一次实验:

typedef struct FlagsCalculate {    int a;    int b;    int c;    int d;    volatile int e;    volatile int f;    volatile int g;} FlagsCalculate;

结果最后触发了断言异常,这是为何呢?因为 volatile 在 C 环境下仅仅是编译层面的内存屏障,仅能保证编译器不优化和重排被 volatile 修饰的内容,但是在 Java 环境下 volatile 具有 CPU 层面的内存屏障作用[4]。不同环境表现不同,这也是 volatile 让我们如此费解的原因。

在 C 环境下,volatile 常常用来保证内联汇编不被编译优化和改变位置,例如我们通过内联汇编放置一个编译层面的内存屏障时,通过 __volatile__ 修饰汇编代码块来保证内存屏障的位置不被编译器改变:

__asm__ __volatile__("" ::: "memory");

总结

到这里,相信你对指令重排和内存屏障有了更加清晰的认识,同时对 volatile 的作用也更加明确了,希望本文能对大家有所帮助。

参考资料

[1]

缓存一致性(Cache Coherency)入门: https://www.infoq.cn/article/cache-coherency-primer

[2]

CPU Reordering – What is actually being reordered?: https://mortoray.com/2010/11/18/cpu-reordering-what-is-actually-being-reordered/

[3]

ARM Information Center - DMB, DSB, and ISB: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0489c/CIHGHHIE.html

[4]

volatile 与内存屏障总结: https://zhuanlan.zhihu.com/p/43526907

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/457857.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

GD32 使用stm32 固件库

1、 系统 1) 晶振起振区别 描述&#xff1a;启动时间&#xff0c;GD32 与STM32 启动时间都是2ms&#xff0c;实际上GD 的执行效率快&#xff0c;所以ST 的HSE_STARTUP_TIMEOUT ((uint16_t)0x0500)是2ms&#xff0c;但是这个宏定义值在GD 上时间就更加短了&#xff0c;所以要加大…

干将莫邪

干将莫邪也为凡铁铸成&#xff0c;只是善加锻造、融入心神&#xff0c;而成上古神兵。宝剑从来都是双刃&#xff0c;正邪之道&#xff0c;存乎一心。

js反混淆还原工具_SATURN反混淆框架

本文为看雪论坛精华文章看雪论坛作者ID&#xff1a;梦野间摘要&#xff1a;近几年&#xff0c;软件的混淆强度一直在不断提升。基于编译器的混淆已经成为业界事实上的标准&#xff0c;最近的一些论文也表明软件的保护方式使用的是编译器级别的混淆。在这篇文章中&#xff0c;我…

android 弹起键盘把ui顶上去的解决办法

键盘输入框上面的ui布局必须为Relative相对布局。然后设置 <activityandroid:name".activity.HomeActivity"Android:windowSoftInputMode"adjustPan|stateHidden"></activity>转载于:https://www.cnblogs.com/zhaoleigege/p/5925831.html

python 多线程并发_寻找python大神!!!python如何多线程并发?

不是大神。尝试回答一下。 首先解释下什么叫做线程&#xff0c;什么叫做进程&#xff0c;在解释这两个概念前&#xff0c;我们还需要明白什么叫做GIL全局解释器锁。GIL 全局解释器锁&#xff1a; GIL(全局解释器锁&#xff0c;GIL 只有cpython有)&#xff1a;在同一个时刻&…

Nginx/Apache发大招

导读网站程序的上传目录通常是不需要PHP执行解释权限&#xff0c;通过限制目录的PHP执行权限可以提网站的安全性&#xff0c;减少被攻击的机率。下面和大家一起分享下如何在Apache和Nginx禁止上传目录里PHP的执行权限。 Apache下禁止指定目录运行PHP脚本在虚拟主机配置文件中增…

第二轮冲刺-Runner站立会议08

今天完成的内容&#xff1a;简单的做了一下主界面的美化和日历界面的美化 遇到的问题&#xff1a;美化按钮还不能自己自定义按钮 如何解决&#xff1a;暂无思路 明天将要进行的内容&#xff1a;调试bug 转载于:https://www.cnblogs.com/Againzg/p/5544301.html

STM32串口通信中使用printf发送数据配置方法 开发环境 Keil

STM32串口通信中使用printf发送数据配置方法(开发环境 Keil RVMDK) 已有 12456 次阅读2011-6-29 23:29 | 在STM32串口通信程序中使用printf发送数据&#xff0c;非常的方便。可在刚开始使用的时候总是遇到问题&#xff0c;常见的是硬件访真时无法进入main主函数&#xff0c;其实…

dmp文件查看表空间_innoDb文件

一&#xff0e;文件总体概述InnoDb文件主要有以下文件1. 参数文件&#xff1a;启动需要的各种参数作2. 日志文件&#xff1a;记录mysql实例某种条件做出的响应而写入的文件&#xff0c;如错误日志、二进制日志、慢查询日志、查询日志等3. Socket文件&#xff1a;连接需要的文件…

论文笔记之:Deep Attention Recurrent Q-Network

Deep Attention Recurrent Q-Network 5vision groups 摘要&#xff1a;本文将 DQN 引入了 Attention 机制&#xff0c;使得学习更具有方向性和指导性。&#xff08;前段时间做一个工作打算就这么干&#xff0c;谁想到&#xff0c;这么快就被这几个孩子给实现了&#xff0c;自愧…

Codeforces Round #354 (Div. 2)

贪心 A Nicholas and Permutation #include <bits/stdc.h>typedef long long ll; const int N 1e5 5; int a[105]; int pos[105];int main() {int n;scanf ("%d", &n);for (int i1; i<n; i) {scanf ("%d", ai);pos[a[i]] i;}int ans abs …

linux c程序中内核态与用户态内存存储问题

Unix/Linux的体系架构 如上图所示&#xff0c;从宏观上来看&#xff0c;Linux操作系统的体系架构分为用户态和内核态&#xff08;或者用户空间和内核&#xff09;。内核从本质上看是一种软件——控制计算机的硬件资源&#xff0c;并提供上层应用程序运行的环境。用户态即上层应…

线程自动退出_C++基础 多线程笔记(一)

join & detachjoin和detach为最基本的用法&#xff0c;join可以使主线程&#xff08;main函数&#xff09;等待子线程&#xff08;自定义的function_1函数&#xff09;完成后再退出程序&#xff0c;而detach可以使子线程与主线程毫无关联的独立运行&#xff0c;当主线程执行…

WEB在线预览PDF

这是我在博客园发表的第一篇文章。以后会陆续把在线预览其他格式文档的解决方案发表出来。 解决思路&#xff1a;把pdf转换成html显示。 在线预览pdf我暂时了解3种解决方案&#xff0c;欢迎大家补充。 方案一&#xff1a; 利用pdf2html软件将PDF转换成HTML。 用法: PDF2HTML [选…

[算法]判断一个数是不是2的N次方

如果一个数是2^n&#xff0c;说明这个二进制里面只有一个1。除了1. a (10000)b a-1 (01111)b a&(a-1) 0。 如果一个数不是2^n&#xff0c; 说明它的二进制里含有多一个1。 a (1xxx100)b a-1(1xxx011)b 那么 a&(a-1)就是 (1xxx000)b&#xff0c; 而不会为0。 所以可…

VMware Ubuntu 全屏问题解决

在终端中输入&#xff1a; sudo apt install open-vm* 回车 自动解决

数组拼接时中间怎么加入空格_【题解二维数组】1123:图像相似度

1123&#xff1a;图像相似度时间限制: 1000 ms 内存限制: 65536 KB【题目描述】给出两幅相同大小的黑白图像(用0-1矩阵)表示&#xff0c;求它们的相似度。说明&#xff1a;若两幅图像在相同位置上的像素点颜色相同&#xff0c;则称它们在该位置具有相同的像素点。两幅图像的…

(旧)子数涵数·C语言——条件语句

首先&#xff0c;我们讲一下理论知识&#xff0c;在编程中有三种结构&#xff0c;分别是顺序结构、条件结构、循环结构&#xff0c;如果用流程图来表示的话就是&#xff1a; 那么在C语言中&#xff0c;如何灵活运用这三种结构呢&#xff1f;这就需要用到控制语句了。 而条件语句…

apache.commons.lang.StringUtils 使用心得

apache.commons.lang.StringUtils 使用心得 转载于:https://www.cnblogs.com/qinglizlp/p/5549687.html

python哪个版本支持xp_windows支持哪个版本的python

Windows操作系统支持Python的Python2版本和Python3版本&#xff0c;下载安装时要根据windows的操作系统来选择对应的Python安装包&#xff0c;否则将不能安装成功。 Python是跨平台的&#xff0c;免费开源的一门计算机编程语言。是一种面向对象的动态类型语言&#xff0c;最初被…