Corredor

ウェブ、プログラミングの勉強メモ。

Git For Windows・Git SDK の起動を爆速にする

Git For Windows やその上位互換である Git SDK (以降「GitBash」で総称する) の起動時のトロさといったら。Mac のターミナルくらい爆速で起動して使い始めたいのに、git-bash.exe を起動して最初のプロンプトが表示されるまで2・3秒待たないといけない。

今回はそんな Windows GitBash の起動を爆速にするための取り組みをまとめる。

Windows GitBash が遅い理由

そもそも何で GitBash は起動や動作が遅いのだろう。

最近気付いたのだが、MacOS 上で、VirtualBox で Windows 環境を作り (Modern.IE とかでいい)、その上で Git For Windows をインストールすると、仮想環境とは思えない速度でターミナルが起動するのだ。ということは、「MacOS の SSD が速くて、Windows マシンの SSD は遅いのかしら?」と、ハードウェアの性能差すら疑ってしまった。

で、そんな疑問を調べていたら、Twitter で素晴らしいリプライをいただいた。

Windowsのファイルアクセスとプロセス起動がUNIX系に比べて相当遅い傾向があるせいじゃないでしょうか。
ファイルアクセスが遅い問題はSSD使うとそれなりには緩和されると思います。
あとVirtualBoxのストレージコントローラには「ホストのI/Oキャッシュを使う」設定があり、これでも速くなりそうです

薄々勘付いてはいたが、そもそも Windows OS 自体が、ファイルアクセスとプロセス起動が遅い傾向にあるようだ。

ファイルアクセスというと、test コマンドなどでファイルの存在チェックを行って、source コマンドで読み込んだり、といったことが起動時に行われている。原理的には、OS を問わず、ファイルアクセスの回数や量に比例して動作が遅くなっていくが、Windows OS ではそれを MacOS や Linux OS よりも顕著に感じる、ということだろう。

GitBash の中身はほぼ MSYS2 で、Bash コマンドを Windows 向けに移植して .exe 化して取り揃えている。Bash の言語仕様上の細かなところを知らないので推測にはなるが、平たく考えると、Bash コマンドを実行する度に、コマンドの実体である .exe ファイルを読みに行き (ファイルアクセス)、そのプロセスを起動して、処理しているワケだ。起動時に使用するコマンド ≒ 処理が多ければ多いほど、Windows ではより顕著に速度性能を体感してしまう、というワケだ。

あと、コレは体感的な話ではあるが、echo によるターミナルへの出力自体も、もっさりしているように感じる。Git SDK はターミナル起動時に「Welcome」とかなんちゃら echo してきやがるので、コンソール出力も止めよう。

そうそう、Mac の VirtualBox 上で動作する Windows だとそのような遅さを感じなかった理由についても判明した。ホスト OS の I/O キャッシュが利用されているので、MacOS のファイルアクセスの性能が活かされているということのようだ。

じゃあ、速くするにはどうしたらいいのか?

Windows は「ファイルアクセス」と「プロセス起動」に時間がかかることが、GitBash の起動の遅さの原因と考えられることが分かった。

ということは、この裏を返せば、起動を速くする方法に繋がるだろう。

  • ファイルアクセスを減らす
    • test コマンドなどによるファイルの存在チェックを省けるだけ省く
    • source コマンドで外部ファイルを読み込んでいる箇所を省く : 呼び出し元のスクリプトファイルにインラインでベタ書きしてやれば同じ結果が得られるので、インライン化してしまう
    • mkdirchmod などファイルを操作するコマンドを調整する
  • プロセス起動を減らす
    • exec や、存在チェックなどの条件分岐や関数呼び出し、変数展開や置換など、考えられる「無駄な処理」は削れるだけ削り、使うコマンドの数を減らす

とりあえず、起動時に読み込まれたり、参照・操作されるファイル数を減らし、余計な処理をガンガン削ったら、原理的には速くなりそうだ。

しかし、起動時に読み込まれているファイルって何なのだろう?

一つずつ調べていくことにした。

Git SDK の前提環境

まずは Git SDK の起動を速くしていこうと思う。前提となる環境は以下のとおり。

  • git-sdk-installer-1.0.7-64.7z.exe (Git SDK v1.0.7) を C:\git-sdk-64\ にインストール後、1回以上普通に起動したことがある (初期設定が完了している)
  • C:\git-sdk-64\ ディレクトリの git log は以下のとおり
    • 74eb921e 2018-12-18 Update 20181218-031256 [Git for Windows Build Agent] (grafted, HEAD -> master, origin/master)
  • C:\git-sdk-64\.git\ ディレクトリを消しておく。そうしないと、/ が Git 管理されているテイになっているので、Git 管理外のディレクトリでも __git_ps1 が表示されてしまう

