优化Android Studio在AMD 2990WX上的编译速度

由来

一个月前,剁手了AMD Ryzen Threadripper 2990WX(官网),这个处理器的参数着实牛逼,32核心64线程,总共80MB的缓存,可以说秒杀目前所有的桌面级处理器了!狠了狠心,搞了一台,辅以32GB DDR4 3200MHz内存和970 EVO NVMe SSD,经过一番折腾(无CPU刷BIOS、装Windows/Linux系统),最终确定使用Windows 10专业工作站版作为日常开发使用。

Cinebench R15跑分:

R15跑分

CPU-Z跑分:

CPU-Z

虽然分数不像网上那些人跑得那么高,但也是相当猛了,再加上任务管理器64个框框,心里十分舒坦!

数框框

于是乎,安装Android Studio等一系列开发工具,心想编译速度总算能够爽很多了……

然鹅!!

在开启Android Studio进行Build的时候,CPU使用率最高不超过20%,基本处于划水状态,偶尔会跑满几个线程,最多不超过16个线程

划水状态的CPU:

划水

但如果在Linux上使用GCC或者在Windows上使用VSBuild编译如Node.js这样的C/C++源码,就能够达到下图的状态

满血状态的CPU:

满血

心里总觉得很不甘、很蹊跷,于是开始折腾……

了解一下牛逼的架构

这张图用的太多了,几乎谈论到AMD的EPYC和Threadripper处理器,都会拿它说事

AMD架构

AMD的EPYC和Threadripper处理器都是采用4个Die的形式,加上优秀的12nm工艺控制功耗和温度,从而实现超多的核心数量,不得不说能想出这样的设计的人,真的是天才!

不同的CPU型号,启用的Die的数量不一样,但实际都是4个,只是有的型号上,关闭的Die作为辅助计算使用

内存访问的不足

但EPYC对内存的访问是完整的8个通道,而2990WX和2970WX则阉割成了4个通道(据说是为了兼容X399芯片组),这样一来就会导致其中2个Die可以直接访问内存,而另外2个Die则需要通过特定的Infinity Fabric来间接地访问内存,一旦操作系统的调度出现问题,可能会导致内存性能骤减,CPU执行一会儿,就要等待一下内存,使得Threadripper对内存密集型进程的性能,同Intel的i9相比表现不佳(如:Photoshop)

相关文章:https://zhuanlan.zhihu.com/p/45606819

NUMA

由于采用了4 Die+4通道内存访问的设计,2990WX即变成了NUMA(Non Uniform Memory Access Architecture,非统一内存访问架构),摇身一变成为一个4路CPU(可以从Windows的任务管理器中看到)

NUMA

虽然这种设计能够使计算机的扩展性更好,但由于内存访问、缓存数据同步等方面的问题,这种架构对操作系统和应用程序的调度设计考验较大,如果没有进行专门的调优,可能并不能完全发挥出硬件的性能

推测&调优

了解了Threadripper的基本架构,于是猜测是不是NUMA限制了Android Studio、JVM在2990WX上的性能,开始进一步的尝试……

注:以下的调优环境,全部基于64位操作系统和JVM虚拟机,32位不在考虑范围内

查到一篇官方资料

在Google各类NUMA、Java和AMD相关的内容时,偶然发现一篇AMD官方的文档,题为:Java Application Performance Tuning for AMD EPYC™ Processors

虽然这篇文章主要是为了用于为多路的EPYC服务器进行Java服务的调优,但综合EPYC和Threadripper的特征来看,Threadripper也是需要调优的!

AMD官方的优化方向主要在以下几个方面:

  1. 垃圾回收(GC)

  2. 合理利用NUMA

  3. 编译器(主要指JIT)和内核设置(Windows也调不成,放弃)

  4. 运行时设置

由于这篇文章针对的操作系统环境是Linux,所以诸如numactl这样的配置,在Windows上就无法使用了,如果你使用Ubuntu,可以参考这篇文章进行更具体的调控:https://linux.die.net/man/8/numactl

了解JVM调优参数

阅读了AMD的官方文档,发现这些优化的主要表现,就是Java程序的运行参数调整,即所谓的Java HotSpot VM Options,所以需要深入了解JVM的Options,比较有用的是以下两篇Oracle文档

JVM Options说明:https://www.oracle.com/technetwork/articles/java/vmoptions-jsp-140102.html

Java命令行参数(Windows系统):https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

有人给出了一套比较完整的Java程序调优流程思想,这里盗个图(原文链接):

Java调优

Java调优关键

从上图的红线部分可以看到,JVM性能优化的核心思想就是对Hotspot虚拟机和GC进行调优

Android Studio调优

