文章目录
由来
一个月前,剁手了AMD Ryzen Threadripper 2990WX(官网),这个处理器的参数着实牛逼,32核心64线程,总共80MB的缓存,可以说秒杀目前所有的桌面级处理器了!狠了狠心,搞了一台,辅以32GB DDR4 3200MHz内存和970 EVO NVMe SSD,经过一番折腾(无CPU刷BIOS、装Windows/Linux系统),最终确定使用Windows 10专业工作站版作为日常开发使用。
Cinebench R15跑分:
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的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的任务管理器中看到)
虽然这种设计能够使计算机的扩展性更好,但由于内存访问、缓存数据同步等方面的问题,这种架构对操作系统和应用程序的调度设计考验较大,如果没有进行专门的调优,可能并不能完全发挥出硬件的性能
推测&调优
了解了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官方的优化方向主要在以下几个方面:
-
垃圾回收(GC)
-
合理利用NUMA
-
编译器(主要指JIT)和内核设置(Windows也调不成,放弃)
-
运行时设置
由于这篇文章针对的操作系统环境是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程序调优流程思想,这里盗个图(原文链接):
从上图的红线部分可以看到,JVM性能优化的核心思想就是对Hotspot虚拟机和GC进行调优
Android Studio调优
那么对于Android Studio的优化,不光是AS自身的性能调优,还需要对Android工程构建依赖的各类子模块进行参数调优,主要分布在以下几种地方:
-
Android Studio(IntelliJ IDEA)的VM Options
这个主要用于调优Android Studio的项目加载速度、Indexing速度、代码阅读、查找速度等
-
Gradle的Properties
由于现在的Android项目都使用Gradle进行构建,调优Gradle的Properties有助于加速Gradle的Sync、Build等过程
-
各类Compiler的命令行参数
这些Compiler主要用于将Java、Kotlin、XML等代码、资源进行编译、打包,其实际都为一个个Java程序,如:Java Compiler、Kotlin Compiler及Android Compiler(包括DEX和Proguard)
对于AMD Threadripper 2990WX,我尝试添加以下几种命令行参数,这里先以Android Studio的studio64.vmoptions文件为例
-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):
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
文件进行全局设置,我的配置如下:
# 开启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的优化项,如图所示:
其中的“Command-line Options”内容如下:
--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的代码混淆工作
在上图所示的界面中,填入Additional command line parameters
或VM Options
框中即可,内容参考Android Studio的VM Option,这里也给出具体的内容:
-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略好一些
具体更换步骤:
-
设置
JAVA_HOME
的环境变量,指向Oracle JDK -
删除Android Studio目录中的jre文件夹
如果不做2中的删除操作,那么使用Android Studio的快捷方式或studio64.exe开启Android Studio时,仍会使用自带的OpenJDK,除非在bin目录下执行studio.bat
相对独立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模式,从而优化内存访问效率
总结
自2013年Android Studio第一个版本推出以来,由于Gradle的引用,导致其全量编译速度比以前的ADT(Eclipse)慢(但增量很快),经过几年的迭代,Gradle的性能也在不断地提升,Android的构建也越来越强大了,但AMD Ryzen Threadripper 2990WX的出现,使得消费级CPU的核心数量大幅提升,一些应用程序、操作系统针对这么多核CPU的优化还没有及时跟上,导致CPU的性能不能完全发挥。为了性能(也为了血汗钱),我们需要不断地折腾,压榨CPU,让它为我们节省时间,提高开发效率,毕竟,时间就是金钱!
最后,放一张优化后再跑Android Studio构建时的CPU利用率图,以此来表现我对这块CPU倾注的心血!