この記事は TeX & LaTeX Advent Calendar 2021 の6日目の記事です。5日目も 自分の記事でした。8日目は zr_tex8r さんです。
TikZ を用いて,しかも \usetikzlibrary
を用いて様々なライブラリをロードしまくったりすると,コンパイル時間がどんどん長くなりますよね。特に,TikZ で「座標を目分量で微調整」などしているときなんかだと,作業時間の大半を「コンパイルの待ち時間」が占めることになります。
そのような場合,(u)pLaTeX であれば,mylatexformatパッケージを用いてコンパイル時間を短縮する手が考えられます。これは,文書のプリアンブル部をフォーマットファイルとしてダンプすることで,これを“キャッシュ”として利用しようというものです。
使い方の基本
ここでは,次の test1.tex
というファイルを upLaTeX でコンパイルしたいものとします。
\documentclass[dvipdfmx]{jlreq} % プリアンブル部でいろいろ大量にロード → 時間がかかる \usepackage{tikz} \usetikzlibrary{calc,shadings,shadows,shadows.blur,fadings,scopes,calc,arrows,shapes,shapes.symbols,patterns,backgrounds,positioning,fit,graphs,petri,intersections,through,trees,turtle,spy,mindmap,calendar,plotmarks,decorations,decorations.text,decorations.pathmorphing,decorations.pathreplacing,decorations.markings,decorations.fractals,decorations.shapes} \usepackage{tcolorbox} \tcbuselibrary{xparse,breakable,documentation,external,fitting,hooks,listings,magazine,poster,raster,skins,theorems,vignette} \begin{document} \begin{tcolorbox} ほげ \end{tcolorbox} \end{document}
1. フォーマットファイル作成
まず,test1.tex
のプリアンブル部をロードさせたフォーマットファイルを,“キャッシュ”として生成します。
$ uplatex -ini -jobname="cache" \&uplatex mylatexformat.ltx test1
これにより,upLaTeXエンジンがINIモードで起動し,cache.fmt
というフォーマットファイルが生成されます。このファイルには,プリアンブル部の展開が終わった状態がダンプされて保存されています。
2. 独自フォーマットファイルを利用してコンパイル
次に,cache.fmt
をロードして test1.tex
コンパイルします。
$ uplatex \&cache test1 $ dvipdfmx test1
すると,プリアンブル部の展開が終わった状態からスタートし,\begin{document}
以降から解釈が始まります。プリアンブル部の展開が既に終わっている状態からスタートするので,コンパイル時間がかなり短縮されます。
注意点
プリアンブル部の内容は,フォーマットファイルを生成した時点の内容で固定されます。つまり,cache.fmt
を作成後に test1.tex
のプリアンブル部の内容を変更しても,その変更は uplatex \&cache test1
によるコンパイル時には反映されません。プリアンブル部を修正したときは,cache.fmt
を作り直す必要があります。
プリアンブル部の途中までしかキャッシュさせない方法
プリアンブル部の内容のうち,\usepackage{tikz}
の展開結果などは,TikZ本体のパッケージのアップデートが起こらない限り内容に変化はないわけですが,自作マクロなどは頻繁に内容の変化が起こることでしょう。そういうときのためには,「ここから先は“キャッシュ生成”の対象としない」とする方法があります。
次の test2.tex
を用意します。
\documentclass[dvipdfmx]{jlreq} \usepackage{tikz} \usetikzlibrary{calc,shadings,shadows,shadows.blur,fadings,scopes,calc,arrows,shapes,shapes.symbols,patterns,backgrounds,positioning,fit,graphs,petri,intersections,through,trees,turtle,spy,mindmap,calendar,plotmarks,decorations,decorations.text,decorations.pathmorphing,decorations.pathreplacing,decorations.markings,decorations.fractals,decorations.shapes} \usepackage{tcolorbox} \tcbuselibrary{xparse,breakable,documentation,external,fitting,hooks,listings,magazine,poster,raster,skins,theorems,vignette} \endofdump % ここから下は“キャッシュ生成”の対象外 \def\mymacro{ほげ} \begin{document} \begin{tcolorbox} \mymacro \end{tcolorbox} \end{document}
これに対して
$ uplatex -ini -jobname="cache" \&uplatex mylatexformat.ltx test2 $ uplatex \&cache test2 $ dvipdfmx test2
とすれば,cache.fmt
の中に \def\mymacro{ほげ}
の部分は含まれません。その内容を変更すると,コンパイルのたびに変更が正しく反映されます。
注意点
\endofdump
という命令は,mylatexformat.ltx
の中で定義されています。それゆえ,この test2.tex
を,mylatexformat
の仕組みを介さずに普通にコンパイルすると,コンパイルエラーとなってしまいます。
$ uplatex test2
...
! Undefined control sequence.
l.8 \endofdump
これに対する解決策としては,次のような方法が考えられます。
対策1:\csname...\endcsname
を使う
\csname...\endcsname
を使った場合,未定義な命令名の場合無視される(正確にはそれが \relax
と等価に定義される)ことを利用します。\endofdump
の代わりに \csname endofdump\endcsname
と書いておけば,mylatexformat.ltx
を介さずにコンパイルしたときには \csname endofdump\endcsname
はエラーにならず無視されます。
対策2:プリアンブル内で \ifdefined
による定義チェックをする
プリアンブルの頭の方に
\ifdefined\endofdump\else \let\endofdump\relax \fi
というコードを仕込んでおき,mylatexformat.ltx
が読まれていないときには \endofdump
を \relax
に定義しておくという手もアリです。
対策3:コマンドラインから \endofdump
の定義を与える
mylatexformat.ltx
なしでコンパイルするときには
$ uplatex "\let\endofdump\relax\input{test2}"
などとすることで,\let\endofdump\relax
を効かせた状態で test2.tex
をコンパイルする手もあります。
スクリプトで自動化する
一連の作業を自動化するための,ptex2pdf
風の Bash スクリプトを作りました。
使い方
コンパイル対象となるファイルを test.tex
としましょう。
$ ptex2pdf-fmt.sh test
とすれば,次のように働きます。
test.fmt
が存在していなければ,test.tex
からtest.fmt
を生成する。- その上で,
test.fmt
を利用してtest.tex
をコンパイル(TeX → DVI → PDF)する。
test.fmt
が存在していればステップ1は省略されるので,2回目からはコンパイル時間が短縮されます。
オプション解説
$ ptex2pdf-fmt.sh -f test
-f
はフォーマットファイル強制再生成モードです。test.fmt
の存在の有無にかかわらず test.fmt
を再生成した上でコンパイルを実行します。test.tex
のプリアンブル(の \endofdump
以前)に変化を加えたときはこれで test.fmt
を再生成するとよいでしょう。
$ ptex2pdf-fmt.sh -i test
-i
はフォーマットファイル無視モードです。mylatexformat
を使わず,普通にコンパイルします。この際,内部的に \let\endofdump\relax\input{test}
として呼び出すので,ソース中で裸の \endofdump
が使われていてもエラーにはなりません。
その他,次のオプション達は ptex2pdf
と同様に働きます。
-u
: pLaTeX ではなく upLaTeX を使ってコンパイルします。-ot "..."
: (u)pLaTeX エンジンに対してオプションとして...
の内容を渡します。-od "..."
: dvipdfmx に対してオプションとして...
の内容を渡します。-output-directory ...
: 出力ファイルのディレクトリを...
に指定します。-s
: DVIを生成した段階で終了します。
たとえば次のように使います。
$ ptex2pdf-fmt.sh -u -ot "-synctex=1 -recorder" -od "-V 5" --output-directory "./work" test
これは,
- upLaTeX を使用(
-u
) uplatex
コマンドのオプションに-synctex=1 -recorder
を指定dvipdfmx
コマンドのオプションに-V 5
を指定./work
に出力
という意味です。
既知の制約事項
- フォーマットファイルは常にカレントディレクトリに出力されるようになっています(
\&format
で参照できるように)。 - フォーマットファイルのファイル名には半角スペースを含められないようなので,TeXソースファイル名にも半角スペースを含めないでください。
-ot
のオプションに-jobname="hoge"
で,元ファイルと異なる jobname を指定すると,後半のdvipdfmx
で失敗します(これはオリジナルのptex2pdf
も同様です)。
実験
実験1
mylatexformat
の威力を調べるために,あえてコンパイルの遅い環境を用意しました。
- Raspberry Pi 4 Model B (RAM 4GB)
- Raspbian (32bit)
- TeX Live 2021
の環境で,先程の test2.tex
のコンパイル(TeX → DVI のみ)を10回繰り返したときの所要時間の平均値を取ってみました。
結果
コンパイル待ち時間が約1/4倍に短縮されました!
実験2
実際に業務で用いているTeX環境の場合,色々なパッケージの読み込み,種々の独自命令の定義が大量になされているため,コンパイルの待ち時間のうちプリアンブル部のロードがかなりの時間を占めています。そのスタイルをロードして,同様に TeX→DVI をmylatexformat
のあるなしそれぞれで10回繰り返して所要時間の平均値を取ってみました。
結果
コンパイル待ち時間が約1/6倍に短縮されました! 劇的な効果を発揮しています。
実際には,TeX文書からPDFを生成する段階を
- プリアンブル部のロード
- 本文の組版
- DVI → PDF
の3段階に分けると,mylatexformat
で待ち時間短縮効果が発揮できるのは 1. の部分のみであり,本文が長くなって 2., 3. が占める比率が増えてくると,相対的にmylatexformat
の待ち時間短縮効果は少なくなってくるわけですが,それでも相当な効率化につながるとは言えるでしょう。
LuaLaTeX ではどうか?
(u)pLaTeX と比べてコンパイルが遅い LuaLaTeX で mylatexformat
が使えれば,さらに威力が絶大になるはず……なのですが,TeX StackExchange の記事によると,Luaの状態やOpenTypeフォントの状態をフォーマットファイルにダンプすることはできないため,LuaLaTeX のコンパイル時間をこの方法で短縮することは困難なようです。残念……。
実装をちょっと読む
mylatexformat.ltx
はどのような仕組みで実装されているのでしょうか。 今年の TeX & LaTeX Advent Calendar 2021 の重点テーマは「TeX言語(とか)しましょう!」ということですので,本記事のおまけとして,最後に mylatexformat.ltx
の実装を冒頭部だけちょっと読んでみましょう。
\begingroup \def\x #1{\endgroup \gdef\begin ##1{\MYLATEX@StopAtdocument{##1}#1} }\expandafter\x\expandafter{\begin{#1}} \def\MYLATEX@StopAtdocument #1{\expandafter \ifx\csname #1\endcsname\document \expandafter\endofdump \fi }% \MYLATEX@StopAtdocument
ここは,\begin
の定義を変更することで,\begin{document}
を \endofdump
に差し替えて,プリアンブル内に \endofdump
が現れないときは \begin{document}
のところで読み込みを打ち切り \dump
を実行することを意図しています。
最初の3行を見やすく整形すると,
\begingroup \def\x#1{% \endgroup \gdef\begin##1{\MYLATEX@StopAtdocument{##1}#1} }% \expandafter\x\expandafter{\begin{#1}}
となります。
- まず
\begingroup
でグループ階層の内側に入る - 引数を1つとるマクロ
\x
を定義 - 定義したばかりの
\x
を早速展開
という動きをしています。
\expandafter\x\expandafter{\begin{#1}}
のところは,
\x{<\begin{#1}の定義内容を展開したもの>}
に展開された上で,\x
の展開に入ります。具体的には,\x
の引数は
\@ifundefined{##1}{\def\reserved@a{\@latex@error{Environment ##1 undefined}\@eha}}{\def\reserved@a{\def\@currenvir{##1}\edef\@currenvline{\on@line}\csname##1\endcsname}}\@ignorefalse\begingroup\@endpefalse\reserved@a
という状態になっています。
次に,\x
を展開すると,
\endgroup \gdef\begin#1{\MYLATEX@StopAtdocument{#1}<\begin{#1}の定義内容を展開したもの>}
となります。まず \endgroup
が実行されてグループ階層から脱出し,先程定義した \x
の定義内容は消え去ります(1回しか使わない使い捨て命令の作り方として上手いですね)。これにより,\begin
の定義の先頭に \MYLATEX@StopAtdocument{#1}
がくっついた形になります。
\MYLATEX@StopAtdocument
の定義は次の形です。
\def\MYLATEX@StopAtdocument#1{% \expandafter\ifx\csname#1\endcsname\document \expandafter\endofdump\fi }
これにより,\MYLATEX@StopAtdocument
は #1
の内容が document
の場合(すなわち \begin{document}
と呼び出された場合)のみ,\endofdump
を呼び出すように働いています。(この \endofdump
の前の \expandafter
って必要なんでしょうか……?🤔)\expandafter\endofdump\fi
は,先の \ifx
の比較が真であった場合に,まず \fi
で \ifx
の中の状態を終結させてから,次に \endofdump
を実行するというための措置です*1。
\endofdump
は,いくつかの準備の上で \dump
を実行し,そこまでの内容でフォーマットファイル作成が実行される,というわけです。