1. 写在前面

现在出去面试,启动优化是绕不开的,到底我们的 APP 该如何去进行优化呢 ?在优化之前我们必须要先了解 LLVM,那什么是 LLVM呢?

在介绍LLVM之前,先来认识一下解释型语言编译型语言

我们编写的源代码是偏向于我们人类直接的语言,我们非常轻松的就理解了,但是对于计算机硬件(CPU)而言,简直就是个天书,计算机是无法直接运行的。计算机只能识别某些特定的二进制指令,所以我们的代码在程序真正运行之前必须将源代码转换成二进制指令。源代码转换成二进制指令,不同的编程语言有不同的规定。

解释型语言

有的编程语言可以一边执行一边转换,不会生成可执行文件再去执行,这种编程语言称为解释型语言,使用的转换工具称为解释器,比如 PythonJavaScriptPHP等。

下面就举个例子,使用vim命令新建立一个 python文件,后缀为.py,写入代码print("hello world!"),通过python命令,解释这段代码,打印一下 hello world ! 这句话。

我看可以看到解释型语言,它是边解释边执行,不可脱离解释器环境运行。

MAC 电脑自带了 Python 环境,无需另外手动配置环境。

编译型语言

有的编程语言要转换成二进制指令,也就是生成一个可执行程序这种编程语言称为编译型语言,使用的转换工具称为编译器,比如C语言、C++OC等。

编译型语言也同样举个例子,新建立一个 C文件,写入如下代码:

#include<stdio.h>
int main (int argc,char *agrv[])
{printf("hello world\n");return 0;
}

通过clang hello.c命令,进行编译处理,会生成一个可执行文件,如下图中红色的a.out文件。


这个可执行文件,可以直接运行,通过./a.out即可运行,如图中也可以正常输出hello world这句话。

编译型语言是先整体编译,再执行,运行速度快,任意改动需重新编译,可脱离编译环境运行。

小结:

  • 解释型语言:读到相应代码就直接执行。

  • 编译型语言:先将代码编译成计算机可以识别的二进制文件,才能执行。

扩展:

通过open /usr/bin命令可以查看,电脑上安装的一些系统软件。

/usr不是user的缩写,其实usrUnix Software Resource的缩写, 也就是Unix操作系统软件资源所放置的目录,而不是用户的数据;所有系统默认的软件都会放置到/usr, 系统安装完时,这个目录会占用最多的硬盘容量。

在该目录下可以看到,有我们的 clang编译器,还有 Python解释器,如下图所示:


MacOS系统 默认安装的是python2的环境,输入python,按下enter回车键,可以查看:

警告:不推荐使用 Python 2.7,为了与旧软件兼容macOS中才包含了此版本。macOS 的未来版本将不包含Python 2.7
相反,建议您从终端内过渡到使用“python3”

如果你是python的开发者,那么日常使用的是python3,可以在终端中输入python3查看是否支持:

可以看到我的电脑是支持的,我这里的版本是Python 3.7.7的版本,如果你的电脑没有支持,可以去python官网下载。

2. LLVM

LLVM简介

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

LLVM计划启动于2000年,最初由美国UIUC大学的 ChrisLattner博士主持开展。2006ChrisLattner加盟AppleInc并致力于LLVMApple开发体系中的应用。 Apple也是LLVM计划的主要资助者。目前LLVM已经被苹果IOS开发工具、Xilinx VivadoFacebookGoogle等各大公司采用。

传统编译器设计

我们先来看看传统编译器设计是怎么样的,如下图所示:

  • 编译器前端(Frontend)

编译器前端的任务是解析源代码。它会进行:词法分析语法分析语义分析,检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree, AST),LLVM的前端还会生成中间代码(intermediate representation,IR)。

  • 优化器(Optimizer)

优化器负责进行各种优化,改善代码的运行时间,例如消除冗余计算等。

  • 后端(Backend)/代码生成器(CodeGenerator)

将代码映射到目标指令集,生成机器语言,并且进行机器相关的代码优化。

iOS的编译器架构

ObjectiveC/C/C++使用的编译器前端是ClangSwiftSwift,后端都是LLVM

LLVM的设计

当编译器决定支持多种源语言或多种硬件架构时,LLVM最重要的地方就来了。

其他的编译器如GCC是非常成功的一款编译器,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。

LLVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端。

Clang

对于我们的开发人员来说,看得见摸得着的,接触最多的就是我们的Clang

