TeX Alchemist Online

TeX のこと,フォントのこと,Mac のこと

mylatexformat を用いてコンパイル時間を短縮しよう!

この記事は TeX & LaTeX Advent Calendar 2021 の6日目の記事です。5日目も 自分の記事でした。8日目は zr_tex8r さんです。

f:id:doraTeX:20211206090515p:plain

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 はエラーにならず無視されます。

hak7a3.hatenablog.com

対策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 スクリプトを作りました。

github.com

使い方

コンパイル対象となるファイルを test.tex としましょう。

$ ptex2pdf-fmt.sh test

とすれば,次のように働きます。

  1. test.fmt が存在していなければ,test.tex から test.fmt を生成する。
  2. その上で,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回繰り返したときの所要時間の平均値を取ってみました。

結果

f:id:doraTeX:20211206022338p:plain

コンパイル待ち時間が約1/4倍に短縮されました!

実験2

実際に業務で用いているTeX環境の場合,色々なパッケージの読み込み,種々の独自命令の定義が大量になされているため,コンパイルの待ち時間のうちプリアンブル部のロードがかなりの時間を占めています。そのスタイルをロードして,同様に TeX→DVI をmylatexformat のあるなしそれぞれで10回繰り返して所要時間の平均値を取ってみました。

結果

f:id:doraTeX:20211206090515p:plain

コンパイル待ち時間が約1/6倍に短縮されました! 劇的な効果を発揮しています。

実際には,TeX文書からPDFを生成する段階を

  1. プリアンブル部のロード
  2. 本文の組版
  3. 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}}

となります。

  1. まず \begingroup でグループ階層の内側に入る
  2. 引数を1つとるマクロ \x を定義
  3. 定義したばかりの \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 を実行し,そこまでの内容でフォーマットファイル作成が実行される,というわけです。

*1:wtsnjpさん,ご指摘ありがとうございます🙇‍♂️