系列文章

  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 分钟后,遇到了断言错误:

显然这是由于乱序执行导致的 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

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

  1. java volatile内存屏障_从汇编看Volatile的内存屏障

    Java的Volatile的特征是任何读都能读到最新值,本质上是JVM通过内存屏障来实现的,让我们看看从字节码以及汇编码的角度,来看下是否真是如此? 一 Volatile与内存屏障 为了实现volat ...

  2. 浅谈代码的执行效率(4):汇编优化

    终于谈到这个话题了,首先声明我不是汇编优化的高手,甚至于我知道的所有关于汇编优化的内容,仅仅来自于学校的课程.书本及当年做过的一些简单练习.换句话说,我了解的东西只能算是一些原则,甚至也有一些&quo ...

  3. CPU指令的流水线执行

    指令集是CPU体系架构的重要组成部分.C语言的语法是对解决现实问题的运算和流程的方法的高度概况和抽象,其主要为算术.逻辑运算和分支控制,而指令集就是对这些抽象的具体支持,汇编只不过是为了让开发人员更好 ...

  4. 命令执行——命令执行漏洞概述(一)

    普及内容 了解命令执行定义 了解命令执行条件 掌握命令执行成因 了解命令执行危害 掌握命令执行实例 掌握管道符号和通用命令符 了解命令执行常见场景 基础概念 命令执行定义 基本定义 命令执行漏洞是指攻 ...

  5. 计算机指令要素,【计算机系统】CPU指令执行流程与指令流水线原理

    [计算机系统]CPU指令执行流程与指令流水线原理 一.指令执行流程 冯诺依曼架构CPU指令执行的五个阶段: 阶段 涉及的功能部件 IF 指令寄存器IR.程序计数器PC ID 指令译码器ID EXE C ...

  6. 腾讯被深圳南山法院强制执行:执行标的25元;B站就招聘争议致歉;华为云回应是否将独立运作|极客头条...

    「极客头条」-- 技术人员的新闻圈! CSDN 的读者朋友们早上好哇,「极客头条」来啦,快来看今天都有哪些值得我们技术人关注的重要新闻吧. 整理 | 梦依丹 出品 | CSDN(ID:CSDNnews ...

  7. centos上自动执行脚本执行php文件

    centos上自动执行脚本执行php文件 1 先编写执行PHP文件的脚本 vi php.sh #!/bin/sh /usr/bin/php /etc/1.php 2把php.sh添加到自动执行任务中 ...

  8. linux执行可执行命令程序ls,linux运行可执行程序命令

    linux 命令行如何运行程序 我用的是Ubuntu,安装了一个分子模拟软件,但是不知道如何运行程序,比cd到目录下,然后ls -l tleap,如果有x权限,直接./tleap,如果没有x,就先执行 ...

  9. 【计算机系统】CPU指令执行流程与指令流水线原理

    [计算机系统]CPU指令执行流程与指令流水线原理 一.指令执行流程 冯诺依曼架构CPU指令执行的五个阶段: 阶段 涉及的功能部件 IF 指令寄存器IR.程序计数器PC ID 指令译码器ID EXE C ...

最新文章

  1. Unknown host 'services.gradle.org' 解决方法
  2. 基本数据类型之间的运算
  3. Buildroot stress-ng Linux系统压力测试
  4. 我说省略号然后点点点点点点
  5. 公务员计算机基础知识笔记,公务员计算机基础知识【精选】.doc
  6. c语言糖果游戏,幼儿园小班糖果游戏教案
  7. Linux上利用NFS实现远程挂载
  8. Linux JAVA JDK JRE 环境变量安装与配置
  9. python shell清屏指令_python shell怎么清屏
  10. office 2019 visio 2016安装
  11. 简单OCX控件的开发
  12. 海康威视Linux下SDK开发(Ubuntu16.04 QT5.10)
  13. win10关机后cpu风扇还在转_win10系统关机后风扇还转的解决方法
  14. 5.6 DMA 方式
  15. 增量式编码器 绝对值编码器
  16. ker矩阵是什么意思_“拨开迷雾”,如何判定矩阵相似?
  17. 这5款堪称神器的插件,能让你的效率提升3-4倍!还不知有点遗憾
  18. 航班信息管理系统(JDBC)
  19. 软工实践第三次作业-结对项目1
  20. CCM-SLAM跑自己的USB摄像头

热门文章

  1. python-flask-1
  2. PHP超全局变量$_SERVER
  3. Mysql 替换字段的一部分内容
  4. Quiver快速入门
  5. js call(),apply(),对象冒充,改变变量作用域
  6. 零元学Expression Blend 4 - Chapter 4元件重复运用的观念
  7. 关于清空object对象里的属性的两种方法
  8. ForkJoinPool 学习示例
  9. mingw + msys 上编译 ffmpeg
  10. 本地浏览器缓存sessionStorage(临时存储) localStorage(长期存储)的使用