那么对于Android Studio的优化,不光是AS自身的性能调优,还需要对Android工程构建依赖的各类子模块进行参数调优,主要分布在以下几种地方:

  1. Android Studio(IntelliJ IDEA)的VM Options

    这个主要用于调优Android Studio的项目加载速度、Indexing速度、代码阅读、查找速度等

  2. Gradle的Properties

    由于现在的Android项目都使用Gradle进行构建,调优Gradle的Properties有助于加速Gradle的Sync、Build等过程

  3. 各类Compiler的命令行参数

    这些Compiler主要用于将Java、Kotlin、XML等代码、资源进行编译、打包,其实际都为一个个Java程序,如:Java Compiler、Kotlin Compiler及Android Compiler(包括DEX和Proguard)

对于AMD Threadripper 2990WX,我尝试添加以下几种命令行参数,这里先以Android Studio的studio64.vmoptions文件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-server                        # 以server模式运行JVM,以达到更高的吞吐量
-XX:+BackgroundCompilation # 使用后台进行机器码编译,优化JIT性能
-XX:+AggressiveOpts # 优化编译性能
-XX:+AggressiveHeap # 优化堆性能
-XX:+UseNUMA # 使用NUMA,默认是不使用的,对于2990WX尤为关键
-XX:+UseParallelOldGC # 使用并行老生代GC
-XX:+UseParallelGC # 使用并行新生代GC
-XX:-UseConcMarkSweepGC # 停用CMS(并发标记清除)GC,因为会与NUMA所需要的并行GC冲突,导致AS无法启动
-XX:ParallelGCThreads=64 # 并发GC线程数,这里根据逻辑CPU数量设定,也可以稍高一些,可以遵循阿姆达尔定律
-XX:CICompilerCount=64 # 编译器线程数,这里根据逻辑CPU数量设定,也可以稍高一些,可以遵循阿姆达尔定律
-XX:SurvivorRatio=28 # Survivor空间占内存的比例,暂时没搞懂具体的意思,从AMD的文档抄来的,推测是为了减少GC次数
-XX:TargetSurvivorRatio=95 # Survivor空间对象的目标生存率(最大100%),也是抄来的,推测是为了减少GC次数
-XX:MaxTenuringThreshold=15 # 设置最大自适应GC的阈值,最大15,为了和ParallelGC配合使用
-XX:MaxGCPauseMillis=500 # 设置理想的最大GC暂停时间,这样是为了提高Android Studio的响应速度,尽量防止GC造成卡顿
-Xms4g # 最小内存值,设高点有助于提升吞吐量
-Xmx4g # 最大内存值,设置为4GB

有关于几种GC类型的说明,可以参考这篇文章:http://www.importnew.com/14086.html

由于需要使用NUMA,所以我强制让Android Studio使用了ParallelGC,而Android Studio默认使用的是ConcMarkSweepGC,只可以而选一,否则会导致Android Studio无法启动的问题

由于同时使用ParallelGC/ConcMarkSweepGC/G1GC导致无法启动(报错:Failed to create JVM: error code -1):

as无法启动

Gradle调优

Gradle也是使用Java开发的,所以对Gradle的优化,原理和Android Studio是一致的,只不过Gradle自身也有一些特定的参数,具体可以参考Gradle的官网文档:

gradle.properties文件参数文档:https://docs.gradle.org/current/userguide/build_environment.html

命令行参数文档:https://docs.gradle.org/current/userguide/command_line_interface.html

我们可以通过用户主目录(Unix-Like系统上为~/,Windows系统上为C:/Users/用户名)下的.gradle目录中的gradle.properties文件进行全局设置,我的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 开启Gradle的Build Cache,减少不必要的编译
org.gradle.caching=true
# 开启Gradle的后台守护进程编码模式,能够在后台自动进行构建,节省点击运行后的编译时间
org.gradle.daemon=true
# 开启Gradle的并行构建模式
org.gradle.parallel=true
# 指定并发数量
org.gradle.parallel.threads=64
# Gradle的JVM命令行参数,可以参考上面AS的JVM参数,但由于是单行字符串的形式,需要在特殊符号(:和=等)前加反斜杠进行转义
org.gradle.jvmargs=-server -XX\:+BackgroundCompilation -XX\:+AggressiveOpts -XX\:+AggressiveHeap -XX\:+UseNUMA -XX\:+UseParallelOldGC -XX\:+UseParallelGC -XX\:-UseConcMarkSweepGC -XX\:ParallelGCThreads\=64 -XX\:CICompilerCount\=64 -XX\:SurvivorRatio\=28 -XX\:TargetSurvivorRatio\=95 -XX\:MaxTenuringThreshold\=15 -XX\:MaxGCPauseMillis\=500 -Xms4g -Xmx4g -XX\:-HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8
# Gradle工作线程数量
org.gradle.workers.max=64
# Kotlin开启增量编译,节省时间
kotlin.incremental=true
# Kotlin编译开启Cache,节省编译时间
kotlin.caching.enabled=true