このような状態で、調査を開始した。

GitBash 起動時に読み込まれているファイル・処理内容を知る

Git SDK (git-bash.exe) を起動すると、現状は3秒くらい時間がかかっている。

まずは起動時にどのようなファイルが読み込まれているか調べるため、/usr/bin/bash.exe の動作ログを出力してみる。やり方は以前別の記事で紹介した。

neos21.hatenablog.com

$ PS4='+$BASH_SOURCE> ' BASH_XTRACEFD=7 bash -xl 7>&2

bash.exe が立ち上がると、C:\git-sdk-64\etc\profile (= /etc/profile) が読み込まれ、このファイルから次のファイル群が source されていた。

  • /etc/msystem : MSYSTEM_PREFIX など環境変数を設定する
  • /etc/post-install/ 配下の全ファイル : インストール直後の初期設定がメイン。設定ができていたら結果的に何もしないのだが、Git SDK を起動するたびに source するので遅くなっているようだった
    • /etc/post-install/01-devices.post : /dev/ 配下のシンボリックリンクを作成する
    • /etc/post-install/03-mtab.post : /proc/mounts の再現
    • /etc/post-install/06-windows-files.post : C:\Windows\System32\drivers\etc\ との設定
    • /etc/post-install/07-pacman-key.post : pacman-key の設定
    • /etc/post-install/08-xml-catalog.post : xmlcatalog の用意
    • /etc/post-install/99-post-install-cleanup.post : 不要なファイルの削除
  • /etc/profile.d/ 配下のファイル : perlbin.csh 以外が呼ばれる
    • /etc/profile.d/aliases.sh : alias 設定
    • /etc/profile.d/bash_completion.sh : Bash の複雑なタブ補完用ファイル /usr/share/bash-completion/bash_completion を読み込む
    • /etc/profile.d/bash_profile.sh : ~/.bash_profile~/.bashrc などのファイルの存在チェック・なければ新規生成する
    • /etc/profile.d/env.sh : 環境変数 PATH の設定
    • /etc/profile.d/git-prompt.sh : 環境変数 PS1 (プロンプト) の設定
    • /etc/profile.d/git-sdk.sh : sdk 関数の用意。Welcome to the Git for Windows SDK! のメッセージはこのスクリプトが出力している
    • /etc/profile.d/lang.sh : 環境変数 LANG (言語) の設定
    • /etc/profile.d/perlbin.sh : Perl に関する環境変数 PATH の設定
    • /etc/profile.d/tzset.sh : 環境変数 TZ (タイムゾーン) の設定
  • /etc/bash.bashrc : プロンプトの設定など
  • あとは ~/.bash_profile および ~/.bashrc。普段ユーザ定義する部分だが、ココのカスタマイズは今回対象外とする

この /etc/profile と、そこから読み込まれる外部ファイル群から「無駄」を省いていけば速くなるかもしれない。

  • これらのファイル群は、MSYS2 系の様々な環境で汎用的に動作するように書かれているので、32 bit 版向けの条件分岐や処理が含まれていたりする
  • SSH 接続時の調整など、自分の用途ではまず使わない場合向けの設定が含まれている

今回、起動を速くするためなら自分が使う環境に強く依存するようなカスタマイズでもいとわないことにした。

まずは source しているファイルの中身を /etc/profile 内にコピペし、外部ファイルの読み込みを全てなくした。その状態で、条件分岐や変数の設定内容などを細かく見ていき、通っていない処理、通っていても実質的に何も変更していない無駄な処理をコメントアウトした。ココは地道に変数の定義前・定義直後で変数を echo したりして調べていった。

この細かな調査記録は以下にコメント込みで記述したファイル全量があるので、気になる人は参照されたし。

Git SDK の起動を爆速にする /etc/profile

こうして極限まで無駄を省いた結果、/etc/profile の内容は以下のように収めることができた。

# Git SDK 向け /etc/profile 超削減版

MSYS2_PATH=/usr/local/bin:/usr/bin:/bin
MANPATH=/usr/local/man:/usr/share/man:/usr/man:/share/man
INFOPATH=/usr/local/info:/usr/share/info:/usr/info:/share/info
ORIGINAL_PATH="$PATH"  # 元のソースでは変数の内容チェックなどがあったが省いた

# 後述するが Git For Windows とは設定内容が違うので注意
MSYSTEM=MSYS
MSYSTEM_PREFIX=/usr
MSYSTEM_CARCH=x86_64
MSYSTEM_CHOST=x86_64-pc-msys
CONFIG_SITE=/etc/config.site

PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/lib/pkgconfig

# 元のソースは /tmp ディレクトリの特定に色々処理していたが、全てベタ書きにした
ORIGINAL_TMP=/tmp
ORIGINAL_TEMP=/tmp
TMPDIR=/tmp

# 本来は "$(exec /usr/bin/hostname)" で代入していたところなのだが、ベタ書きにしてしまった。export する必要性がイマイチ分かっていない変数
HOSTNAME=Neos-Windows
# プロンプトは最小構成にしておく。自分は ~/.bash_profile で設定しているのでココでは余計なことはせずにシンプルにしておく
PS1='\n$ '
# locale コマンドの結果だけ書いてしまう
LANG=ja_JP.UTF-8
# tzset コマンドの結果だけ書いてしまう
TZ=Asia/Tokyo
# 環境変数 PATH への代入は数箇所に登場していたので、1箇所でまとめてやるようにした
PATH="$HOME/bin:$MSYS2_PATH:/opt/bin:$ORIGINAL_PATH:/usr/bin/vendor_perl:/usr/bin/core_perl"

# export コマンドの実行は1回だけにする
export PATH MANPATH INFOPATH PKG_CONFIG_PATH LANG TZ TMP TEMP TMPDIR HOSTNAME PS1 SHELL ORIGINAL_TMP ORIGINAL_TEMP ORIGINAL_PATH MSYSTEM MSYSTEM_PREFIX MSYSTEM_CARCH MSYSTEM_CHOST CONFIG_SITE

コメント・空行を省いて19行になった。コレ以外は、条件分岐に合致せず呼び出されていないコードだったり、実行されても何も変化がないコードだったりしたので、全て削った。

変数展開のコストがどれだけかかるのか分からないが、余計な処理コストがかかるのを避けるため、原則的にベタ書きに変更してしまった。

一応処理として通ってはいたので残したものの、起動後の使用箇所が分からずまだ削れそうな変数もある。

  • $ORIGINAL_TMP$ORIGINAL_TEMP : 中身は /tmp になっていたので、結局 $TMP$TEMP と同じじゃん、と。$TMPDIR ももしかしたら要らなさそう
  • $HOSTNAME : プロンプトに使用する \h では参照していないので、もしかしたらコレも要らないかも
  • $PS1 : ~/.bash_profile で独自に設定しているし、別にココで用意してあげなくても良いかも

とはいえ、基本は変数への固定値代入で、コマンドらしいコマンドは export 1回だけに押さえられた。

C:\git-sdk-64\etc\profile = /etc/profile のファイルの内容を、上述の内容に差し替えれば高速化できる。

Git For Windows も高速化する

同じ要領で、通常の GitBash である Git For Windows も高速化してみよう。

  • C:\Program Files\Git\ に v2.20.1 をインストール後、1回以上普通に起動した状態から調査・改良を開始した
  • /etc/profile の内容は Git SDK のモノと全く同一だったので、$MSYSTEM の初期値が 'MSYS' ではなく 'MINGW64' であることによる、一部の条件分岐の違い程度と推測した
  • が、念のため全体の動きを調べ直し、設定する環境変数の違いなどをまとめた

Git For Windows の起動を爆速にする /etc/profile

ということで出来上がった /etc/profile は以下のとおり。

MSYS2_PATH=/usr/local/bin:/usr/bin:/bin
MANPATH=/usr/local/man:/usr/share/man:/usr/man:/share/man
INFOPATH=/usr/local/info:/usr/share/info:/usr/info:/share/info
ORIGINAL_PATH="$PATH"

# 以下の内容が Git SDK と異なる
MSYSTEM=MINGW64
MSYSTEM_PREFIX=/mingw64
MSYSTEM_CARCH=x86_64
MSYSTEM_CHOST=x86_64-w64-mingw32
MINGW_CHOST=x86_64-w64-mingw32
MINGW_PREFIX=/mingw64
MINGW_PACKAGE_PREFIX=mingw-w64-x86_64
CONFIG_SITE=/mingw64/etc/config.site

PKG_CONFIG_PATH=/mingw64/lib/pkgconfig:/mingw64/share/pkgconfig

ORIGINAL_TMP=/tmp
ORIGINAL_TEMP=/tmp
TMPDIR=/tmp

# やりすぎず "$(exec /usr/bin/hostname)" とした方が良いかも。もしくは不要?
HOSTNAME=Neos-Windows

# env.sh より以下を追加した
DISPLAY=needs-to-be-defined
SSH_ASKPASS=/mingw64/libexec/git-core/git-gui--askpass

