これは「TeX & LaTeX Advent Caleandar 2023」の1日目の記事です。
(2日目は h20y6m さんです。)
皆さん、“最強のLaTeXエンジン”であるLuaLaTeXは使っていますか? 日本でもLuaLaTeXの普及が着々と進んでいるようですが、その一方で「遅いとは聞いていたが、実際に使うとやっぱり遅すぎて使えない」という人もそれなりにいるようです。高機能であるLuaLaTeXがレガシーなLaTeXより遅いのはある意味当然で、実際フツーの日本語文書に関して言うと、pLaTeX+dvipdfmxのワークフローと比較すると大体2~3倍くらいの時間がかかります。
腕に覚えがある人の中には「遅いLuaLaTeXをどうにかして速くしよう」と試みる人もありますが、なかなか成果は出せていないようです。そもそも「LaTeXカーネルを作っている人はみんな(TeX的に)凄腕の人ばかり」なので、もし高速化の余地が存在するのならそれはカーネルで採用されて標準になっていることでしょう1。
一方で見過ごされがちなのが、「環境構築や文書ビルドの際の手順ミスのせいでLuaLaTeXによるビルドが遅くなっている」という可能性です。実際、ネット上に公開されている記事を見た感じではフツーよりも遅くなるようなアレな設定を使っている事例をそれなりに見かけます2。
本記事では、LuaLaTeXのビルドがフツーよりも遅くなるようなアレな設定パターンとそれに対する改善策について解説します。LuaLaTeXが遅くなる原因を解消して幸せになりましょう!
「タイプセットの回数が多すぎ」パターン
相互参照などの“チョット複雑な機能”を使ったLaTeX文書をビルド3するときには「2回タイプセット(コンパイル)する」必要があります。このことはLaTeXの中級者にとっては常識でしょう。
ところが、誤解している人も多いのですが、“2回タイプセットする”機能を使っている文書だからといって「その文書の編集作業中に常に2回タイプセットする必要がある」わけではありません。数行の文を加筆するくらいの軽微な変更であれば大抵は1回で済みます。つまり、“LaTeXとして正しい”フツーのビルド手順としては飽くまでタイプセットの回数は “必要なだけ” 実行するべきなのです。
イマドキのLaTeXの使用では「もう1回タイプセットする必要があるのか」をユーザが判断する4のは難しい(と見なされるようになっている)ので、少なくとも複雑な文書を扱うレベルにおいてはLaTeX用のビルドツール(Latexmkやllmkなど)を導入して“適切なビルド手順を自動で行う”のが常套手段になっています。
ただし「編集途中のビルドでタイプセットの回数が足りない」のは実用上問題にならないことが多いので、LaTeXに慣れている人なら「編集途中のビルドではタイプセットを1回しか行わない」という方針をとることも実際にはよくあります(自分もよくやります)。この場合「編集作業が終わって最終成果物を得る」段階において“十分な回数”のタイプセットを行うことになります。
どの手段を使うにせよ、多くの場合に編集途中でのビルドにおいては前回からの変更は軽微なものに留まるため、フツーのビルド手順を使っている場合はタイプセットの回数は「平均するとほぼ1回」になるはずです。
ところが実際には、ビルド手順がアレであるため「ビルドごとに常に2回(以上)のタイプセットが行われてしまう」ことがよくあります。当然、これではビルドの所要時間がフツーの設定と比較して平均して2倍(以上)に跳ね上がってしまいます。タイプセットが元々遅いLuaLaTeXだとこれは致命的でしょう。
もちろん「タイプセットを余計に行っているせいでビルドが必要以上に遅くなる」という問題はLuaLaTeXに限った話ではなく、(u)pLaTeXなどの他のエンジンでも全く同じ話が成り立ちます。しかしタイプセットが遅いLuaLaTeXについては「余計なタイプセットを減らす」ことの優先度が他のエンジンよりも高いと考えられます。
ビルド手順がアレかどうか診断する
自分のビルド手順がここで述べたアレパターンに当てはまっているかチェックするための特殊な文書ファイルを用意しました。
% 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を「いつも通りにビルド」することで「ビルド時に何回タイプセットしているか」を調べられるわけです。
ビルド手順で「PDFファイルを出力するディレクトリ」を別に指定している場合は、判定用ファイルもそちらのディレクトリに作られる可能性があります。
早速、以下の手順を実行してチェックしてみましょう。
-
check.tex
ファイルを「いつも通りにビルド」する。 - 判定用ファイル(
check-‹整数値›.zrchk
)の個数を数える。 - 4行目の
{\TeX}
をLua{\TeX}
に変えて再び「いつも通りにビルド」する。 - 判定用ファイルが増えた個数を数える。
もし「いつも通りにビルド」する手順として「編集途中用のビルド」と「最終出力用のビルド」の2種類を使い分けている場合は、上記のチェックにおいては(2回とも)「編集途中用のビルド」を行ってください。
ビルド手順がフツーである場合は手順4で増えた判定ファイルは1個であるはずです(手順3のcheck.tex
の変更は軽微なので)。2個以上判定ファイルが増えた場合はアレだということなります。
以降では、ビルド手順がアレになる原因とその対策を説明します。
原因①:愚直に2回実行している
もし「いつも通りにビルド」が指している手順が
lualatex ‹ファイル名›.tex
lualatex ‹ファイル名›.tex
のように手動で愚直に「常に2回タイプセットする」ことなのであれば、まさにそれはアレです。
エディタの自動ビルドの機能を利用していてもビルドツールを導入していない場合はビルド手順が結局「常に2回タイプセットする」になっていることが多くあります。例えば、VSCodeのLaTeX Workshopで以下のようなレシピ設定を使っている場合が該当します。
"latex-workshop.latex.recipes": [
{
"name": "lualatex ×2", // 名前が既にアレ
"tools": [ // 常に2回実行している!
"lualatex",
"lualatex"
]
},
// ……他の設定
]
「常に2回タイプセットする」と設定されていれば当然「常に2回タイプセットする」わけで、そのビルド手順はアレです。
フツーにする方法
「常に2回タイプセットする」をやっている限りは永遠にアレなので、ビルド手順を変更しましょう。
正攻法はLatexmkなどのビルドツールを導入してビルド手順をビルドツールを利用したもの(“必要な回数”だけタイプセットする)に変更することです。もし現状のビルド手順が冒頭に述べたように「本当にlualatex
を2回実行しているだけで他のこと(bibtex
とかmakeindex
とか)はしていない」のであれば、変更は比較的容易です。ビルド手順として代わりに次のコマンドを使いましょう。
latexmk -lualatex ‹ファイル名›.tex
もし現状の手順でlualatex
に-shell-escape
や-halt-on-error
等のオプションが付いている場合は、latexmk
のオプションにそれを追加してください。
元のビルド手順が複雑である場合はLatexmkを用いた手順を自分で構築する必要があります。残念ながら、現状ではその方法を手短に解説した資料が存在しない5ため、これはかなり難しい作業になります
比較的簡単な次善策として「『編集途中用のビルド』と『最終出力用のビルド』を使い分ける」という方法もあります。先に述べた通り、編集途中でのビルドはタイプセットが少なくてもあまり問題にならないので、「編集途中用のビルドはタイプセットを1回にする」とすれば問題を解決できます。その上で「最終出力用には従来のビルド手順(2回タイプセット)を使う」ことにすればよいわけです。
原因②:中間ファイルを常に削除している
Latexmkなどのビルドツールを利用している場合は、本来は「“必要なだけ”タイプセットする」はずなので、軽微な修正で2回目のタイプセットが行われる場合は何かビルド手順に問題があります。
よくある原因は「タイプセットにより生成される中間ファイル(*.aux
などのファイル)を毎回のビルドの後に常に削除してしまっている」ということです。編集途中でのビルドが「1回タイプセット」で済むのは前回のビルド時の中間ファイルが残っているからであって、中間ファイルを常に消してしまうのではこのメリットも消えてしまいます。
自分のビルド手順が「中間ファイルを常に削除する」かは先のcheck.tex
を用いたチェック手順で簡単に判断できます。「いつも通りにビルド」した後にcheck.aux
がどこにも存在しなければ、そのビルド手順はアレです。
フツーにする方法
「中間ファイルを常に削除する」のはやめましょう。「中間ファイルを削除する(クリーンアップ)」のは別の手順として分離して、必要なとき(最終出力後、あるいは一時的に編集作業を中断してディレクトリを綺麗にしておきたい場合)にだけ実行するようにしましょう。
つまり、編集途中の状況では作業ディレクトリが多数の中間ファイルで“散らかった”状態になるわけですが、LaTeXではそれがフツーのことであると理解してください。もしどうしても「編集途中の状態でも綺麗にしておきたいので、ビルドが遅くなるのは止むをえない」と考えるのであれば、それでもかまわないでしょう。
「ビルドが遅くなるのは避けたいので中間ファイルは残したいが、作業ディレクトリが“散らかる”のも避けたい」という場合は、「ビルドをテンポラリディレクトリで実行する」機能をもつビルドツールである「ClutTeX」(TeX Liveに収録済)を使うという手段もあります。
「中間ファイルを削除する」のに使われる設定・コマンドには以下のようなものがあります。
-
Latexmkでは以下のコマンド:
latexmk -c ‹ファイル名›.tex
-
llmkでは以下のコマンド:
llmk -c ‹ファイル名›
-
VSCodeのLaTeX Workshopでは以下の設定項目:6
(設定画面)
(setting.json)
"latex-workshop.latex.autoClean.run": "onBuilt"
ビルドツールのコマンドを使っている場合は、ビルド手順からはそのコマンドを外しましょう。そのコマンドだけ実行する“クリーンアップ手順”を別に用意しておくとよいでしょう。
LaTeX Workshopを使っている場合は、当該の設定項目の値をonFailed
に変更しましょう7。
"latex-workshop.latex.autoClean.run": "onFailed"
また、左サイドパネルのLaTeX Workshop用メニューから「Clean up auxiliary files」を選ぶ8ことでクリーンアップを単体で実行することができます。
「Dockerでキャッシュが効いてない」パターン
最近ではLaTeXの環境構築をDocker(などのコンテナ環境)でやっている人も多くなってきました。ところが、LuaLaTeXはOpenTypeフォント周りの処理に関してキャッシュファイルを多用しています(当然キャッシュが効くことを前提にしている)。このため、DockerでLuaLaTeXをフツーに使おうとすると追加の設定が必要になり、他のエンジンと比べて手順が少し複雑になります。これがうまくいっていないとLuaLaTeXがフツーよりも遅くなってしまいます。
この件については以下の記事に詳細が述べられていまので、そちらを参照してください。
アドベントカレンダー最終日の記事として「Dockerでキャッシュが効いてない」問題に対策するための “裏技” を紹介した記事を書きました。
まとめ
これまで“フツーよりも遅い”LuaLaTeXを使っていた人は、フツーのLuaLaTeXにチェンジして幸せになりましょう!
-
例えば、かつて「いつも読み込まれるコードを読み込んだ状態のフォーマットを作る」という方法を試した人がいましたが、LuaTeXエンジンの制限(Lua処理系の状態はダンプできない)のためこの方法が適用できる範囲は「expl3の実装やUnicode関連データなど」に限られます。それでも多少の効果はあるので、LaTeXカーネルの2020-02-02の更新で当該コードがフォーマットに含まれるようになりました。(つまりこの方法での高速化の余地はほとんど残っていません。) ↩
-
もちろん、何らかの(速度以外の)メリットを得るために敢えてそれを選択しているというケースもあるでしょう。 ↩
-
LaTeXエンジンのコマンド(
lualatex
など)を実行することを「タイプセット(typeset)」(または「コンパイル(compile)」)と呼びます。これに対して「現在のLaTeX文書のソースファイル群に対応するPDF文書を得るために一連の(一般的に複数の)コマンドを実行する」ことを「ビルド(build)」と呼びます。 ↩ -
Label(s) may have changed. Rerun to get cross-references right.
のような警告メッセージが端末に出力されるのでそれを見て判断する必要があるのですが、パッケージが出力するものも含めるとパターンは限りなくあるのでユーザが判断するのは極めて困難です。 ↩ -
頼みの綱であるTeX Wikiが、この件に限っては全く機能しないのですよね……。Latexmkの公式ドキュメントは元々読みにくいうえにTeXシステムに関する事前知識をかなり要求するので今の用途では推奨できません……。 ↩
-
latex.autoClean.run
の設定は全てのレシピに適用される(編集途中用と最終出力用で分けることができない)ので使える場面が本来はかなり限られるはずです。だけどネット上ではこの設定にしている人をなぜかよく見かけます…… ↩ -
onFailed
は「ビルドが失敗したときにだけ中間ファイルを削除する」という設定です。中間ファイルが壊れているせいでタイプセットが失敗することが時々あるため、ビルド失敗時にクリーンアップするのは有効な解決手段の一つです。従って一般的にはnever
よりはonFailed
の方が推奨されます。 ↩ -
このコマンドの名前は
LaTeX Workshop: Clean up auxiliary files
です。既定設定ではCTRL+ALT+C
のショートカットキーが割り当てられています。 ↩