同时,也需要在Android Studio的设置中,以命令行参数的形式,配置Gradle的优化项,如图所示:

as-gradle

其中的“Command-line Options”内容如下:

1
--parallel --daemon --build-cache --max-workers=128 -Dorg.gradle.jvmargs="-server -XX\:+BackgroundCompilation -XX\:+AggressiveOpts -XX\:+AggressiveHeap -XX\:+UseNUMA -XX\:+UseParallelOldGC -XX\:+UseParallelGC -XX\:-UseConcMarkSweepGC -XX\:ParallelGCThreads\=64 -XX\:CICompilerCount\=64 -XX\:SurvivorRatio\=28 -XX\:TargetSurvivorRatio\=95 -XX\:MaxTenuringThreshold\=15 -XX\:MaxGCPauseMillis\=500 -Xms4g -Xmx4g -XX\:-HeapDumpOnOutOfMemoryError -Dfile.encoding\=UTF-8"

如果需要对不同项目进行不同配置,在项目根目录中的gradle.properties文件中配置即可

Java Compiler、Kotlin Compiler、Android Compiler调优

其中,Java Compiler和Kotlin Compiler分别负责Java代码和Kotlin代码的虚拟机(JVM或Dalvik)字节码翻译工作,而Android Compiler负责将字节码打包为DEX文件,以及Proguard的代码混淆工作

compiler-option

在上图所示的界面中,填入Additional command line parametersVM Options框中即可,内容参考Android Studio的VM Option,这里也给出具体的内容:

1
-server -XX:+BackgroundCompilation -XX:+AggressiveOpts -XX:+AggressiveHeap -XX:+UseNUMA -XX:+UseParallelOldGC -XX:+UseParallelGC -XX:-UseConcMarkSweepGC -XX:ParallelGCThreads=64 -XX:CICompilerCount=64 -XX:SurvivorRatio=28 -XX:TargetSurvivorRatio=95 -XX:MaxTenuringThreshold=15 -XX:MaxGCPauseMillis=500 -Xms4g -Xmx4g -XX:-HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8

至此,Android Studio和其构建工具链在参数配置方面的优化,基本就已经完成了

其他优化

选择合适的JRE

Android Studio自带的JRE来自JetBrains自己编译的64位OpenJDK(和IDEA、WebStorm等IDE一致),可以满足大部分的应用场景,但我仍然推荐使用Oracle的JDK,性能优化的效果要比OpenJDK略好一些

具体更换步骤:

  1. 设置JAVA_HOME的环境变量,指向Oracle JDK

  2. 删除Android Studio目录中的jre文件夹

如果不做2中的删除操作,那么使用Android Studio的快捷方式或studio64.exe开启Android Studio时,仍会使用自带的OpenJDK,除非在bin目录下执行studio.bat

Oraclejdk

相对独立Module

Gradle的并行构建能力,对于比较独立的Project、Module来说优化较好。这也比较容易理解,如果各Module之间的耦合、依赖过强,那么构建过程基本就变成了串行执行,多核CPU的能力自然无法完全发挥出来,可以参考Gradle对Decoupled Project的定义:https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:decoupled_projects

大致优化的思路就是减少模块的耦合程度,尽量独立,特别是要减少链式的依赖

操作系统

由于Windows 10操作系统对AMD的这种多Die形式调度优化不佳,所以可以尝试使用Ubuntu 18.04,如果比较依赖Windows,也推荐使用Windows 10 Pro for Workstation,即专业工作站版,这个比专业版的调度优化更好,能够尽可能多地发挥多Die的性能,比如开启“卓越性能”的电源计划

其次,需要为Threadripper(2990WX、2970WX)安装最新版的X399芯片组驱动和Ryzen Master,并开启Dynamic Local模式,从而优化内存访问效率

ryzen-master

总结

自2013年Android Studio第一个版本推出以来,由于Gradle的引用,导致其全量编译速度比以前的ADT(Eclipse)慢(但增量很快),经过几年的迭代,Gradle的性能也在不断地提升,Android的构建也越来越强大了,但AMD Ryzen Threadripper 2990WX的出现,使得消费级CPU的核心数量大幅提升,一些应用程序、操作系统针对这么多核CPU的优化还没有及时跟上,导致CPU的性能不能完全发挥。为了性能(也为了血汗钱),我们需要不断地折腾,压榨CPU,让它为我们节省时间,提高开发效率,毕竟,时间就是金钱!

最后,放一张优化后再跑Android Studio构建时的CPU利用率图,以此来表现我对这块CPU倾注的心血!

100%