重点解説！ LuaLaTeXを遅くしない方法

:::note info
これは「[TeX & LaTeX Advent Caleandar 2023](https://adventar.org/calendars/9305)」の1日目の記事です。
（2日目は [h20y6m さん](https://h20y6m.github.io/posts/2023-12-02-texadvent2023/)です。）
:::

皆さん、“最強のLaTeXエンジン”であるLuaLaTeXは使っていますか？ 日本でもLuaLaTeXの普及が着々と進んでいるようですが、その一方で「遅いとは聞いていたが、実際に使うと**やっぱり遅すぎて**使えない」という人もそれなりにいるようです。高機能であるLuaLaTeXがレガシーなLaTeXより遅いのはある意味当然で、実際フツーの日本語文書に関して言うと、pLaTeX＋dvipdfmxのワークフローと比較すると大体2～3倍くらいの時間がかかります。

腕に覚えがある人の中には「遅いLuaLaTeXをどうにかして**速くしよう**」と試みる人もありますが、なかなか成果は出せていないようです。そもそも「LaTeXカーネルを作っている人はみんな（TeX的に:innocent:）凄腕の人ばかり」なので、もし高速化の余地が存在するのならそれはカーネルで採用されて標準になっていることでしょう[^kernel]。

[^kernel]: 例えば、かつて「いつも読み込まれるコードを読み込んだ状態のフォーマットを作る」という方法を試した人がいましたが、LuaTeXエンジンの制限（Lua処理系の状態はダンプできない）のためこの方法が適用できる範囲は「expl3の実装やUnicode関連データなど」に限られます。それでも多少の効果はあるので、LaTeXカーネルの2020-02-02の更新で当該コードがフォーマットに含まれるようになりました。（つまりこの方法での高速化の余地はほとんど残っていません。）

一方で見過ごされがちなのが、「環境構築や文書ビルドの際の**手順ミス**のせいでLuaLaTeXによるビルドが**遅くなって**いる」という可能性です。実際、ネット上に公開されている記事を見た感じではフツーよりも遅くなるような**アレ**な設定を使っている事例をそれなりに見かけます[^reason]。

[^reason]: もちろん、何らかの（速度以外の）メリットを得るために敢えてそれを選択しているというケースもあるでしょう。

本記事では、LuaLaTeXのビルドが**フツーよりも遅くなる**ような**アレ**な設定パターンとそれに対する改善策について解説します。LuaLaTeXが遅くなる原因を解消して**幸せ**:blush:になりましょう！

## 「タイプセットの回数が多すぎ」パターン

相互参照などの“チョット複雑な機能”を使ったLaTeX文書をビルド[^build]するときには「**2回タイプセット**（コンパイル）する」必要があります。このことはLaTeXの中級者にとっては常識でしょう。

[^build]: LaTeXエンジンのコマンド（`lualatex`など）を実行することを**​「タイプセット(typeset)」​**（または「コンパイル(compile)」）と呼びます。これに対して「現在のLaTeX文書のソースファイル群に対応するPDF文書を得るために一連の（一般的に複数の）コマンドを実行する」ことを**​「ビルド(build)」​**と呼びます。

ところが、誤解している人も多いのですが、“2回タイプセットする”機能を使っている文書だからといって「その文書の編集作業中に**常に**2回タイプセットする必要がある」わけでは**ありません**。数行の文を加筆するくらいの軽微な変更であれば大抵は1回で済みます。つまり、“LaTeXとして正しい”フツーのビルド手順としては飽くまでタイプセットの回数は **“必要なだけ”** 実行するべきなのです。

イマドキのLaTeXの使用では「もう1回タイプセットする必要があるのか」をユーザが判断する[^decide]のは難しい（と見なされるようになっている）ので、少なくとも複雑な文書を扱うレベルにおいてはLaTeX用の**ビルドツール**（Latexmkや[llmk]など）を導入して“適切なビルド手順を自動で行う”のが常套手段になっています。

[^decide]: `Label(s) may have changed. Rerun to get cross-references right.`のような警告メッセージが端末に出力されるのでそれを見て判断する必要があるのですが、パッケージが出力するものも含めるとパターンは限りなくあるのでユーザが判断するのは極めて困難です。

:::note info
ただし「編集途中のビルドでタイプセットの回数が足りない」のは実用上問題にならないことが多いので、LaTeXに慣れている人なら「編集途中のビルドではタイプセットを1回しか行わない」という方針をとることも実際にはよくあります（自分もよくやります）。この場合「編集作業が終わって最終成果物を得る」段階において“十分な回数”のタイプセットを行うことになります。
:::

どの手段を使うにせよ、多くの場合に編集途中でのビルドにおいては前回からの変更は軽微なものに留まるため、フツーのビルド手順を使っている場合はタイプセットの回数は「平均すると**ほぼ1回**」になるはずです。

ところが実際には、ビルド手順が**アレ**:sushi:であるため「ビルドごとに**常に2回**（以上）のタイプセットが行われてしまう」ことがよくあります。当然、これではビルドの所要時間がフツーの設定と比較して平均して2倍（以上）に跳ね上がってしまいます。タイプセットが元々遅いLuaLaTeXだとこれは致命的でしょう。

:::note info
もちろん「タイプセットを余計に行っているせいでビルドが必要以上に遅くなる」という問題はLuaLaTeXに限った話ではなく、(u)pLaTeXなどの他のエンジンでも全く同じ話が成り立ちます。しかしタイプセットが遅いLuaLaTeXについては「余計なタイプセットを減らす」ことの優先度が他のエンジンよりも高いと考えられます。
:::

### ビルド手順がアレかどうか診断する

自分のビルド手順がここで述べた**アレ**:sushi:パターンに当てはまっているかチェックするための特殊な文書ファイルを用意しました。

```tex:check.tex
% for LuaLaTeX
\documentclass{article}
\begin{document}
Hello {\TeX} world!
\directlua{
  local ts = math.floor(os.gettimeofday()*1000)
  local f = assert(io.open('./\jobname-'..ts..'.zrchk','wb'))
  f:write('OK')
  f:close()
}
\end{document}
```

この`check.tex`をLuaLaTeXでタイプセットすると、`check-‹整数値›.zrchk`という形の名前の判定用ファイルが1つ作成されます。check.texを「いつも通りにビルド」することで「ビルド時に何回タイプセットしているか」を調べられるわけです。

:::note info
ビルド手順で「PDFファイルを出力するディレクトリ」を別に指定している場合は、判定用ファイルもそちらのディレクトリに作られる可能性があります。
:::

早速、以下の手順を実行してチェックしてみましょう。

 1. `check.tex`ファイルを「いつも通りにビルド」する。
 2. 判定用ファイル（`check-‹整数値›.zrchk`）の個数を数える。
 3. 4行目の`{\TeX}`を`Lua{\TeX}`に変えて再び「いつも通りにビルド」する。
 4. 判定用ファイルが**増えた個数**を数える。

:::note info
もし「いつも通りにビルド」する手順として「編集途中用のビルド」と「最終出力用のビルド」の2種類を使い分けている場合は、上記のチェックにおいては（2回とも）「編集途中用のビルド」を行ってください。
:::

ビルド手順がフツーである場合は手順4で増えた判定ファイルは**1個**であるはずです（手順3の`check.tex`の変更は軽微なので）。**2個以上**判定ファイルが増えた場合は**アレ**:sushi:だということなります。

以降では、ビルド手順が**アレ**:sushi:になる原因とその対策を説明します。

### 原因①：愚直に2回実行している

もし「いつも通りにビルド」が指している手順が

```bash
lualatex ‹ファイル名›.tex
lualatex ‹ファイル名›.tex
```

のように手動で愚直に「常に2回タイプセットする」ことなのであれば、まさにそれは**アレ**:sushi:です。

エディタの自動ビルドの機能を利用していてもビルドツールを導入して**いない**場合はビルド手順が結局「常に2回タイプセットする」になっていることが多くあります。例えば、VSCodeのLaTeX Workshopで以下のようなレシピ設定を使っている場合が該当します。

```js
"latex-workshop.latex.recipes": [
  {
    "name": "lualatex ×2", // 名前が既にアレ
    "tools": [ // 常に2回実行している!
      "lualatex",
      "lualatex"
    ]
  },
  // ……他の設定
]
```

「常に2回タイプセットする」と設定されていれば当然「常に2回タイプセットする」わけで、そのビルド手順は**アレ**:sushi:です。

#### フツーにする方法

「常に2回タイプセットする」をやっている限りは永遠に**アレ**:sushi:なので、ビルド手順を変更しましょう。

正攻法はLatexmkなどの**ビルドツールを導入**してビルド手順をビルドツールを利用したもの（“必要な回数”だけタイプセットする）に変更することです。もし現状のビルド手順が冒頭に述べたように「本当に`lualatex`を2回実行している**だけ**で他のこと（`bibtex`とか`makeindex`とか）はしていない」のであれば、変更は比較的容易です。ビルド手順として代わりに次のコマンドを使いましょう。

```bash
latexmk -lualatex ‹ファイル名›.tex
```

:::note info
もし現状の手順で`lualatex`に`-shell-escape`や`-halt-on-error`等のオプションが付いている場合は、`latexmk`のオプションにそれを追加してください。
:::

元のビルド手順が複雑である場合はLatexmkを用いた**手順を自分で構築する**必要があります。残念ながら、現状ではその方法を手短に解説した資料が存在しない[^latexmk]ため、これはかなり難しい作業になります:cry:

[^latexmk]: 頼みの綱であるTeX Wikiが、この件に限っては全く機能しないのですよね……。Latexmkの公式ドキュメントは元々読みにくいうえにTeXシステムに関する事前知識をかなり要求するので今の用途では推奨できません……。

比較的簡単な次善策として**​「『編集途中用のビルド』と『最終出力用のビルド』を使い分ける」​**という方法もあります。先に述べた通り、編集途中でのビルドはタイプセットが少なくてもあまり問題にならないので、「編集途中用のビルドはタイプセットを1回にする」とすれば問題を解決できます。その上で「最終出力用には従来のビルド手順（2回タイプセット）を使う」ことにすればよいわけです。

### 原因②：中間ファイルを常に削除している

Latexmkなどのビルドツールを利用している場合は、本来は「“必要なだけ”タイプセットする」はずなので、軽微な修正で2回目のタイプセットが行われる場合は何かビルド手順に問題があります。

よくある原因は「タイプセットにより生成される**中間ファイル**（`*.aux`などのファイル）を**毎回のビルドの後に常に削除してしまっている**」ということです。編集途中でのビルドが「1回タイプセット」で済むのは前回のビルド時の中間ファイルが残っているからであって、中間ファイルを常に消してしまうのではこのメリットも消えてしまいます。

自分のビルド手順が「中間ファイルを常に削除する」かは先の`check.tex`を用いたチェック手順で簡単に判断できます。「いつも通りにビルド」した後に`check.aux`がどこにも存在しなければ、そのビルド手順は**アレ**:sushi:です。

#### フツーにする方法

「中間ファイルを常に削除する」のはやめましょう。「中間ファイルを削除する（クリーンアップ）」のは**別の手順として分離**して、必要なとき（最終出力後、あるいは一時的に編集作業を中断してディレクトリを綺麗にしておきたい場合）にだけ実行するようにしましょう。

:::note warn
つまり、編集途中の状況では作業ディレクトリが多数の中間ファイルで“散らかった”状態になるわけですが、LaTeXでは**それがフツー**のことであると理解してください。もしどうしても「編集途中の状態でも綺麗にしておきたいので、ビルドが遅くなるのは止むをえない」と考えるのであれば、それでもかまわないでしょう。

「ビルドが遅くなるのは避けたいので中間ファイルは残したいが、作業ディレクトリが“散らかる”のも避けたい」という場合は、「ビルドをテンポラリディレクトリで実行する」機能をもつビルドツールである**​「[ClutTeX][cluttex]」​**（TeX Liveに収録済）を使うという手段もあります。
:::

「中間ファイルを削除する」のに使われる設定・コマンドには以下のようなものがあります。

  * Latexmkでは以下のコマンド：

    ```bash
    latexmk -c ‹ファイル名›.tex
    ```

  * llmkでは以下のコマンド：

    ```bash
    llmk -c ‹ファイル名›
    ```

  * VSCodeのLaTeX Workshopでは以下の設定項目：[^autoclean]

    (設定画面)

    ![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/e4ba1a83-3da2-1f57-8b6b-c1740c57ee3c.png)


    (setting.json)

    ```js
    "latex-workshop.latex.autoClean.run": "onBuilt"
    ```

[^autoclean]: `latex.autoClean.run`の設定は全てのレシピに適用される（編集途中用と最終出力用で分けることができない）ので使える場面が本来はかなり限られるはずです。だけどネット上ではこの設定にしている人をなぜかよく見かけます……:astonished:

ビルドツールのコマンドを使っている場合は、ビルド手順からはそのコマンドを外しましょう。そのコマンドだけ実行する“クリーンアップ手順”を別に用意しておくとよいでしょう。

LaTeX Workshopを使っている場合は、当該の設定項目の値を`onFailed`に変更しましょう[^onfailed]。

[^onfailed]: `onFailed`は「ビルドが失敗したときにだけ中間ファイルを削除する」という設定です。中間ファイルが壊れているせいでタイプセットが失敗することが時々あるため、ビルド失敗時にクリーンアップするのは有効な解決手段の一つです。従って一般的には`never`よりは`onFailed`の方が推奨されます。

```js
"latex-workshop.latex.autoClean.run": "onFailed"
```

また、左サイドパネルのLaTeX Workshop用メニューから「Clean up auxiliary files」を選ぶ[^cleanup]ことでクリーンアップを単体で実行することができます。

[^cleanup]: このコマンドの名前は`LaTeX Workshop: Clean up auxiliary files`です。既定設定では`CTRL+ALT+C`のショートカットキーが割り当てられています。

![image-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/14550/5a297a6c-08d2-d0e3-d5b3-fa8617d36d91.png)

## 「Dockerでキャッシュが効いてない」パターン

最近ではLaTeXの環境構築をDocker（などのコンテナ環境）でやっている人も多くなってきました。ところが、LuaLaTeXはOpenTypeフォント周りの処理に関してキャッシュファイルを多用しています（当然キャッシュが効くことを前提にしている）。このため、DockerでLuaLaTeXをフツーに使おうとすると追加の設定が必要になり、他のエンジンと比べて手順が少し複雑になります。これがうまくいっていないとLuaLaTeXがフツーよりも遅くなってしまいます。

この件については以下の記事に詳細が述べられていまので、そちらを参照してください。

  * [TeX Live 2019 + 原ノ味フォント + llmk の Docker コンテナを作る][doratex]（Qiita/@doraTeX）

:::note info
アドベントカレンダー最終日の記事として「Dockerでキャッシュが効いてない」問題に対策するための **“裏技”** を紹介した記事を書きました。

* [重点解説！ “DockerでLuaLaTeX”を遅くしない方法][zr-new]

:::

## まとめ

これまで“フツーよりも遅い”LuaLaTeXを使っていた人は、フツーのLuaLaTeXにチェンジして**幸せ**:blush:になりましょう！:information_desk_person:

[cluttex]: https://github.com/minoki/cluttex
[doratex]: https://qiita.com/doraTeX/items/f40f9597763ca63daabb#luatex-%E3%81%AE%E5%88%9D%E5%9B%9E%E3%83%95%E3%82%A9%E3%83%B3%E3%83%88%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E4%BD%9C%E6%88%90%E3%81%8C%E9%81%85%E3%81%84%E5%95%8F%E9%A1%8C
[llmk]: https://github.com/wtsnjp/llmk
[zr-new]: https://qiita.com/zr_tex8r/items/c1ec09f4fa3a39f5e365