概要
ただしく1${\LaTeX}$を高速化しましょう。
想定読者
${\LaTeX}$を高速化したい人であって
-
Typst
の導入コストに耐えられない人 - ${\LaTeX}$の既存資産を活用したい人2
- ${\LaTeX}$への偏愛がある人
- 意味を孕んだ複雑性が好きな人
想定環境
ステップ一覧
問題点 | 対策 | 新規性 |
---|---|---|
タイプセットを何度も何度も |
latexmk の導入 |
x |
そもそもタイプセットが遅い |
uplatex の使用 |
x |
パッケージ読み込みが重い |
mylatexformat の導入 |
o |
.tex ファイルがでかい |
subfiles の導入 |
x |
第三列から明らかなように、当記事の主題はmylatexformat
の適切な使用法であるが、周辺技術についても記述しておく。
latexmk
の導入
${\TeX}$のあるあるとして参照が切れないように複数回タイプセットがあるが、ちゃんと設定しないと変更がない中間ファイルも複数回生成される。bibtex
やらmakeindex
やらが絡むとなおさらである。latexmk
は中間ファイルを管理・監視して、必要最小限のタイプセットで.pdf
ファイルを生成してくれる。
インストール
TeX Live
に同梱なので省略。
latexmkrc
の設定
探せばいくらでも転がっているが、例。
#!/usr/bin/env perl
# カレントディレクトリ変更
$do_cd = 1;
# uplatexの呼び出し(後で変わる)
$pdf_mode = 3;
$latex = 'uplatex -synctex=1 -file-line-error -halt-on-error %O %S';
$dvipdf = 'dvipdfmx %O -o %D %S';
$max_repeat = 5;
# bibtex系
$bibtex_use=2;
$bibtex = 'upbibtex %O %S';
$biber = 'biber --bblencoding=utf8 -u -U --output_safechars %O %S';
# index
$makeindex = 'upmendex %O -o %D %S -s jpbase';
# ヴューワ
$dvi_previewer = "open %S";
$pdf_previewer = "open %S";
# 出力フォルダ指定
$out_dir = ".";
# 中間ファイルを別フォルダに隠しておける
$emulate_aux = 1;
$aux_dir = ".tex_intermediates";
# 中間ファイル登録
$clean_ext="$clean_ext run.xml";
latexmkの設定[雑多な記録]を参考とした。大まかな要素はそちらを参照していただきたい。以下は補足。
# カレントディレクトリ変更
$do_cd = 1;
latexmk
がタイプセット対象の.tex
ファイルの位置にcd
してくれる。${\TeX}$は変な文字を嫌う(~
など)ので、こうしておくとファイルパス中のダメ文字を拾われずに済む。
顕著な例として、iCloud Drive
の実体は/Users/<ユーザ名>/Library/Mobile Documents/com~apple~CloudDocs/
にあるため、これを入れないと確定で失敗する。
# uplatexの呼び出し
$pdf_mode = 3;
$latex = 'uplatex -synctex=1 -file-line-error -halt-on-error %O %S';
$dvipdf = 'dvipdfmx %O -o %D %S';
$max_repeat = 5;
$latex
で指定したオプションは以下の意図がある。
-
-synctex=1
SyncTeX
機能が有効になり、.synctex.gz
ファイルが生成される。これがあると.pdf
ヴューワと.tex
ヴューワで対応する位置を相互に移れる。LaTeX Workshop
下でも有効なのでつけておくと便利。 -
-file-line-error
エラーメッセージが<ファイル名>:<行番号>:<エラー内容>
になる。LaTeX Workshop
にエラーを認識してもらうのに必要。 -
-halt-on-error
エラーを吐いたら停止する。というか停止せず続行するってなんなんだろうか。-interaction=nonstopmode
使ってる人の気が知れない。
# bibtex系
$bibtex_use=2;
$bibtex = 'upbibtex %O %S';
$biber = 'biber --bblencoding=utf8 -u -U --output_safechars %O %S';
$bibtex_use=2;
を指定すると適宜.bib
ファイルから適宜.bbl
ファイルを生成する。つまり.bbl
ファイルが中間ファイル扱いになる。自前の.bbl
ファイルを使いたい人はファイルが消されないよう$bibtex_use=<0 or 1 or 1.5>;
にしておくべきだが、まぁそんなことは滅多にあるまい。
# index
$makeindex = 'upmendex %O -o %D %S -s jpbase';
upmendex
を用いる。これは索引ツール比較[$\TeX$ Wiki]によればuplatex
で利用可能な最も上位互換なツールのため。
-s jpbase
で索引のフォーマットをjpbase
5に指定している。本当は.tex
ファイル側でフォーマットを指定できるべきだし、実際できるのだが、それをやるには-shell-escape
というやや危ない橋を渡ることになるのでとりあえず一番振る舞いの良いこいつで固定。このあとやるlatexmkrc
改造と似たようなことをすれば変更可能になるはず。
# ヴューワ
$dvi_previewer = "open %S";
$pdf_previewer = "open %S";
latexmk
に-v
オプションを入れた時に使うヴューワを指定するが、LaTeX Workshop
を使っているならそんな機会はまずないだろう。
# 出力フォルダ指定
$out_dir = ".";
# 中間ファイルを別フォルダに隠しておける
$emulate_aux = 1;
$aux_dir = ".tex_intermediates";
出力フォルダの.
はカレントディレクトリを意味し、cd
しているから要するに.tex
ファイルのある位置を意味する。
中間ファイルの出力先を変更する機能は${\TeX}$系のツールには存在しないのだが、latexmk
が適宜動かしてくれる。.tex_intermediates
のように、隠しフォルダに入れておくとメインのフォルダがスッキリ6して良い。
# 中間ファイル登録
$clean_ext="$clean_ext run.xml";
LaTeX Workshop
がプロセス監視の結果として.run.xml
を生成するので、こいつを消去対象の拡張子に加えておく。
LaTeX Workshop
の設定
中間ファイルの管理を全部latexmk
側に押し付けると、LaTeX Workshop
の設定はこのぐらいでいい。
{
// ファイル保存時の実行「レシピ」
"latex-workshop.latex.recipe.default": "latexmk",
"latex-workshop.latex.recipes": [
{
// latexmkを叩くだけのレシピ
"name": "latexmk",
"tools": ["latexmk"]
}
],
// レシピに使われるパーツ
"latex-workshop.latex.tools": [
{
"name": "latexmk",
"command": "latexmk",
"args": [
// "-time"で実行時間を表示してくれる。
"-time",
"%DOC%"
],
}
],
// あとはお好みで
"latex-workshop.intellisense.package.enabled": true,
"latex-workshop.latex.outDir": "",
"latex-workshop.synctex.afterBuild.enabled": true,
"latex-workshop.view.pdf.viewer": "tab",
"latex-workshop.latex.autoBuild.cleanAndRetry.enabled": false,
}
VScodeでLaTex環境を整える際のあれこれを参考にした。
用法
これもいくらでも転がっているが、頻用するのは
latexmk <.texファイル名> # タイプセット
latexmk -c # 中間ファイルだけ削除
latexmk -C # 全出力ファイル削除(.pdfも消える)
ぐらいか。
uplatex
の使用
lualatex
はスクリプトとかかけて便利ですが、普通に遅いのでuplatex
使いましょう。ただしさ7は速さを必ずしも意味しないので。
時間測定
後でやるかも?
mylatexformat
の導入
大規模パッケージを複数入れたりすると、タイプセットのオーバーヘッドがかなりの時間を占めるようになってくる。
実は${\TeX}$にはフォーマットファイルを作っておく機能(イニシャルモード)があるのだが、普通にやると\begin{document}
以降の中身までフォーマット化してしまう。
mylatexformat
パッケージは${\TeX}$のイニシャルモードをハックして、プリアンブル部分だけのフォーマットファイルを作成してくれる。
latexmkrc
の改造
# uplatexの呼び出し
$pdf_mode = 3;
$latex = 'uplatex -synctex=1 -file-line-error -halt-on-error %O %S';
$dvipdf = 'dvipdfmx %O -o %D %S';
$max_repeat = 5;
を
# fmtlatexの呼び出し
$pdf_mode = 3;
$latex = 'internal fmtlatex uplatex %Z %Y %A %S %R -synctex=1 -file-line-error -halt-on-error %O';
$dvipdf = 'dvipdfmx %O -o %D %S';
$max_repeat = 5;
# 作業パス
my $comdir=$ENV{HOME};
my $comname=".latexmk";
my $pwd=`pwd`;
chomp $pwd;
# fmtlatex メインルーチン
{
# 拡張子を登録
$clean_ext="$clean_ext fmt";
my $initial = 1;
sub fmtlatex {
# 引数読込
my ($engine, $outpath, $auxpath, $basename, $texname, $jobname, @args) = @_;
my $options = join(' ', @args);
# 初回実行時
if ($initial == 1){
$initial = 0;
# フォーマット生成フラグ
my $flag = 0;
print "fmtlatex: checking if the preamble changed...\n";
if (&check_preamble_change($auxpath,$jobname,$texname) == 0){
print "fmtlatex: the preamble is not changed.\n";
print "fmtlatex: checking if the common fmt file is owned...\n";
if (&check_com_owned("$pwd/$texname") == 0){
print "fmtlatex: the common fmt file is not owned.\n";
$flag = 1;
}else{
print "fmtlatex: the common fmt file is owned.\n";
}
}else{
print "fmtlatex: the preamble is changed.\n";
$flag = 1;
}
if ($flag == 1){
print "rewriting the common fmt file in ini mode...\n";
# フォーマット生成
my $iniret=Run_subst("$engine -ini $options -output-directory=\"$comdir\" -jobname=\"$comname\" \\\&$engine mylatexformat.ltx $texname");
if($iniret == 0){
print "fmtlatex: the common fmt file rewrited. saving preamble...\n";
&memorize_preamble_change($auxpath,$jobname);
&hold_com("$pwd/$texname");
}else{
print "fmtlatex: failed to rewrite the common fmt file.\n";
&forget_preamble_change($auxpath,$jobname);
&throw_com("$pwd/$texname");
return $iniret;
}
}else{
print "keep the common fmt file.\n";
&forget_preamble_change($auxpath,$jobname);
}
}
print "fmtlatex: the common fmt file is ready, so running normal latex... \n";
# 通常のタイプセット
my $finalres = Run_subst("$engine -fmt \"$comdir/$comname\" $options $texname");
return $finalres;
}
}
# 共有フォーマットファイルの確認・確保・破棄
{
# 確認
sub check_com_owned(){
my $path=$_[0];
open(my $fh, "<", "$comdir/$comname.info");
my $holder=<$fh>;
close($fh);
if($path eq $holder){
return 1;
}else{
return 0;
}
}
# 確保
sub hold_com(){
my $path=$_[0];
open(my $fh, ">", "$comdir/$comname.info");
print $fh "$path";
close($fh);
}
# 破棄(生成失敗時用)
sub throw_com(){
open(my $fh, ">", "$comdir/$comname.info");
print $fh "";
close($fh);
}
}
# プリアンブル差分検知
{
my $prea_ext = "prea";
$clean_ext="$clean_ext $prea_ext";
# プリアンブル抽出用のコマンド(未改修)
# \endofdumpまたは\begin{document}まで読み出して保存
my $gethead = "awk '!/%.*/{if (p) print}BEGIN{p=1}/\\\\endofdump/{p=0}/\\\\begin\\{document\\}/{p=0}'";
my $comphead = "sed -e 's/ *\$//g' -e 's/%.*\$//g'";
sub check_preamble_change{
my ($auxpath, $basename, $texname) = @_;
my $preapath="$auxpath$basename.$prea_ext";
# プリアンブル部の一時ファイルをクリア
system("echo \"\" > \"$preapath.tmp\"");
my $chain_flag=1;
# subfilesによるプリアンブル依存が終わるまで続ける
do{
system("$gethead \"$texname\"|$comphead >> \"$preapath.tmp\"");
system("echo \"\" >> \"$preapath.tmp\"");
# subfilesの利用を検出
# 第1行が\documentclass[親ファイルパス]{subfiles}であればsubfiles使用とする
my $mastername = `head -n 1 "$texname"`;
if ($mastername =~ /^ *\\documentclass\[.*\]\{subfiles\} *$/){
$mastername =~ s/^ *\\documentclass\[//g;
$mastername =~ s/\]\{subfiles\} *$//g;
}else{
$mastername = "";
}
chomp($mastername);
# $masternameはsubfilesを利用していれば拡張子なしの親ファイルパスが入っている
if ($mastername ne ""){
$texname = "$mastername.tex";
}else{
$chain_flag=0;
}
}while($chain_flag == 1);
### 2024/05/24 追記 @utaoji 氏のリクエストを反映 ###
# input先を読み込む
# $preapath.tmpの中身を一行ずつ読む
&process_input_files($preapath);
sub process_input_files{
my ($preapath) = @_;
#プリアンブル読み込み制限(inputの循環回避用)
my $loading_limit=1000;
open(my $fh, '<', $preapath.".tmp") or die "Error: $!\n";
print "Processing $preapath.tmp\n";
my $i=0;
while (my $line = <$fh>) {
$i=$i+1;
last if $i >= $loading_limit;
if ($line =~ /\\input\{([^}]*)\}/) {
my $inputname = $1;
$inputname =~ s/\} *$//g;
print "Found input directive: $inputname\n";
system("$gethead \"$inputname\"|$comphead >> \"$preapath.tmp\"");
system("echo \"\" >> \"$preapath.tmp\"");
}
}
}
### 追記終わり ###
# 比較
my $checkret = system("diff -Bb \"$preapath.tmp\" \"$preapath\"");
return $checkret;
}
sub forget_preamble_change{
my ($auxpath, $basename) = @_;
system("rm \"$auxpath$basename.$prea_ext.tmp\"");
}
sub memorize_preamble_change{
my ($auxpath, $basename) = @_;
system("mv \"$auxpath$basename.$prea_ext.tmp\" \"$auxpath$basename.$prea_ext\"");
}
}
に置換。mylatexformatでLaTeX高速化(latexmk・Overleaf対応)[むしゃくしゃしてやった,今は反省している日記]やmylatexformat を用いてコンパイル時間を短縮しよう![TeX Alchemist Online]などを参考にした。以下解説。
# fmtlatexの呼び出し
$pdf_mode = 3;
$latex = 'internal fmtlatex uplatex %Z %Y %A %S %R -synctex=1 -file-line-error -halt-on-error %O';
$dvipdf = 'dvipdfmx %O -o %D %S';
$max_repeat = 5;
internal fmtlatex
でPerl
のサブルーチンとしてのfmtlatex
を呼び出すことができる。引数がやたら多いのは後で使うため。
# 作業パス
my $comdir=$ENV{HOME};
my $comname=".latexmk";
my $pwd=`pwd`;
chomp $pwd;
フォーマットファイルおよび関連ファイルを~/.latexmk.<拡張子>
で生成すると設定。これはフォーマットファイルのサイズが数十MBぐらいあり、そのままだと各.tex
ファイルにつき数十MBが消費されるため。.tex
ファイルの編集作業の局所性を活かして、フォーマットファイルをキャッシュとしてだけ保持する形にして節約している。またクラウドドライブに巨大な書き込みをしないためでもある。
以降、このホームディレクトリ直下に生成されるフォーマットファイルを共通フォーマットファイルと呼ぶことにする。
# fmtlatex メインルーチン
{
# 拡張子を登録
$clean_ext="$clean_ext fmt";
my $initial = 1;
sub fmtlatex {
# 引数読込
my ($engine, $outpath, $auxpath, $basename, $texname, $jobname, @args) = @_;
my $options = join(' ', @args);
# 初回実行時
if ($initial == 1){
$initial = 0;
# フォーマット生成フラグ
my $flag = 0;
print "fmtlatex: checking if the preamble changed...\n";
if (&check_preamble_change($auxpath,$jobname,$texname) == 0){
print "fmtlatex: the preamble is not changed.\n";
print "fmtlatex: checking if the common fmt file is owned...\n";
if (&check_com_owned("$pwd/$texname") == 0){
print "fmtlatex: the common fmt file is not owned.\n";
$flag = 1;
}else{
print "fmtlatex: the common fmt file is owned.\n";
}
}else{
print "fmtlatex: the preamble is changed.\n";
$flag = 1;
}
if ($flag == 1){
print "rewriting the common fmt file in ini mode...\n";
# フォーマット生成
my $iniret=Run_subst("$engine -ini $options -output-directory=\"$comdir\" -jobname=\"$comname\" \\\&$engine mylatexformat.ltx $texname");
if($iniret == 0){
print "fmtlatex: the common fmt file rewrited. saving preamble...\n";
&memorize_preamble_change($auxpath,$jobname);
&hold_com("$pwd/$texname");
}else{
print "fmtlatex: failed to rewrite the common fmt file.\n";
&forget_preamble_change($auxpath,$jobname);
&throw_com("$pwd/$texname");
return $iniret;
}
}else{
print "keep the common fmt file.\n";
&forget_preamble_change($auxpath,$jobname);
}
}
print "fmtlatex: the common fmt file is ready, so running normal latex... \n";
# 通常のタイプセット
my $finalres = Run_subst("$engine -fmt \"$comdir/$comname\" $options $texname");
return $finalres;
}
}
メインルーチンは専らmylatexformatでLaTeX高速化(latexmk・Overleaf対応)[むしゃくしゃしてやった,今は反省している日記]のそれを拡張した形になっている。条件分岐は以下の通り。
- 初回タイプセットである
↓- プリアンブルが変更されていない
↓- 共通フォーマットファイルが維持されている
→通常のタイプセット - 共通フォーマットファイルが維持されていない
→フォーマット生成+通常のタイプセット
- 共通フォーマットファイルが維持されている
- プリアンブルが変更されている
→フォーマット生成+通常のタイプセット
- プリアンブルが変更されていない
- 初回タイプセットではない
→ 通常のタイプセット
これにより、真にフォーマットファイル生成が必要な時のみ生成することになり、ほとんどのタイプセットでプリアンブル部分のタイプセットを省略することになる。
全体のコードが中括弧{}
で囲ってあるのは変数のスコープを切るため。グローバル空間に$initial
が置かれてるのは怖すぎる。
# 共有フォーマットファイルの確認・確保・破棄
{
# 確認
sub check_com_owned(){
my $path=$_[0];
open(my $fh, "<", "$comdir/$comname.info");
my $holder=<$fh>;
close($fh);
if($path eq $holder){
return 1;
}else{
return 0;
}
}
# 確保
sub hold_com(){
my $path=$_[0];
open(my $fh, ">", "$comdir/$comname.info");
print $fh "$path";
close($fh);
}
# 破棄(生成失敗時用)
sub throw_com(){
open(my $fh, ">", "$comdir/$comname.info");
print $fh "";
close($fh);
}
}
$comdir/$comname.info"
、すなわち~/.latexmk.info
にはタイプセット対象の.tex
ファイルのパスが書き込まれる。これを以って共通フォーマットファイルの生成元を識別する。多分Perl
チョットデキル人にかかればもっといい感じ8にできるんだろうが、個人用PCではそこまで問題にならないだろう。
# プリアンブル差分検知
{
my $prea_ext = "prea";
$clean_ext="$clean_ext $prea_ext";
# プリアンブル抽出用のコマンド(未改修)
# \endofdumpまたは\begin{document}まで読み出して保存
my $gethead = "awk '!/%.*/{if (p) print}BEGIN{p=1}/\\\\endofdump/{p=0}/\\\\begin\\{document\\}/{p=0}'";
my $comphead = "sed -e 's/ *\$//g' -e 's/%.*\$//g'";
sub check_preamble_change{
my ($auxpath, $basename, $texname) = @_;
my $preapath="$auxpath$basename.$prea_ext";
# プリアンブル部の一時ファイルをクリア
system("echo \"\" > \"$preapath.tmp\"");
my $chain_flag=1;
# subfilesによるプリアンブル依存が終わるまで続ける
do{
system("$gethead \"$texname\"|$comphead >> \"$preapath.tmp\"");
system("echo \"\" >> \"$preapath.tmp\"");
# subfilesの利用を検出
# 第1行が\documentclass[親ファイルパス]{subfiles}であればsubfiles使用とする
my $mastername = `head -n 1 "$texname"`;
if ($mastername =~ /^ *\\documentclass\[.*\]\{subfiles\} *$/){
$mastername =~ s/^ *\\documentclass\[//g;
$mastername =~ s/\]\{subfiles\} *$//g;
}else{
$mastername = "";
}
chomp($mastername);
# $masternameはsubfilesを利用していれば拡張子なしの親ファイルパスが入っている
if ($mastername ne ""){
$texname = "$mastername.tex";
}else{
$chain_flag=0;
}
}while($chain_flag == 1);
### 2024/05/24 追記 @utaoji 氏のリクエストを反映 ###
# input先を読み込む
# $preapath.tmpの中身を一行ずつ読む
&process_input_files($preapath);
sub process_input_files{
my ($preapath) = @_;
#プリアンブル読み込み制限(inputの循環回避用)
my $loading_limit=1000;
open(my $fh, '<', $preapath.".tmp") or die "Error: $!\n";
print "Processing $preapath.tmp\n";
my $i=0;
while (my $line = <$fh>) {
$i=$i+1;
last if $i >= $loading_limit;
if ($line =~ /\\input\{([^}]*)\}/) {
my $inputname = $1;
$inputname =~ s/\} *$//g;
print "Found input directive: $inputname\n";
system("$gethead \"$inputname\"|$comphead >> \"$preapath.tmp\"");
system("echo \"\" >> \"$preapath.tmp\"");
}
}
}
### 追記終わり ###
# 比較
my $checkret = system("diff -Bb \"$preapath.tmp\" \"$preapath\"");
return $checkret;
}
sub forget_preamble_change{
my ($auxpath, $basename) = @_;
system("rm \"$auxpath$basename.$prea_ext.tmp\"");
}
sub memorize_preamble_change{
my ($auxpath, $basename) = @_;
system("mv \"$auxpath$basename.$prea_ext.tmp\" \"$auxpath$basename.$prea_ext\"");
}
}
.tex
ファイルからプリアンブルを抽出し、<texファイル名(拡張子なし)>.prea.tmp
に書き出し。これを前回の結果である<texファイル名(拡張子なし)>.prea
と比較して差分を検出する。ここで、この後導入するsubfiles
によるプリアンブル依存の解消もやっている。
かなーり場当たり的に拡張してきたので、もっといい感じ8に確実にできるが、いかんせん余暇がない。余裕がある人はお好きにどうぞ。
時間測定
後でやるかも?
subfiles
の導入
ファイル分割パッケージにも色々あるが、プリアンブルを省略・共有できるsubfiles
を使うのが一般的な用法だと相性が良さそうに見える。
使い方
例えば、subex.tex
を
\documentclass[uplatex,dvipdfmx,a4paper]{jsarticle}
\usepackage{subfiles}
\newcommand{\fermi}{フェルミオンのファミリーマート、フェルミーオーン}
\newcommand{\bose}{ボソンのローソン、ボーソン}
\begin{document}
\section{メインファイル}
\fermi
\subfile{subexsub}
\end{document}
subexsub.tex
を
\documentclass[./subex]{subfiles}
\begin{document}
\section{サブファイル}
\bose
\end{document}
にすると(.tex
の拡張子はいらないことに注意)、それぞれこんな感じの.pdf
ファイルが出力される。
subex.tex
で定義した\bose
をsubexsub.tex
でも使えていることがわかる。
分割した LaTeX ファイルを subfiles を使ってコンパイルするを参考にした。
終わりに
良い子のみんなは初めからTypst
使って幸せになろうね。
追記記録
2024/05/24: @utaoji 氏の編集リクエストを反映。\input{}
先の更新を検知するようになりました。ありがとうございます。