1. 程式人生 > >優化Android Studio在AMD 2990WX上的編譯速度

優化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)

NUMA

由於採用了4 Die+4通道記憶體訪問的設計,2990WX即變成了NUMA(Non Uniform Memory Access Architecture,非統一記憶體訪問架構),搖身一變成為一個4路CPU(可以從Windows的工作管理員中看到)

NUMA

雖然這種設計能夠使計算機的擴充套件性更好,但由於記憶體訪問、快取資料同步等方面的問題,這種架構對作業系統和應用程式的排程設計考驗較大,如果沒有進行專門的調優,可能並不能完全發揮出硬體的效能

推測&調優

瞭解了Threadripper的基本架構,於是猜測是不是NUMA限制了Android Studio、JVM在2990WX上的效能,開始進一步的嘗試……

注:以下的調優環境,全部基於64位作業系統和JVM虛擬機器,32位不在考慮範圍內

查到一篇官方資料

雖然這篇文章主要是為了用於為多路的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文件

有人給出了一套比較完整的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檔案為例

-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

由於需要使用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的官網文件:

我們可以通過使用者主目錄(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的優化項,如圖所示:

as-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的程式碼混淆工作

compiler-option

在上圖所示的介面中,填入Additional command line parametersVM 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略好一些

具體更換步驟:

  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%