这是 (Plain) pdftex 的测试文件。要测试它,您需要一些foo.pdf
工作目录中的图形文件。我的是通过 找到的文件的副本kpsewhich example-image-a.pdf
。调用以下命令并在命令行上testspeedgraphics.tex
执行。pdftex testspeedgraphics
\input graphicx.tex
\def\B{\noexpand\A\noexpand\A\noexpand\A\noexpand\A\noexpand\A%
\noexpand\A\noexpand\A\noexpand\A\noexpand\A\noexpand\A}%
\edef\C{\B\B\B\B\B\B\B\B\B\B}% 100 \A
\def\A{\noexpand\C}%
\edef\D{\C}% 100 \C, each one expanding to 100 \A
\def\A{\setbox0\hbox{\foo}}
\def\foo{\includegraphics{foo.pdf}}
\pdfresettimer
\D % 10000 usage of \includegraphics
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\input xintexpr.sty
\pdfresettimer
\D % 10000 usage of \includegraphics
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\input tikz.tex
\pdfresettimer
\D % 10000 usage of \includegraphics
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\input xlop.tex
\pdfresettimer
\D % 10000 usage of \includegraphics
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\bye
\includegraphics{foo.pdf}
此文件将执行4 次 10000。\box0
我们加载越来越多的包。在 2.8GHz 计算机上,我通常会在控制台输出中看到以下内容:
**** 1.12306pt****
(loading of xintexpr)
**** 1.19366pt****
(loading of tikz)
**** 1.36714pt****
(loading of xlop)
**** 1.40392pt****
因此,时间增加了,而且人们感觉包裹越大,影响就越大。
现在注释掉上面测试文件中的所有额外包的加载并重复实验。我通常会得到
**** 1.13177pt****
**** 1.12141pt****
**** 1.14122pt****
**** 1.12416pt****
即没有时间漂移......
现在再来一个测试文件,我们仍然包含graphicx.tex
但没有使用\includegraphics
,而是扩展了一个虚拟宏\foo
。这样速度会快得多,我们进行了 1000000 次重复。
\input graphicx.tex
\def\B{\noexpand\A\noexpand\A\noexpand\A\noexpand\A\noexpand\A%
\noexpand\A\noexpand\A\noexpand\A\noexpand\A\noexpand\A}%
\edef\C{\B\B\B\B\B\B\B\B\B\B}% 100 \A
\def\A{\noexpand\C}%
\edef\D{\C}% 100 \C, each one expanding to 100 \A
\def\A{\noexpand\D}%
\edef\E{\C}% 100 \D, each one expanding to 100 \C
\def\A{\setbox0\hbox{\foo}}
\def\foo{foo}
\pdfresettimer
\E % 1000000 "foo"
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\input xintexpr.sty
\pdfresettimer
\E % 1000000 "foo"
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\input tikz.tex
\pdfresettimer
\E % 1000000 "foo"
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\input xlop.tex
\pdfresettimer
\E % 1000000 "foo"
\edef\zzz{\the\dimexpr\pdfelapsedtime sp}
\message{^^J^^J**** \zzz ****^^J^^J}
\bye
此测试文件执行所有\input
额外包的操作。通常我从以下位置获取pdftex testspeedfoo.tex
:
**** 0.48016pt****
(xintexpr)
**** 0.49115pt****
(tikz)
**** 0.48283pt****
(xlop)
**** 0.47798pt****
即根本没有影响。
现在,为什么会产生影响\includegraphics
。仅仅是因为它的扩展要复杂得多吗?如果是,那又该如何解释呢?还是它与\includegraphics
交易有关的特定事物?那么,又该如何解释呢?
也许与哈希表有关?定义的宏越多,TeX 扩展宏的效率就越低?(那么事情就不具体了\includegraphics
,我在那里停止了测试,让专家先表达他们的直觉)。
最初,这个问题是在约瑟夫回答使用xfp
和\includegraphics
。我想测试使用draft
选项是否\includegraphics
有影响,然后意识到加载或不加载都会xfp
改变时间。然后我意识到它没有什么xfp
特别的,但任何大包都可以。
我还测试了以下变体\foo
:
\def\A{\setbox0\hbox{\foo}}
\def\foo{\fooa}
\def\fooa{\foob}
\def\foob{\fooc}
\def\fooc{\food}
\def\food{\fooe}
\def\fooe{\foof}
\def\foof{\foog}
\def\foog{\fooh}
\def\fooh{\fooi}
\def\fooi{\fooj}
\def\fooj{\fook}
\def\fook{\fool}
\def\fool{\foom}
\def\foom{\foon}
\def\foon{\fooo}
\def\fooo{\foop}
\def\foop{\fooq}
\def\fooq{\foor}
\def\foor{\foos}
\def\foos{\foot}
\def\foot{\foou}
\def\foou{\foov}
\def\foov{\foow}
\def\foow{\foox}
\def\foox{\fooy}
\def\fooy{\fooz}
\def\fooz{\fooA}
\def\fooA{\fooB}
\def\fooB{\fooC}
\def\fooC{\fooD}
\def\fooD{\fooE}
\def\fooE{\fooF}
\def\fooF{\fooG}
\def\fooG{\fooH}
\def\fooH{\fooI}
\def\fooI{\fooJ}
\def\fooJ{\fooK}
\def\fooK{\fooL}
\def\fooL{\fooM}
\def\fooM{\fooN}
\def\fooN{\fooO}
\def\fooO{\fooP}
\def\fooP{\fooQ}
\def\fooQ{\fooR}
\def\fooR{\fooS}
\def\fooS{\fooT}
\def\fooT{\fooU}
\def\fooU{\fooV}
\def\fooV{\fooW}
\def\fooW{\fooX}
\def\fooX{\fooY}
\def\fooY{\fooZ}
\def\fooZ{\fooaa}
\def\fooaa{\foobb}
\def\foobb{\foocc}
\def\foocc{\foodd}
\def\foodd{\fooee}
\def\fooee{\fooff}
\def\fooff{\foogg}
\def\foogg{\foohh}
\def\foohh{\fooii}
\def\fooii{\foojj}
\def\foojj{\fookk}
\def\fookk{\fooll}
\def\fooll{\foomm}
\def\foomm{\foonn}
\def\foonn{\foooo}
\def\foooo{\foopp}
\def\foopp{\fooqq}
\def\fooqq{\foorr}
\def\foorr{\fooss}
\def\fooss{\foott}
\def\foott{\foouu}
\def\foouu{\foovv}
\def\foovv{\fooww}
\def\fooww{\fooxx}
\def\fooxx{\fooyy}
\def\fooyy{\foozz}
\def\foozz{\fooAA}
\def\fooAA{\fooBB}
\def\fooBB{\fooCC}
\def\fooCC{\fooDD}
\def\fooDD{\fooEE}
\def\fooEE{\fooFF}
\def\fooFF{\fooGG}
\def\fooGG{\fooHH}
\def\fooHH{\fooII}
\def\fooII{\fooJJ}
\def\fooJJ{\fooKK}
\def\fooKK{\fooLL}
\def\fooLL{\fooMM}
\def\fooMM{\fooNN}
\def\fooNN{\fooOO}
\def\fooOO{\fooPP}
\def\fooPP{\fooQQ}
\def\fooQQ{\fooRR}
\def\fooRR{\fooSS}
\def\fooSS{\fooTT}
\def\fooTT{\fooUU}
\def\fooUU{\fooVV}
\def\fooVV{\fooWW}
\def\fooWW{\fooXX}
\def\fooXX{\fooYY}
\def\fooYY{\fooZZ}
\def\fooZZ{foo}
尝试通过扩展许多不同的宏来模拟这种情况。但这并没有显示加载包时的任何漂移,即我没有重现这种情况\includegraphics
:在我的 2.8GHz 计算机上,所有 1000000 的四次执行\A
每次大约需要 2.9 秒 - 3 秒。
答案1
这真是一个难题。
这个答案会很长,因为我花了很长时间才弄清楚,但这些部分都有标题和编号,所以你可以跳过那些你不关心的部分。你甚至可以跳到最后的一句话总结。:-)
1. 调试过程
这将描述我如何得出答案。如果你不关心,只想知道答案,你可以跳到下一部分。
1.1. 问题重现
我拿了顶部的文件问题,将其保存到文件中,也通过将其复制为来jfbu1.tex
创建,然后运行,果然,看到了如下输出:foo.pdf
cp $(kpsewhich example-image-a.pdf) foo.pdf
pdftex jfbu1.tex
This is pdfTeX, Version [etc, some lines skipped]
**** 0.87534pt****
(/usr/share/texlive/texmf-dist/tex/generic/xint/xintexpr.sty (...))
**** 0.93637pt****
(/usr/share/texlive/texmf-dist/tex/plain/pgf/frontendlayer/tikz.tex (...))
**** 1.03432pt****
(/usr/share/texlive/texmf-dist/tex/generic/xlop/xlop.tex [...])
**** 1.06012pt****
数量还在不断增加。
1.2. 简化测试用例
由于测量的内容涉及\includegraphics{foo.pdf}
,我尝试解开的定义\includegraphics
以查看是否可以进一步简化。 (我也尝试了其他方法,例如直接\D
用 的扩展的 10000 个出现替换\A
,但这并没有什么不同。)好吧,\show\includegraphics
显示其定义是\leavevmode \@ifstar {\Gin@cliptrue \Gin@i }{\Gin@clipfalse \Gin@i }
,所以让我们\includegraphics
在文件中用它来替换,看看是否仍然观察到该现象,然后尝试删除部分内容。 (例如,条件的第一个情况\@ifstar
可能无关紧要,因为在这个例子中我们没有*
,所以我们不会走那条路。)
我做了一点,主要依靠\show
,并诅咒 LaTeX 的复杂性(仅仅用 TeX 原语写出的完整定义\includegraphics
就太复杂了)。(小提示:下次使用 LaTeX 源代码而不是\show
;这样更容易阅读。)
无论如何,经过这几个步骤(深入定义几步\includegraphics
),我发现我们可以替换原始文件的
\def\foo{\includegraphics{foo.pdf}}
使用(仅使用定义的早期部分\includegraphics
):
\def\foo{\filename@parse {foo.pdf} \Gin@getbase {\Gin@sepdefault \filename@ext }}
并且我们仍然观察到随着我们加载更多包,时间增加的现象(尽管它运行得更快)。事实上,继续下去,这已经足够了:
\def\foo{\IfFileExists{foo.pdf}{}{}}
— 显然,随着我们加载更多的包,简单地测试一个文件是否存在会花更长的时间!
再进行几个步骤,我们就可以得到 TeX 基元(无需加载graphicx
):
\def\foo{\openin0{foo.pdf}\closein0{foo.pdf}}
— 它只是打开和关闭文件(显然这是\IfFileExists
内部工作原理的一部分)。
在做了一些更改之后 — — 例如,我们不需要使用转换\pdfelapsedtime
成秒数的技巧(?),因为我们更关心它是否在增加,而不是它的实际值,并且即使重复次数少于 10000 次(例如 81 次,将 10 替换为 3 次),也能始终观察到这种现象 — — 我们得到了以下稍微简单一点的文件jfbu2.tex
,其中仍然可以观察到这种现象:
\def\B{\noexpand\A\noexpand\A\noexpand\A}
\edef\C{\B\B\B} % Now the definition of \C is 9 times \A
\def\A{\noexpand\C}
\edef\D{\C} % Expands to 9 \C, each of which will expand to 9 \A
\def\filename{foo.pdf}
\def\A{\setbox0\hbox{\openin0\filename\closein0\filename}}
\pdfresettimer\the\pdfelapsedtime
\D
\message{^^J^^J****\the\pdfelapsedtime****^^J^^J}
\input xintexpr.sty
\pdfresettimer\the\pdfelapsedtime
\D
\message{^^J^^J****\the\pdfelapsedtime****^^J^^J}
\input tikz.tex
\pdfresettimer\the\pdfelapsedtime
\D
\message{^^J^^J****\the\pdfelapsedtime****^^J^^J}
\input xlop.tex
\pdfresettimer\the\pdfelapsedtime
\D
\message{^^J^^J****\the\pdfelapsedtime****^^J^^J}
\bye
并且这仍然会打印如下输出(并不总是相同的数字,但增加的模式是一致的):
This is pdfTeX, Version [etc, some lines skipped]
***74***
[loading of xintexpr.sty]
***96***
[loading of tikz.tex]
****142****
[loading of xlop.tex]
****149****
\the\pdfelapsedtime
(关于之前的无用之处\D
,它只是在排版输出中插入“0”,我将在下面详细说明。)
但谜团依然存在:\A
上面的操作似乎应该只执行恒定数量的操作,为什么加载更多包时需要更长的时间?
现在我们已经了解了 TeX 原语,在宏(“TeX 编程”)级别上没有什么可解压的了,我们需要研究 TeX 程序本身。
1.3. 使用源代码进行调试
我们可以使用类似这样的调试器逐步调试 TeX 程序gdb
。
要做到这一点,使用 LuaTeX 源代码通常更容易,因为它(某种程度上)是直接用 C 编写的,而不是 TeX/pdfTeX/XeTeX 的源代码,因为它们在.web
编译之前要经过几轮从源代码到 C 代码的转换。(因此,在 LuaTeX 的情况下,最终编译的 C 代码更具可读性。)但事实证明(在适当的定义\pdfelapsedtime
) 我们感兴趣的这种现象在 LuaTeX 中不会发生,所以我们只能使用 pdfTeX。(它确实发生在 TeX / XeTeX 中,但没有方便之处,\pdfelapsedtime
我们只能通过视觉观察它需要更长的时间。)
1.3.1. 构建gdb
要使用 gdb,需要使用 编译程序-g
,并且二进制文件不应被剥离。幸运的是,我以前做过这件事:诀窍是,当从 TeX Live 源构建时(请参阅这里和这里), 代替
./Build
使用
./Build --no-clean -g
并更新$PATH
以适当使用新的二进制文件。(或者,如果使用make install strip
,则应替换为make install
。)
1.3.2. 使用 gdb 的第一步
可以使用类似以下命令启动 gdb gdb pdftex
(确保 PATH 正确,否则指定我们专门编译的pdftex
二进制文件的完整路径)。然后,可以在运行程序(就像我们pdftex jfbu2.tex
在命令行上运行一样)之前设置断点run jfbu2.tex
。
要设置哪些断点?我们希望在调用某个不经常调用的特定函数时停止。我的选择是调用的函数\pdfelapsedtime
(虽然事后我猜使用 forpdfresettimer
会更好),通过查看源代码和/或 gdb,恰好是 (or call) 。(这就是上述文件中getmicrointerval
“extra”的原因 ,因为我想在那里中断。)\the\pdfelapsedtime
因此,我们可以启动gdb
、设置break getmicrointerval
并运行程序,程序将在到达函数调用处后停止。然后,我们可以键入continue
以继续执行直到下一个断点(或程序结尾),或者键入next
以调用程序的下一个语句(跳过函数调用,即不进入它们)或step
在执行时执行相同操作进入函数调用。当你不断按下 Enter 键时,它会显示每个被调用的函数,以及执行的每一行源代码。
做了一点之后,很明显手动做这件事会花很长时间。
1.3.3. 脚本 gdb
长话短说:输入以下内容~/.gdbinit
:
define mystep
step
refresh
end
define keepstepping
while(1)
step
end
end
set pagination off
set logging on
file pdftex
break getmicrointerval
run jfbu2.tex
continue
keepstepping
这就像输入“step”并手动按几百万次 Enter 直到程序完成,并且 gdb 输出的所有内容都将写入文件gdb.txt
。
这样,整个程序运行了几个小时,产生了一个gdb.txt
超过 700 MB 大小的文件,包含超过 2000 万行代码。
该文件的开头如下所示:
Breakpoint 1 at 0x84bad: file pdftex0.c, line 3471.
Breakpoint 1, getmicrointerval () at pdftex0.c:3471
3471 secondsandmicros ( s , m ) ;
Breakpoint 1, getmicrointerval () at pdftex0.c:3471
3471 secondsandmicros ( s , m ) ;
get_seconds_and_micros (seconds=0x7fffffffd92c, micros=0x7fffffffd928) at ../../../texk/web2c/lib/texmfmp.c:2329
2329 gettimeofday(&tv, NULL);
2330 *seconds = tv.tv_sec;
2331 *micros = tv.tv_usec;
2342 }
getmicrointerval () at pdftex0.c:3472
3472 if ( ( s - epochseconds ) > 32767 )
3474 else if ( ( microseconds > m ) )
3477 else Result = ( ( s - epochseconds ) * 65536L ) + ( ( ( m - microseconds )
3478 / ((double) 100 ) ) * 65536L ) / ((double) 10000 ) ;
3477 else Result = ( ( s - epochseconds ) * 65536L ) + ( ( ( m - microseconds )
3479 return Result ;
3480 }
zscansomethinginternal (level=5 '\005', negative=0) at pdftex0.c:11926
11926 break ;
11990 curvallevel = 0 ;
12059 break ;
12115 while ( curvallevel > level ) {
12123 if ( negative ) {
(第一个Breakpoint 1 at 0x84bad: file pdftex0.c, line 3471
是在我们设置断点时打印的;continue
第一次 gdb 在断点处暂停后,所以直到下一次才有输出。)上面输出中显示的部分是每次调用时都会出现\pdfelapsedtime
的(我们甚至还没有讲到这个\D
部分)。
当然,我们无法通过手动读取来处理这个 2000 万个几百兆字节的文件。事实上,grep --line-number Breakpoint gdb.txt
可以用来查看在连续出现\pdfelapsedtime
(调用getmicrointerval
)之间执行的程序步骤数:
4:Breakpoint 1 at 0x84bad: file pdftex0.c, line 3471.
9:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
12:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
100418:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
3631431:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
3766236:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
11906822:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
12159055:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
20605166:Breakpoint 1, getmicrointerval () at pdftex0.c:3471
这表明
- 加载 xintexpr 大约需要 100418-12=100406 步,
- 之后
\D
大约需要 3631431-100418=3531013脚步, - 加载 tikz 大约需要 3766236-3631431=134805 个步骤,
- 之后
\D
大约需要 11906822-3766236=8140586脚步, - 加载 xlop 大约需要 12159055-11906822=252233 步,
- 之后
\D
大约需要 20605166-12159055=8446111脚步
我们可以看到上面粗体数字的增加。(\D
由于“继续”,所以错过了第一个。)
1.3.4. 处理 gdb.txt
主要思想是,尽管文件有 2000 万行长,但执行的不同行集要小得多,我们想要比较的是在每个连续的断点之间哪些行执行得更频繁。
我们可以保存文件中“Breakpoint 1, getmicrointerval”连续出现之间执行了多少次的计数器。使用以下 Python 脚本:
pattern = 'Breakpoint 1, getmicrointerval'
f = open('gdb.txt', 'r')
line = f.readline()
while pattern not in line:
line = f.readline()
print line
# Now, line has an occurrence of pattern
# Counter 0: From occurrence 0 to occurrence 1
# Counter 1: From occurrence 1 to occurrence 2
# etc.
from collections import Counter
c = {}
i = 0
while line:
assert pattern in line
line = f.readline()
cur = Counter()
while pattern not in line:
cur[line] += 1
line = f.readline()
if not line: break
if i % 2 == 0 and i > 0:
for _ in range(10): print
print i
frequent = cur.most_common(61)
out = [(-count, l) for (l, count) in frequent]
for (count, l) in sorted(out):
print '%d\t\t%s' % (-count, l),
c[i] = cur
i += 1
2
这是(第二次出现 )的前几行输出\D
。第一列是执行的次数,然后是 gdb 打印的内容(通常是行号和源代码行)。
2
975726 1067 while ( s > 255 ) {
975726 1069 if ( ( strstart [s + 1 ]- strstart [s ]) == len ) {
975564 1077 decr ( s ) ;
25596 1034 if ( ( strstart [s + 1 ]- strstart [s ]) != ( strstart [t + 1 ]-
13932 1039 while ( j < strstart [s + 1 ]) {
13770 1041 if ( strpool [j ]!= strpool [k ])
12798 1033 result = false ;
12798 1035 strstart [t ]) )
12798 1037 j = strstart [s ];
12798 1038 k = strstart [t ];
12798 1047 lab45: Result = result ;
12798 1048 return Result ;
12798 1049 }
12798 1071 if ( streqstr ( s , search ) )
12636 1042 goto lab45 ;
12636 zsearchstring (search=9213) at pdftex0.c:1077
11988 54 while (*p != 0 && !(brace_level == 0
6804 37 while (*key != 0)
6480 44 n = (n + n + TRANSFORM (*key++)) % table.size;
5994 55 && (env_p ? IS_ENV_SEP (*p) : IS_DIR_SEP (*p)))) {
5832 56 if (*p == '{') ++brace_level;
5832 57 else if (*p == '}') --brace_level;
5832 62 p++;
3429 9427 lab20: curcs = 0 ;
3429 9428 if ( curinput .statefield != 0 )
3417 9909 else if ( curinput .locfield != -268435455L )
与 4 (加载\D
后第三次出现的 )进行比较:tikz
4
2502900 1067 while ( s > 255 ) {
2502900 1069 if ( ( strstart [s + 1 ]- strstart [s ]) == len ) {
2502738 1077 decr ( s ) ;
29322 1034 if ( ( strstart [s + 1 ]- strstart [s ]) != ( strstart [t + 1 ]-
16038 1039 while ( j < strstart [s + 1 ]) {
15876 1041 if ( strpool [j ]!= strpool [k ])
14661 1033 result = false ;
14661 1035 strstart [t ]) )
14661 1037 j = strstart [s ];
14661 1038 k = strstart [t ];
14661 1047 lab45: Result = result ;
14661 1048 return Result ;
14661 1049 }
14661 1071 if ( streqstr ( s , search ) )
14499 1042 goto lab45 ;
14499 zsearchstring (search=18640) at pdftex0.c:1077
11988 54 while (*p != 0 && !(brace_level == 0
6804 37 while (*key != 0)
6480 44 n = (n + n + TRANSFORM (*key++)) % table.size;
5994 55 && (env_p ? IS_ENV_SEP (*p) : IS_DIR_SEP (*p)))) {
5832 56 if (*p == '{') ++brace_level;
5832 57 else if (*p == '}') --brace_level;
5832 62 p++;
3429 9427 lab20: curcs = 0 ;
3429 9428 if ( curinput .statefield != 0 )
3417 9909 else if ( curinput .locfield != -268435455L )
xlop
对于 6 (加载之后的最后一个):
6
2603826 1067 while ( s > 255 ) {
2603826 1069 if ( ( strstart [s + 1 ]- strstart [s ]) == len ) {
2603664 1077 decr ( s ) ;
29646 1034 if ( ( strstart [s + 1 ]- strstart [s ]) != ( strstart [t + 1 ]-
16200 1039 while ( j < strstart [s + 1 ]) {
16038 1041 if ( strpool [j ]!= strpool [k ])
14823 1033 result = false ;
14823 1035 strstart [t ]) )
14823 1037 j = strstart [s ];
14823 1038 k = strstart [t ];
14823 1047 lab45: Result = result ;
14823 1048 return Result ;
14823 1049 }
14823 1071 if ( streqstr ( s , search ) )
14661 1042 goto lab45 ;
14661 zsearchstring (search=19263) at pdftex0.c:1077
11988 54 while (*p != 0 && !(brace_level == 0
6804 37 while (*key != 0)
6480 44 n = (n + n + TRANSFORM (*key++)) % table.size;
5994 55 && (env_p ? IS_ENV_SEP (*p) : IS_DIR_SEP (*p)))) {
5832 56 if (*p == '{') ++brace_level;
5832 57 else if (*p == '}') --brace_level;
5832 62 p++;
3429 9427 lab20: curcs = 0 ;
3429 9428 if ( curinput .statefield != 0 )
3417 9909 else if ( curinput .locfield != -268435455L )
1.3.5. 比较输出
我们可以通过在单独的选项卡中打开每个选项卡并在它们之间切换来直观地完成此操作。例如,(最频繁的)循环或测试在加载while ( s > 255 ) {
后执行了 2502900 次,而之前执行了 975726 次。之后的所有内容(频率较低的)都运行了相同的次数(在至少执行 500 次的语句中),并且上面的所有内容都来自函数内部,或者来自上面的函数(从 调用)。因此,罪魁祸首完全是中的这个函数。tikz.tex
zsearchstring
zsearchstring
zstreqstr
zsearchstring
zsearchstring
pdftex0.c
如果我们理解了这zsearchstring
是什么以及为什么叫它这个名字,那么调试过程就结束了。
2. 理解我们的发现
如果您跳过上一节:到目前为止,我们发现不同调用之间的所有额外工作\D
都发生在函数中zsearchstring
,pdftex0.c
随着加载更多的包,该函数似乎被调用更多次(并执行更多操作)。为什么?
2.1. 什么是zsearchstring
?
2.1.1. 在源代码中定位
zsearchstring
我们可以看到inpdftex0.c
或 in的完整定义tex0.c
(它们都Build/source/Work/texk/web2c/
在 texlive 目录内):
strnumber
zsearchstring ( strnumber search )
{
/* 40 */ register strnumber Result; searchstring_regmem
strnumber result ;
strnumber s ;
integer len ;
result = 0 ;
len = ( strstart [search + 1 ]- strstart [search ]) ;
if ( len == 0 )
{
result = 345 ;
goto lab40 ;
}
else {
s = search - 1 ;
while ( s > 255 ) {
if ( ( strstart [s + 1 ]- strstart [s ]) == len ) {
if ( streqstr ( s , search ) )
{
result = s ;
goto lab40 ;
}
}
decr ( s ) ;
}
}
lab40: Result = result ;
return Result ;
}
但乍一看,它似乎没有在文件中的其他地方使用。这是因为在pdftexcoerce.h
(或texcoerce.h
)中,你会发现一个声明和一个定义为它的宏:
strnumber zsearchstring (strnumber search);
#define searchstring(search) zsearchstring((strnumber) (search))
您确实可以在或中找到searchstring
几次使用。pdftex0.c
tex0.c
searchstring
不过,这段 C 代码的可读性比必要的要差一些。事实上,找到 的文件列表包括tex.p
,这可能是 纠缠的结果tex.web
。然而,如果你看看 TeX 源代码(带有texdoc tex
例如,您将找不到该函数,因为它不是 Knuth 编写的代码的一部分。相反,它是“系统相关更改”的一部分 — 在 web2c 中所做的更改,以生成可运行的 TeX 程序。相反,您需要查看“完整”的 (pdf)TeX 源代码,其中也包含更改文件。类似于以下内容(假设这texlive
是 texlive 目录):
weave Build/source/texk/web2c/pdftexdir/pdftex.web Build/source/Work/texk/web2c/pdftex.ch
生成一个pdftex.tex
文件,然后pdftex pdftex.tex
(可选择将 更改\input webmac
为\input pdfwebmac
)。(也可以.ch
直接查看文件,但 WEB 代码很丑陋,最好不要直接查看。)
现在我们可以在生成的 PDF 中查找search_string
。
2.1.2. 定义、记录和使用search_string
这是 的定义search_string
;与我们上面的早期zsearchstring
C 代码(由此生成)进行比较:
这最终解释了它search_string
是什么以及它为什么存在。(我们将在下面详细说明。)看看它使用的地方会让事情更加清楚。它用于三个程序:end_name
、start_input
和slow_make_string
。让我们看看前两个:
与 TeX 程序中的相应部分进行比较:§517和第537条search_string
,在调用 之后不要使用make_name_string
。值得一看的定义(至少是文档/上下文)那也:
2.2. 与文件操作的关系
\input
我们在上面看到,这些函数被称为“在、\openin
或操作中扫描文件名时\openout
”。这当然包括\includegraphics{foo.pdf}
问题中的示例,以及\openin
简化示例中的示例。
请注意,有时扫描不需要创建字符串:我们可以通过将测试用例更改为以下内容来看到这一点:
\toks0={foo.pdf}
\def\A{\setbox0\hbox{\openin0\the\toks0\closein0\the\toks0}}
未观察到该现象的地方。(我还没有通过 gdb 运行它,但是数字并没有增加。)
另请参阅为什么处理文件名首先需要系统相关的更改— 在开发 TeX 的时候,文件名在不同的操作系统上非常不一致;事实上,在开发 TeX 的地方(SAIL),文件名由“基础”、“扩展”和“区域”组成,其中包括用户的姓名首字母和项目(或类似的东西)。
2.3. 什么是字符串池等?
一些背景知识,用于理解我们上面看到的代码的上下文。在 Knuth 最初(重写)TeX 时(1980-1982 年),编程语言 Pascal(至少是他可用的版本以及许多将使用 TeX 的地方)对字符串的支持并不好。因此,TeX 基本上负责手动分配所有字符串:有一个名为 的巨大字符数组,str_pool
在程序开始时初始化,每当 TeX 需要存储新字符串时,它都会将新字符串的字符(在构建过程中,例如从输入文件中扫描)存储在该数组的连续索引处。例如,第 k 个字符串从 开始,str_pool[str_start[k]]
一直到str_pool[str_start[k+1]-1]
。或者您可以阅读此内容在程序中:
请注意,字符串池只是一个数组,并没有针对发现其中的字符串:最初编写的 TeX 程序会保存对任何需要的字符串的引用(例如,它将保存“k”,从而知道在哪里找到第 k 个字符串)。它永远不需要在数组中的所有字符串中查找特定字符串,就像搜索计算机内存的所有字节来查找特定值是不合理的一样。
但是,当对 web2c 进行系统相关的更改时(很久以前),slow_make_string
引入了一个函数,该函数在保存字符串之前,会搜索整个字符串池(!)以查看它是否已经以其他名称(数字)存在。如果是,则重复使用相同的字符串(数字)。这解释了非常频繁(执行数百万次)循环
while ( s > 255 ) {
if ( ( strstart [s + 1 ]- strstart [s ]) == len ) {
...
}
decr ( s ) ;
}
我们看到的是gdb
:它正在搜索全部字符串数字s
,从最大(最新)值开始。
当内存限制比时间限制更严格时(您总是可以等待更长时间),这似乎很有道理,尤其是因为这还意味着字符串池很小,因此搜索整个字符串池所花费的时间限制较小。在当前的内存大小(以及内存访问时间,在过去几十年中,内存访问时间相对于(算术)CPU 指令而言一直变得越来越昂贵)下,可能值得重新考虑...
(TeX 最初编写时并没有这样做。从一些文档来看,好像 TeX 只是创建了这个新字符串,不存储对它的引用,然后继续前进,这听起来像是一个典型的内存泄漏错误 - 可能值得 DEK 的一张奖励支票?:P - 但从查看一些代码来看,似乎 TeX 无条件地刷新了字符串,所以更确切地说,改变后的(web2c)TeX 出于某种原因想要保留一个引用,所以它需要这个解决方法......我不清楚是哪种情况。)
2.4. 其他 TeX 发行版
除了 TeX Live,我还研究了 MiKTeX,它对这些部分的代码几乎完全相同。(只是从“54/web2c-string”重命名为“54/MiKTeX-string”。)我还没有研究过其他不太常见的(不是基于 web2c)TeX 发行版,如 KerTeX 或 TeX-gpc,当然也没有研究过闭源(商业)发行版,如 BaKoMa TeX 或 Texpad。
2.5. 查看字符串池使用情况
在 TeX 运行结束时,如果\tracingstats=1
,程序会将统计信息打印到日志文件(“这是您使用的 TeX 内存量”)。这些是通过将 移动\bye
到上述文件中的不同位置(添加 后\tracingstats=1
)得到的结果:
在文件顶部(紧接着之后
\tracingstats=1
):5 strings out of 495042 126 string characters out of 6159513
首先定义
\A
、\B
、之后\C
:\D
5 strings out of 495042 126 string characters out of 6159513
(不会改变,因为单字母名称没有单独存储。)
在这些之后,还
\filename
定义了:6 strings out of 495042 134 string characters out of 6159513
(有意义:
\filename
是一个字符串,长度为 8 个字符。)就在第一个之前
\D
:8 strings out of 495042 153 string characters out of 6159513
(这两个字符串总共 19 个字节,分别是不是原始的
\pdfresettimer
和\pdfelapsedtime
(那些可能已经存储了),而是由创建的某些东西\the\pdfelapsedtime
。不确定细节。)就在第一个之后
\D
,或者就在第一个之后\message
:9 strings out of 495042 156 string characters out of 6159513
\input xintexpr
加载之后(以及 之前的任何位置\input tikz
):3815 strings out of 495042 61780 string characters out of 6159513
后
\input tikz
:13243 strings out of 495042 266711 string characters out of 6159513
(请注意与之前相比有大幅增长。)
之后
\input xlop
(或文件末尾):13866 strings out of 495042 274144 string characters out of 6159513
字符串池大小的相对增加大致与执行时间的相对增加相匹配\D
。
3. 总结/回答
在 TeX 的常见实现中,TeX 扫描文件名的命令(例如 的情况\includegraphics
)涉及搜索整个字符串池,并且随着加载更多的包,速度会变得越来越慢,因为包定义了控制序列(宏),其名称存储在字符串池中。