ClangLLVM项目中的一个子项目。它是基于LLVM架构的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度。它是负责编译CC++Objecte- C语言的编译器,它属于整个LLVM架构中的,编译器前端。对于开发者来说,研究Clang可以给我们带来很多好处。

3. 编译流程

那么我们写一段代码,来测试一下,看看编译流程是什么样子的。

int main(int argc, const char * argv[]) {@autoreleasepool {}return 0;
}

编译的各个阶段

通过一下命令,可以打印源码的编译阶段。

clang -ccc-print-phases main.m

  • 0:输入文件:找到源文件。
  • 1:预处理阶段:这个过程处理包括宏的替换,头文件的导入。
  • 2:编译阶段:进行词法分析、语法分析、检测语法是否正确,最终生成IR
  • 3:后端:这里LLVM会通过一个一个的Pass(可以理解为一个节点)去优化,每个Pass做一些事情,最终生成汇编代码
  • 4:汇编代码生成目标文件
  • 5:链接:链接需要的动态库和静态库,生成相应的镜像可执行文件。
  • 6:根据不同的系统架构,生成对应的可执行文件。

上面已经知道了编译的流程了,那么我们一步一步去看看各个阶段是什么样子的。

#import <stdio.h>
#define B 50
int main(int argc, const char * argv[]) {int a = 10;int c = 20;printf("%d",a + c + B);return 0;
}

预处理阶段

执行如下命令

clang -E main.m >> main1.m


执行完毕后,我们可以在 main1.m 的文件中,可以看到头文件的导入和宏的替换

词法分析

编译阶段-词法分析

预处理完成后就会进行词法分析,这里会把代码切成一个个Token,比如大小括号,等于号还有字符串等。

#import <stdio.h>
#define B 50
typedef int JP_INT;
int main(int argc, const char * argv[]) {JP_INT a = 10;JP_INT c = 20;printf("%d",a + c + B);return 0;
}

clang -fmodules-fsyntax-only -Xclang -dump-tokens main.m


命令运行之后,进行了词法分析,每一行的代码都分开了,切成一个个Token

语法分析

词法分析完成之后就是语法分析,它的任务是验证语法是否正确。在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽象语法树(AbstractSyntaxTree,AST)。语法分析其目的就是对源程序进行分析判断,在结构上是否正确。

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

  • FunctionDecl函数方法声明,范围是第 10行第 1个字符开始 到第 15行第 1个字符结束。第 10行第 5个字符开始,名称叫 main,返回值是int类型,第一个参数的类型是int,第二个参数的类型是const char **。这里为什么是const char **呢?因为数组的名称就是一个指针,const char ** argv 等于const char * argv[]
  • ParmVarDecl参数,当前行的第 10 个字符到第 14个字符是int类型所占有,第 14个字符是参数argc
  • CompoundStmt复合语句,当前行第 41个字符到,第 15行代码的第1个字符,也就是{}包裹的范围。
  • 这两句代码JP_INT a = 10; JP_INT c = 20;对应的是下面这个
  • CallExpr调用表达式, 代码中的printf 函数的打印语法分析如下图
    包括printf函数的指针,告诉我们函数的类型和返回值的类型;第一个参数"%d",第二个参数是一个+加运算的结果,是由ac相加之和,再与50进行相加得到。
  • ReturnStmt返回
  • VarDecl 变量声明
  • StringLiteral字符串字面量
  • IntegerLiteral整型字面量
  • BinaryOperator二元运算符

补充:如果导入的头文件找不到,可以指定SDK

clang isysroot/Applications/Xcode.app/Contents/Developer/Platforms/
iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的 sdk路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m

中间代码IR

完成以上步骤后就开始生成中间代码IR(intermediate representation)了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR

#import <stdio.h>
//#define B 50
//typedef int JP_INT;
int JPTest(int a,int b) {return  a + b + 1;
}
int main(int argc, const char * argv[]) {int c = JPTest(1, 2);printf("%d",c);return 0;
}

通过下面命令可以生成.ll的文本文件,查看IR代码,如下。

clang -S -fobjc-arc -emit-llvm main.m


从图中可以看到,生成了一个.ll的文件,使用 VS Code打开如下:

JPTest方法的生成的IR代码解读如下:

ObjectiveC代码在这一步会进行runtime的桥接:property合成,ARC处理等。

IR的基本语法

