Apple M1は、Web開発でもクソ速い Kotlin & TypeScript編
2020年も残すところわずかとなりました。本年もお疲れ様でした。
今は2020年12月31日大晦日の夜です。本当は2021年1月1日0時0分にドヤ顔で公開しようと思ったのですが、力尽きたのでもう公開します。
この記事は「イエソド アウトプット筋 トレーニング Advent Calendar 2020 無限列車編」のXX日目です。縮退しているたけうちさんがお送りいたします。
TL;DR
- Apple M1は、Intel Core i7と比べて、KotlinやTypeScriptを使った実プロダクト開発で、倍近く速い。
- ただし、まだ自分でトラブルシューティング出来る玄人向け(僕はサポートしたくありません)。
前振り
2020年11月末に、開発機として使用していたMacBook Proの液晶が壊れてしまったのですが、色々大事なデータが入っているのと、忙しいのもあって年を越してもまだ修理に出せていません。
このちらつく画面を見ながら作業するのは気分が悪くなってしまうので(いわゆるポリゴンショック状態)、2020年12月途中までは、持ち運びでは2017年製MacBookを、机ではクラムシェルモードで画面の壊れたMacBook Proを使っていました。
とはいえ、この状態を続けるのもつらく(一時ノートPCを3台持ち歩いていた)、なぜか手元にApple M1が載ったMacBook Air(私物)があったので、クリスマスあたりから開発環境の移行を始めてみました。Ryzen 7 Pro 4750Uが載ったThinkPad T14 Gen 1 (AMD)(私物)もなぜか手元にあって、WSL2で開発環境を作ろうともしてみたのですが、Dockerとかファイルシステムとか、弊社の開発環境としてはいろいろなじまなかったので諦めました。
クリスマスに、周りを改めて見てみるとApple Silicon向けのDocker Previewが発表されていたり、JDKもAzul SystemsがZulu of OpenJDKをApple Silicon向けに出してくれていたり、Node.jsもビルド成功報告が上がってきていたりいけそうでした。
また、昨日IntelliJ IDEAもApple Silicon対応の正式版がリリースされていたり、VS Codeはインサイダービルドがだいぶ前から対応していたりして、これはもう流れが来ています。
ちなみに、IntelliJ IDEA Ultimate for Apple Siliconも爆速でした。
実プロダクトの開発環境でのベンチマーク
Apple M1は速い速いと散々言われていましたが、どうせ特定用途だけだろと思っていて、我々Web開発者達が恩恵受けるのはまだまだ先なんじゃないかと思ってたんですが、ごめんなさいなめてました。
Apple M1への移行試行時に、手始めにNode.jsでプロダクトのフロントエンドをビルドしてみたら、頭の中で?がいくつか並んで、JDKでバックエンドをビルドしてみたら、頭の中が?でいっぱいになりました。うちのプロダクトのビルドってこんなに速く終わったっけ?って。
比較対象
両者共に盛々スペックです。
MacBook Air (M1, 2020) | MacBook Pro (Core i7, 2020) | |
---|---|---|
CPU | Apple M1 | Intel Core i7-1068NG7 |
コア/スレッド | 8/8 (4つの高性能コアと4つの高効率コア) | 4/8 |
メモリ | 16GB | 32GB |
備考 | 液晶画面が壊れてる |
比較内容
イエソド社プロダクト「YESOD」の実プロダクトのフロントエンドとバックエンドコアをビルドして、いくつかの主要な操作の速度の調査。
フロントエンドは、Vue.js+TypeScript、バックエンドコアは、Kotlin+Ktor+Exposedです。また、メインDBはPostgreSQLを使用しています。
JDK (Kotlin)
JDKは11.0.9.1 (Azul Systems, Inc. 11.0.9.1+1-LTS)
で統一。Kotlinは1.4.20
。
$ cloc backend/core/src backend/loader/src
202 text files.
202 unique files.
5 files ignored.
github.com/AlDanial/cloc v 1.88 T=0.06 s (3347.4 files/s, 244872.6 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Kotlin 173 1838 212 11163
JSON 21 0 0 1083
YAML 1 0 0 77
XML 2 2 0 36
-------------------------------------------------------------------------------
SUM: 197 1840 212 12359
-------------------------------------------------------------------------------
./gradlew clean classes
ベンチマーク名 成果物を一度全て削除した後、Kotlinで一からJavaのクラスファイルにビルドします。
./gradlew clean assemble
ベンチマーク名 成果物を一度全て削除した後、Kotlinで一からJavaのクラスファイルにビルドし、成果物としてjarならびに依存ライブラリ全部入りのshadowJarを生成します。tarとzipの生成はスキップしてます。
./gradlew test
ベンチマーク名 ビルド後のクラスファイルが全てある状態で、テストを走らせます。JUnit5の並列実行モード(concurrent)を使用しています。ユニットテストだけでなく、DBとの接続を挟んだシナリオテストも含まれています。
Node.js (TypeScript + SCSS + eslint + stylelint)
v15.5.0
で統一。普段は、v12.x LTS
系を使っていますが、v15.5.0
でないとApple Silicon向けのビルドが通らないため。ソースコードの変更は特に変更必要ありませんでした。
$ cloc frontend/src
381 text files.
381 unique files.
7 files ignored.
github.com/AlDanial/cloc v 1.88 T=0.16 s (2290.1 files/s, 291471.8 lines/s)
-------------------------------------------------------------------------------
Language files blank comment code
-------------------------------------------------------------------------------
Vuejs Component 212 3966 274 28947
TypeScript 133 1467 219 10351
YAML 1 0 0 859
Sass 17 144 211 755
JSON 7 0 0 391
SVG 4 0 0 16
-------------------------------------------------------------------------------
SUM: 374 5577 704 41319
-------------------------------------------------------------------------------
npm run serve
ベンチマーク名 Vue CLIを利用して、開発用のサーバを起動し、ホットリロード可能な状態になるまでの時間を計測しています。上に書いたとおり、TypeScript + SCSS + eslint + stylelint
でトランスパイル要素満載です。なんか最近lintを色々追加したせいで遅いです。
npm run build
ベンチマーク名 Vue CLIを利用して、成果物のHTML、JavaScript、CSSにトランスパイルします。
結果
MacBook Air (M1, 2020) | MacBook Pro (Core i7, 2020) | |
---|---|---|
gradle clean classes | 7.475 sec | 15.296 sec |
gradle clean assemble | 13.193 sec | 28.099 sec |
gradle test | 9.672 sec | 14.386 sec |
npm run serve | 5.917 sec | 9.958 sec |
npm run build | 14.948 sec | 21.382 sec |
んあああああぁぁああ!!!!は゛や゛い゛ぃ゛ぃ゛ぃ゛い゛い゛!゛!゛!゛!゛
ついでに、パソコンも冷えたままでファンも回らず、とても快適です。
余談
Web開発者としての、Apple M1の乗りこなし方について、ポイントだけ紹介します。
ターミナルの用意
iTerm2を使いましょう。「Rosetta を使用して開く」にチェックボックスを入れろとかいう謎記事は信じてはいけません。信じるとたぶん死にます。
uname -a
コマンドを打って、最後の方にarm64
と書いてあることを必ず確認してください。
$ uname -a
Darwin arm64.local 20.2.0 Darwin Kernel Version 20.2.0: Wed Dec 2 20:40:21 PST 2020; root:xnu-7195.60.75~1/RELEASE_ARM64_T8101 arm64
Homebrew Documentationも必ずよく読んで、/opt/homebrew
にApple Silicon向けのビルドを入れてください。/usr/local
にIntel向けビルドも同時にインストールできますが、よくわからず混ざると死にます。ただ、混ぜると便利です。
JDKの入れ方
Zulu Builds of OpenJDKからARM 64-bit版を選択して入れましょう。
tar.gz
をダウンロードして/Library/Java/JavaVirtualMachines/
に配置して、
export JAVA_HOME=$(/usr/libexec/java_home -v 11)
とか
set -x JAVA_HOME (/usr/libexec/java_home -v 11)
みたいに書くのが好みです。
Node.jsの入れ方
nvmを入れて、おもむろに
nvm install v15.5.0
と叩いてください。たぶんビルドが始まります。始まらなかったら自分で調べてください。brew install coreutils
したらうまくいくかもしれないですが知りません。
Dockerの入れ方
罠はありますが動きます。
まとめ
つよつよエンジニアなら、Web開発でもApple M1で爆速アウトプットできます。
自分が強いエンジニアだと信じられないなら、もうちょっと待った方が良いです。たぶんトラブルシューティングできず死にます。
自分はもうIntel Macには戻りません。
補足資料
詳細なコマンドと結果
所々端折ってますが、大勢は変わりません。
MacBook Air (M1, 2020)
スペック
arm64 $ uname -a
Darwin arm64.local 20.2.0 Darwin Kernel Version 20.2.0: Wed Dec 2 20:40:21 PST 2020; root:xnu-7195.60.75~1/RELEASE_ARM64_T8101 arm64
arm64 $ system_profiler SPHardwareDataType
Hardware:
Hardware Overview:
Model Name: MacBook Air
Model Identifier: MacBookAir10,1
Chip: Apple M1
Total Number of Cores: 8 (4 performance and 4 efficiency)
Memory: 16 GB
System Firmware Version: 6723.61.3
Serial Number (system):
Hardware UUID:
Provisioning UDID:
Activation Lock Status: Enabled
バックエンドコア
arm64 backend$ java -version
openjdk version "11.0.9.1" 2020-11-04 LTS
OpenJDK Runtime Environment Zulu11.43+1021-CA (build 11.0.9.1+1-LTS)
OpenJDK 64-Bit Server VM Zulu11.43+1021-CA (build 11.0.9.1+1-LTS, mixed mode)
arm64 backend$ ./gradlew -v
------------------------------------------------------------
Gradle 6.7
------------------------------------------------------------
Build time: 2020-10-14 16:13:12 UTC
Revision: 312ba9e0f4f8a02d01854d1ed743b79ed996dfd3
Kotlin: 1.3.72
Groovy: 2.5.12
Ant: Apache Ant(TM) version 1.10.8 compiled on May 10 2020
JVM: 11.0.9.1 (Azul Systems, Inc. 11.0.9.1+1-LTS)
OS: Mac OS X 11.1 aarch64
arm64 backend$ time ./gradlew clean classes
> Task :clean
> Task :core:clean
> Task :loader:clean
> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :core:compileKotlin
> Task :core:compileJava NO-SOURCE
> Task :core:processResources
> Task :core:classes
> Task :core:inspectClassesForKotlinIC
> Task :core:jar
> Task :loader:compileKotlin
> Task :loader:compileJava NO-SOURCE
> Task :loader:processResources NO-SOURCE
> Task :loader:classes UP-TO-DATE
BUILD SUCCESSFUL in 6s
8 actionable tasks: 8 executed
./gradlew clean classes 0.64s user 0.08s system 9% cpu 7.475 total
arm64 backend$ time ./gradlew clean assemble
> Task :clean
> Task :core:clean
> Task :loader:clean
> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :inspectClassesForKotlinIC
> Task :jar
> Task :assemble
> Task :core:compileKotlin
> Task :core:compileJava NO-SOURCE
> Task :core:processResources
> Task :core:classes
> Task :core:inspectClassesForKotlinIC
> Task :core:jar
> Task :core:startScripts
> Task :core:distTar SKIPPED
> Task :core:distZip SKIPPED
> Task :core:shadowJar
> Task :core:startShadowScripts
> Task :core:shadowDistTar SKIPPED
> Task :core:shadowDistZip SKIPPED
> Task :core:assemble
> Task :loader:compileKotlin
> Task :loader:compileJava NO-SOURCE
> Task :loader:processResources NO-SOURCE
> Task :loader:classes UP-TO-DATE
> Task :loader:inspectClassesForKotlinIC
> Task :loader:jar
> Task :loader:assemble
BUILD SUCCESSFUL in 12s
15 actionable tasks: 15 executed
./gradlew clean assemble 0.65s user 0.06s system 5% cpu 13.193 total
arm64 backend$ time ./gradlew test
> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin NO-SOURCE
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :core:compileKotlin UP-TO-DATE
> Task :core:compileJava NO-SOURCE
> Task :core:processResources
> Task :core:classes
> Task :core:compileTestKotlin
> Task :core:compileTestJava NO-SOURCE
> Task :core:processTestResources UP-TO-DATE
> Task :core:testClasses UP-TO-DATE
> Task :core:test
> Task :core:inspectClassesForKotlinIC UP-TO-DATE
> Task :core:jar
> Task :loader:compileKotlin
> Task :loader:compileJava NO-SOURCE
> Task :loader:processResources NO-SOURCE
> Task :loader:classes UP-TO-DATE
> Task :loader:compileTestKotlin NO-SOURCE
> Task :loader:compileTestJava NO-SOURCE
> Task :loader:processTestResources NO-SOURCE
> Task :loader:testClasses UP-TO-DATE
> Task :loader:test NO-SOURCE
BUILD SUCCESSFUL in 9s
8 actionable tasks: 5 executed, 3 up-to-date
./gradlew test 0.69s user 0.06s system 7% cpu 9.672 total
フロントエンド
arm64 frontend$ node -v
v15.5.0
arm64 frontend$ npm run serve
> @yesodco/yesod-frontend@0.1.0 serve
> vue-cli-service serve
INFO Starting development server...
Starting type checking service...
Using 1 worker with 2048MB memory limit
DONE Compiled successfully in 5917ms
No type errors found
Version: typescript 4.1.2
Time: 4599ms
Webpack Bundle Analyzer is started at http://127.0.0.1:8088
Use Ctrl+C to close it
App running at:
- Local: http://localhost:3000/
- Network: http://yesod.me:443/
Note that the development build is not optimized.
To create a production build, run npm run build.
arm64 frontend$ time npm run build
> @yesodco/yesod-frontend@0.1.0 build
> vue-cli-service build
⠙ Building for production...Starting type checking service...
Using 1 worker with 2048MB memory limit
⠴ Building for production...
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
npm run build 28.18s user 1.75s system 200% cpu 14.948 total
MacBook Pro (Core i7, 2020)
スペック
x86_64 $ uname -a
Darwin x86_64.local 20.2.0 Darwin Kernel Version 20.2.0: Wed Dec 2 20:39:59 PST 2020; root:xnu-7195.60.75~1/RELEASE_X86_64 x86_64
x86_64 $ system_profiler SPHardwareDataType
Hardware:
Hardware Overview:
Model Name: MacBook Pro
Model Identifier: MacBookPro16,2
Processor Name: Quad-Core Intel Core i7
Processor Speed: 2.3 GHz
Number of Processors: 1
Total Number of Cores: 4
L2 Cache (per Core): 512 KB
L3 Cache: 8 MB
Hyper-Threading Technology: Enabled
Memory: 32 GB
System Firmware Version: 1554.60.15.0.0 (iBridge: 18.16.13030.0.0,0)
Serial Number (system):
Hardware UUID:
Provisioning UDID:
Activation Lock Status: Enabled
バックエンドコア
x86_64 backend$ java -version
openjdk version "11.0.9.1" 2020-11-04 LTS
OpenJDK Runtime Environment Zulu11.43+55-CA (build 11.0.9.1+1-LTS)
OpenJDK 64-Bit Server VM Zulu11.43+55-CA (build 11.0.9.1+1-LTS, mixed mode)
x86_64 backend$ ./gradlew -v
------------------------------------------------------------
Gradle 6.7
------------------------------------------------------------
Build time: 2020-10-14 16:13:12 UTC
Revision: 312ba9e0f4f8a02d01854d1ed743b79ed996dfd3
Kotlin: 1.3.72
Groovy: 2.5.12
Ant: Apache Ant(TM) version 1.10.8 compiled on May 10 2020
JVM: 11.0.9.1 (Azul Systems, Inc. 11.0.9.1+1-LTS)
OS: Mac OS X 10.16 x86_64
x86_64 backend$ time ./gradlew clean classes
> Task :clean
> Task :core:clean
> Task :loader:clean
> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :core:compileKotlin
> Task :core:compileJava NO-SOURCE
> Task :core:processResources
> Task :core:classes
> Task :core:inspectClassesForKotlinIC
> Task :core:jar
> Task :loader:compileKotlin
> Task :loader:compileJava NO-SOURCE
> Task :loader:processResources NO-SOURCE
> Task :loader:classes UP-TO-DATE
BUILD SUCCESSFUL in 14s
8 actionable tasks: 8 executed
./gradlew clean classes 1.35s user 0.29s system 10% cpu 15.296 total
x86_64 backend$ time ./gradlew clean assemble
> Task :clean
> Task :core:clean
> Task :loader:clean
> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :inspectClassesForKotlinIC
> Task :jar
> Task :assemble
> Task :core:compileKotlin
> Task :core:compileJava NO-SOURCE
> Task :core:processResources
> Task :core:classes
> Task :core:inspectClassesForKotlinIC
> Task :core:jar
> Task :core:startScripts
> Task :core:distTar SKIPPED
> Task :core:distZip SKIPPED
> Task :core:shadowJar
> Task :core:startShadowScripts
> Task :core:shadowDistTar SKIPPED
> Task :core:shadowDistZip SKIPPED
> Task :core:assemble
> Task :loader:compileKotlin
> Task :loader:compileJava NO-SOURCE
> Task :loader:processResources NO-SOURCE
> Task :loader:classes UP-TO-DATE
> Task :loader:inspectClassesForKotlinIC
> Task :loader:jar
> Task :loader:assemble
BUILD SUCCESSFUL in 27s
15 actionable tasks: 15 executed
./gradlew clean assemble 1.76s user 0.44s system 7% cpu 28.099 total
x86_64 backend$ time ./gradlew test
> Task :compileKotlin NO-SOURCE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin NO-SOURCE
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :core:compileKotlin UP-TO-DATE
> Task :core:compileJava NO-SOURCE
> Task :core:processResources
> Task :core:classes
> Task :core:compileTestKotlin
> Task :core:compileTestJava NO-SOURCE
> Task :core:processTestResources UP-TO-DATE
> Task :core:testClasses UP-TO-DATE
> Task :core:test
> Task :core:inspectClassesForKotlinIC UP-TO-DATE
> Task :core:jar
> Task :loader:compileKotlin
> Task :loader:compileJava NO-SOURCE
> Task :loader:processResources NO-SOURCE
> Task :loader:classes UP-TO-DATE
> Task :loader:compileTestKotlin NO-SOURCE
> Task :loader:compileTestJava NO-SOURCE
> Task :loader:processTestResources NO-SOURCE
> Task :loader:testClasses UP-TO-DATE
> Task :loader:test NO-SOURCE
BUILD SUCCESSFUL in 13s
8 actionable tasks: 5 executed, 3 up-to-date
./gradlew test 1.22s user 0.26s system 10% cpu 14.386 total
フロントエンド
x86_64 frontend$ node -v
v15.5.0
x86_64 frontend$ npm run serve
> @yesodco/yesod-frontend@0.1.0 serve
> vue-cli-service serve
INFO Starting development server...
Starting type checking service...
Using 1 worker with 2048MB memory limit
DONE Compiled successfully in 9958ms
No type errors found
Version: typescript 4.1.2
Time: 7387ms
Webpack Bundle Analyzer is started at http://127.0.0.1:8088
Use Ctrl+C to close it
App running at:
- Local: http://localhost:3000/
- Network: http://yesod.me:443/
Note that the development build is not optimized.
To create a production build, run npm run build.
x86_64 frontend$ time npm run build
> @yesodco/yesod-frontend@0.1.0 build
> vue-cli-service build
⠙ Building for production...Starting type checking service...
Using 1 worker with 2048MB memory limit
⠸ Building for production...
DONE Build complete. The dist directory is ready to be deployed.
INFO Check out deployment instructions at https://cli.vuejs.org/guide/deployment.html
npm run build 42.81s user 3.71s system 217% cpu 21.382 total
Discussion
M1 Mac が欲しくなりました……
本文に追記するのが正しいかも知れませんが、
uname -a
の内容によって、どのアーキテクチャ向けにバイナリをビルドするか変わるOSSが多いです。uname -a
の末尾がarm64
の時のNode.jsビルド抜粋引数に
-arch arm64
が入るuname -a
の末尾がx86_64
の時Node.jsビルド抜粋引数に
-arch x86_64
が入る実行ファイルがどちらの環境向けにビルドされているかのチェックの方法
Appleが提供しているバイナリは、だいたいUniversal Binaryになっていて気づきにくいので注意。
Mach-O 64-bit executable arm64
と書いてあれば、arm64向けにビルドされている。