Apple M1は、Web開発でもクソ速い Kotlin & TypeScript編

19 min読了の目安(約17700字TECH技術記事 2

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