@: 全局标识
% : 局部标识
alloca: 开辟空间
align: 内存对齐
i32: 32个bit,4个字节
store: 写入内存
load: 读取数据
call: 调用函数
ret: 返回

以上生成的代码是没有经过优化的,我们可以手动的开启编译器的优化,在 XCode里面可以进行设置的。

IR的优化

LLVM的优化级别分别是-O0-O1-O2-O3-Os(第一个是大写英文字母O)


使用终端的命令,也是可以优化的,那么现在去优化一下,刚刚的代码。

clang -Os -S -fobjc-arc -emit-llvm main.m -o main1.ll


从上面的对比图,可以看出优化之后,JPTestmian代码都少了很多,在mian函数里面并没有看到调用JPTest函数,而是printf直接打印了c的结果 4,这就是优化的强大之处,如下:

优化之后,直接就算出来结果了,这优化还是很给力的哈!优化等级也不是越高就越好。在XCode 里面的优化选项里面release 环境下默认的优化就是最好的了,苹果肯定是给你最好的优化啊。

  • 小结:

编译流程:首先是预处理,对输入代码的宏进行展开;然后是词法分析,会分成一个一个的 token;再是语法分析,会生成 AST语法树;再就会生成IR代码,交给优化器去处理优化代码。

  • bitCode

这是xcode7以后开启bitcode苹果会做进一步的优化,生成bc的中间代码。我们通过优化后的IR代码生成bc代码,这也是一个中间代码,目的是会根据 CPU 的不同架构生成不同大小的包(App Store 商店下载)。

clang -emit-llvm -c main.ll -o main.bc

生成汇编代码

  • 生成汇编代码

我们通过最终的.bc或者.ll代码生成汇编代码

clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s

  • 生成的汇编比较


图中是三种不同后缀生成的汇编代码

  • IR直接生成的汇编是 55行,计算优化了
  • IR生成的 bc在生成汇编,在 IR 的基础上没有进一步的优化了,还是 55
  • 原始的main代码直接生成的汇编就是 62 行了

生成汇编代码的时候也是可以再次进行优化的,那么我们用上面生成的.bc 试一下,开启优化最大,看看生成的汇编是有多少行呢?

clang -Os -S -fobjc-arc main.bc -o main3.s

我们把优化等级调到最高,生成的汇编代码就 47 行了,比上面的55 行少了 8 行,也就是说生成的 IR或者 bc的时候,优化并没有停止,每一个节点上面都有可能再次优化。

生成目标文件(汇编器)

目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object-file),这个阶段就是属于编译器后端的工作了。

clang -fmodules -c main.s -o main.o

通过nm命令,查看下main.o中的符号

xcrun nm -nm main.o

  • _printf是一个是undefined external的。
  • undefined表示在当前文件暂时找不到符号_printf
  • external表示这个符号是外部可以访问的。

生成可执行文件(链接)

连接器把编译产生的.o文件和(dylib .a)文件,生成一个mach-o文件(可执行文件)。

clang main.o -o main

  • 查看链接之后的符号

  • 现在打印的信息就多了,_JPTest_main 也还在,偏移地址也有了,也就是说在执行文件中的位置就确定了。
  • 现在的外部函数除了_printf还有dyld_stub_binder,这是为什么呢?
  • dyld_stub_binder是在 dyld里面,当我们的执行文件 mach-o 进入的内存之后,外部的符号就会立刻马上和dyld_stub_binder进行绑定,这个过程是 dyld 强制绑定的。
  • 链接和绑定是两个概念:链接是我要知道你外部的符号在哪个动态库里面,就是做个标记,我要知道去哪个动态库里面找到你。
  • 绑定是在执行的时候,把动态库libSystem里面的和你这个外部调用的_printf进行绑定,绑定是在执行期,链接是在编译期。

以上就是 LLVM大致的工作流程,接下来将介绍如何写一个自己的Clang插件。

4. 写在后面

关注我,更多内容持续输出

  • CSDN
  • 掘金
  • 简书