PS1='\n$ '
LANG=ja_JP.UTF-8

# Git For Windows には tzset.sh がなかったので本来は以下の変数が設定されないが、Git SDK と同じ内容を設定しておく
TZ=Asia/Tokyo

PATH="$HOME/bin:$MSYS2_PATH:/opt/bin:$ORIGINAL_PATH:/usr/bin/vendor_perl:/usr/bin/core_perl"

# ACLOCAL_PATH にも値が最初から入っていたので export 対象に入れた
export PATH MANPATH INFOPATH PKG_CONFIG_PATH LANG TZ TMP TEMP TMPDIR HOSTNAME PS1 SHELL ORIGINAL_TMP ORIGINAL_TEMP ORIGINAL_PATH MSYSTEM MSYSTEM_PREFIX MSYSTEM_CARCH MSYSTEM_CHOST CONFIG_SITE ACLOCAL_PATH DISPLAY SSH_ASKPASS

コメント行、空行を除くと24行になった。

環境変数 $MSYSTEM のデフォルト値が Git SDK と Git For Windows とで異なっていたので、その違いによる環境変数の差分を取り込んだ。32bit 版の Git For Windows を使っている人はまたちょっと変わってくるはず。

どのくらい速くなったのか検証する

さて、当初の体感では2・3秒起動時に待たされていたのが、上述の改修後は、起動して2秒後にはプロンプトが表示されているくらいに高速化された。

数値としてどのくらい高速化したのか、Git SDK 側の変更前後で違いを見てみる。

/etc/profile の1行目と最終行、および ~/.bash_profile の最終行 (~/.bashrc の読込後) の3箇所に、以下のコマンドを仕掛けた。

date '+%F %T %N'
# 'YYYY-MM-DD HH:mm:ss ナノ秒' が出力される

この状態で git-bash.exe を起動し、変更前の /etc/profile と変更後の /etc/profile とで比較してみた。

元々の /etc/profile の実行結果

2018-12-20 22:04:54 425751900  # /etc/profile 開始
Welcome to the Git for Windows SDK!

The common tasks are automated via the `sdk` function;
See `sdk help` for details.
2018-12-20 22:04:56 071816800  # /etc/profile 終了
2018-12-20 22:04:56 648292200  # ~/.bash_profile 終了

sdk() 関数の welcome の内容が出力されているので少し分かりづらいが、/etc/profile の終了まで2秒近くかかっていることが分かる。

~/.bash_profile の終了にも0.6秒程度かかっているが、コレはこのファイルから ~/.bashrcgit-completion.bash などを source していたりするため。ユーザ定義の場所なので参考程度に見て欲しい。

変更後の /etc/profile の実行結果

2018-12-20 22:32:09 508269500  # /etc/profile 開始
2018-12-20 22:32:09 548539700  # /etc/profile 終了
2018-12-20 22:32:09 983198600  # ~/.bash_profile 終了

2秒ほどかかっていた /etc/profile の読み込みは、なんと0.04秒程度で完了するようになった。コメントアウトや空行の有無は実行速度に影響なかった。

こうなると ~/.bash_profile~/.bashrc の読み込み速度の方が気になってくるレベル。それでも全体では0.5秒程度で起動スクリプトの実行が終わっているのは驚異的。

Git For Windows の方も同様だった。Git For Windows の方は /etc/post-install/ ディレクトリ自体が元々存在しなかったので、変更前から Git SDK よりも少し速かったのだが、それよりも格段に速くなった。

以上

体感できているとおり、GitBash の起動を爆速にすることができた。

/etc/profile の変更内容を見てもらえば分かるとおり、自分の環境で git-bash.exe を起動した時に上手く動きさえすれば良い、というノリで滅茶苦茶にコードを削っている。/etc/profile の内容は別環境には共用できないし、まだ遭遇していないだけで思いもよらぬ不具合が怒るかもしれない。ご利用は計画的に。

今回の改修および調査結果は、全て以下のリポジトリに置いているので、GitBash の起動を爆速化してみたい人は、参考にしてほしい。

その他参考

Windowsで使えるUNIX環境 Cygwin徹底入門

Windowsで使えるUNIX環境 Cygwin徹底入門

Cygwin環境構築ガイド

Cygwin環境構築ガイド

初心者のためのCygwin入門―「インストール」「基本操作」から「サーバ構築」まで (I・O BOOKS)

初心者のためのCygwin入門―「インストール」「基本操作」から「サーバ構築」まで (I・O BOOKS)

Cygwinコンパクトリファレンス (Compact reference)

Cygwinコンパクトリファレンス (Compact reference)