优化 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%