iOS底层探索之LLVM(一)——初识LLVM相关推荐

  1. iOS底层探索(二) - 写给小白看的Clang编译过程原理

    iOS底层探索(一) - 从零开始认识Clang与LLVM 写在前面 编译器是属于底层知识,在日常开发中少有涉及,但在我的印象中,越接近底层是越需要编程基本功,也是越复杂的.但要想提升技术却始终绕不开 ...

  2. iOS底层探索二(OC 中 alloc 方法 初探)

    前言 相关文章: iOS底层探索一(底层探索方法) iOS底层探索三(内存对齐与calloc分析) iOS底层探索四(isa初探-联合体,位域,内存优化) iOS底层探索五(isa与类的关系) iOS ...

  3. iOS 底层探索篇 —— KVC 底层原理

    iOS 底层探索篇 -- KVC 底层原理 1. Method Swizzling的坑与应用 1.1 method-swizzling 是什么? 1.2 坑点 坑点1:method-swizzling ...

  4. iOS 底层探索 - 消息转发

    一.动态方法解析流程分析 我们在上一章<消息查找>分析到了动态方法解析,为了更好的掌握具体的流程,我们接下来直接进行源码追踪. 我们先来到 _class_resolveMethod 方法, ...

  5. app启动页数秒加载 代码_iOS 底层探索 - 应用加载

    一.前导知识 以下参考自 WWDC 2016 Optimizing App Startup Time : 1.1 Mach-O Mach-O is a bunch of file types for ...

  6. iOS底层原理之内存管理

    文章目录 定时器 CADisplayLink.NSTimer GCD定时器 内存管理 iOS程序的内存布局 Tagged Pointer OC对象的内存管理 拷贝 引用计数的存储 dealloc 自动 ...

  7. The LLVM Compiler Infrastructure | LLVM编译器基础设施

    https://llvm.org/ LLVM项目是模块化和可重用的编译器及工具链技术的集合.尽管名称如此,LLVM与传统虚拟机关系不大.名称" LLVM"本身不是缩写.它是项目的全 ...

  8. 【iOS底层】11:消息转发

    一.msgSend消息发送监听 在探索了很多次了lookUpImpOrForward方法中,查找完成后会写入cache 在写入cache中发现有个打印log的操作 我们来看下是否可以通过这个输出到本地 ...

  9. ios底层核心模板结构 - list_array_tt entsize_list_tt

    探索objc底层原理过程中,经常会碰到一个模板 list_array_tt 你在类加载,方法查找,散列表,同步锁等等都会碰到这个结构,c++的模板功能十分强大,但代码看上去确实比较头疼 其实没那么糟了 ...

  10. iOS底层原理 - 常驻线程

    iOS底层原理 - 常驻线程 在 AFN 2.0 时代,会经常看到 AFN 创建一个常驻线程的方式: 0️⃣ AFN 2.0 时代的常驻线程 + (NSThread *)networkRequestT ...

最新文章

  1. C错误异常处理,异常处理
  2. 经典的java程序_Java经典程序
  3. 【CodeForces - 987C 】Three displays (dp,最长上升子序列类问题,三元组问题)
  4. 给网站文字添加图标-Font Awesome
  5. 电动葫芦使用注意事项(转载)
  6. 在linux中查找重复的文件夹,如何在Linux上找出并删除重复的文件:FSlint
  7. 强烈建议每个技术人做下这个测评,找到你的优势!
  8. bootice 修改ubuntu win10 系统引导在一个硬盘上时的系统启动顺序
  9. OpenCV-python安装教程
  10. Nginx学习使用记录
  11. 麦块java文件夹_我的世界游戏文件夹目录作用介绍 各个文件夹都是什么用
  12. [Maven]intellij 如何使用mvn clean
  13. 一个直播例子:快速集成iOS基于RTMP的视频推流
  14. HTML期末大作业~仿小米商城网页设计模板(HTML+CSS+JavaScript)
  15. 收不到验证码怎么办?
  16. SpringCloud之Eureka(微服务注册)
  17. 公开课发布:《SQL开发中容易被忽略的BUG》by郑老师
  18. python图表制作方法_Python中一种简单的动态图表制作方法
  19. 启动radius服务器报错
  20. html弹窗可以关闭,js 点击弹窗以外 关闭弹窗

热门文章

  1. 涉及到大小变化,类似QScrollArea判断大小是否显示滚动条
  2. 简单的excel上传解析和错误反馈信息下载
  3. java-web 过滤器 amp; 监听器 amp; 拦截器
  4. C#多线程池演示例程--下载图片
  5. 一个解析XML的VB类
  6. mysql安装运行(centos)
  7. Oracle服务的作用
  8. PAT1030 Travel Plan (30)---DFS
  9. lua MVC框架 Orbit初探
  10. js 禁止剪切、复制、粘贴的文本框代码