汇编为什么分段执行总是执行不了_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;所以要加大…

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

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

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脚本在虚拟主机配置文件中增…

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;自愧…

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;当主线程执行…

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

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

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

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

识别操作系统

使用p0f进行操作系统探测 p0f是一款被动探测工具&#xff0c;通过分析网络数据包来判断操作系统类型。目前最新版本为3.06b。同时p0f在网络分析方面功能强大&#xff0c;可以用它来分析NAT、负载均衡、应用代理等。 p0f的命令参数很简单&#xff0c;基本说明如下&#xff1a; l…

常用RGB颜色表

转载于:https://www.cnblogs.com/Itwonderful/p/5550800.html

linux ssh yum升级_Linux 运维必备的 13 款实用工具,拿好了

作者丨Erstickthttp://blog.51cto.com/13740508/2114819本文介绍几款 Linux 运维比较实用的工具&#xff0c;希望对 Linux 运维人员有所帮助。1. 查看进程占用带宽情况 - NethogsNethogs 是一个终端下的网络流量监控工具可以直观的显示每个进程占用的带宽。下载&#xff1a;htt…

iOS应用如何支持IPV6

本文转自 http://www.code4app.com/forum.php?modviewthread&tid8427&highlightipv6 果然是苹果打个哈欠&#xff0c;iOS行业内就得起一次风暴呀。自从5月初Apple明文规定所有开发者在6月1号以后提交新版本需要支持IPV6-Only的网络&#xff0c;大家便开始热火朝天的研…

SQL Server -- SQLserver 存储过程执行错误记录到表

SQLserver 存储过程执行错误记录到表 From: http://blog.csdn.net/leshami/article/details/51333650 对于在执行存储过程中碰到的一些错误&#xff0c;如果未及时捕获或者说传递给前端应用程序来&#xff0c;在这样的情形下&#xff0c;故障的排查显得尤为困难。基于此&…

Windows下C语言连接Oracle数据库

为什么80%的码农都做不了架构师&#xff1f;>>> 最近公司有个项目需要用到Oracle数据库&#xff0c;我负责前期的调研。由于项目要用到C和PHP两种语言&#xff0c;所以先收集这两种语言连接Oracle的方法。PHP使用的是Laravel框架&#xff0c;直接使用了Laravel-OCI…

SU suspecfk命令学习

用suplane生成平面&#xff0c;并查看其FK谱&#xff0c; 水平反射界面经FK变换后&#xff0c;波数为0&#xff0c; 正好处于临界&#xff0c;乃奎斯特频率&#xff0c; 有空间假频&#xff0c; Over&#xff0c;不足之处&#xff0c;欢迎批评指正。 转载于:https://www.cnblog…

dblink查询_分库数据如何查询统计

分库后的计算不能直接使用SQL&#xff1b;异构库 SQL 函数不尽相同&#xff1b;JAVA 硬编码实施难度大&#xff1b;即使借助透明网关访问远程数据库&#xff0c;分库性能优化也是头疼问题。一般常规办法&#xff1a;方法1&#xff1a;java硬编码简单的跨库count运算&#xff0c…

【Python五篇慢慢弹(5)】类的继承案例解析,python相关知识延伸

类的继承案例解析&#xff0c;python相关知识延伸 作者&#xff1a;白宁超 2016年10月10日22:36:57 摘要&#xff1a;继<快速上手学python>一文之后&#xff0c;笔者又将python官方文档认真学习下。官方给出的pythondoc入门资料包含了基本要点。本文是对文档常用核心要点…

领域驱动设计:软件核心复杂性应对之道_人人都可以领域驱动设计(一)

最近几年&#xff0c;领域驱动设计&#xff08;Domain-Driven Design&#xff0c;DDD&#xff09;这个术语越来越多地出现在软件工程师的视野里。对DDD不熟悉的人可能会觉得它是软件领域里的一个新的概念&#xff0c;但是实际上&#xff0c;Eric Evans在十几年前就已经提出了这…