latex3 / fontspec

Font selection in LaTeX for XeTeX and LuaTeX
http://latex3.github.io/fontspec/
LaTeX Project Public License v1.3c
275 stars 33 forks source link

\addfontfeature should cache fonts #391

Open hmenke opened 4 years ago

hmenke commented 4 years ago

Description

The \addfontfeature command does not cache fonts at global scope. Why is this important? It turns out that users don't seem to be aware that \addfontfeature reloads the whole font from disk with adjusted features and that is absolutely detrimental for typesetting performance. Even worse, commands that seem innocuous, like \textsuperscript can become real monsters, when, in this case by loading the realscripts package, adding \addfontfeature to the definition of \textsuperscript and thus reloads the whole font on every invocation!

However, the usage of \addfontfeature seems to me mostly to be done programmatically, i.e. inside a macro with always the exact same set of features. It seems, therefore, advantageous to cache fonts at the macro level. It could also be done in the font loader, which might be possible for LuaTeX, but since XeTeX development has basically been abandoned, there is not much chance there.

Check/indicate

Minimal example demonstrating the issue

An example for this issue can't be minimal because the problem really only manifests when \addfontfeature is called many times. However, we can have a minimal example generator, which is only a few lines of Lua. Here I just read a dictionary from disk. On Linux /usr/share/dict/words is a file with one word per line.

-- save as make_big.lua
local words = {}
do
    local file = io.open("/usr/share/dict/words", "r")
    for word in file:lines() do
        words[#words + 1] = word
    end
    file:close()
end

local preamble = [[
\documentclass{article}
\usepackage{fontspec}
\setmainfont{SourceSerifPro-Regular.otf}
\begin{document}
]]

do  
    local file = io.open("big.tex", "w")
    file:write(preamble)
    for i = 1, math.min(#words, 8000) do
        file:write(string.format("{\\addfontfeature{Style=Alternate}%s}\n\n", words[i]))
    end
    file:write([[\end{document}]])
    file:close()
end

Running

$ texlua make_big.lua

will generate a file, similar to this:

\documentclass{article}
\usepackage{fontspec}
\setmainfont{SourceSerifPro-Regular.otf}
\begin{document}
{\addfontfeature{Style=Alternate}A}

% ... thousands more

\end{document}

Note that I deliberately chose Style=Alternate as a font feature, because Source Serif Pro actually does not have this feature, which worsens the problem because for each time the font is loaded from disk, a multiline warning message has to be written to the log, increasing the amount of disk I/O and slowing down the process further.

Running this in LuaTeX generates the following timing on my machine

$ time lualatex --interaction=batchmode big.tex
This is LuaTeX, Version 1.10.0 (TeX Live 2019) 
 restricted system commands enabled.

luaotfload | main : initialization completed in 0.142 seconds
[1]+  Done                    emacs test.py

real    0m39.038s
user    0m39.631s
sys 0m2.421s

However, just adding

\renewcommand\addfontfeature[1]{}

after \begin{document}, the timing reduces to

$ time lualatex --interaction=batchmode big.tex
This is LuaTeX, Version 1.10.0 (TeX Live 2019) 
 restricted system commands enabled.

luaotfload | main : initialization completed in 0.141 seconds

real    0m2.532s
user    0m2.406s
sys 0m0.124s

That is a whopping 20 times speedup!

Further details

In fact, if you keep scaling this up, the huge number local macro definitions from expl3 falls on your feet. In this TeX.SX question, the user actually ran out of strings: https://tex.stackexchange.com/questions/517505

A related topic is Release space in the string pool.

hmenke commented 4 years ago

Here is a simple plain TeX reproducer of the problem.

\input luaotfload.sty

\def\mainfont{SourceSerifPro-Regular.otf}

\long\def\onum#1{%
    \begingroup
        \expandafter\font\csname\mainfont:+onum\endcsname="\mainfont:+onum" at 10pt
        \csname\mainfont:+onum\endcsname#1%
    \endgroup
}

\newcount\counter
\counter=0
\loop\ifnum\counter<10000
    \onum{123}\endgraf
    \advance\counter by 1
\repeat

\bye

On my machine this takes 7.968s to typeset. However, when I replace the definition of \onum by

\long\def\onum#1{%
    \begingroup
        \ifcsname\mainfont:+onum\endcsname\else
            \global\expandafter\font\csname\mainfont:+onum\endcsname="\mainfont:+onum" at 10pt
        \fi
        \csname\mainfont:+onum\endcsname#1%
    \endgroup
}

it suddenly only takes 0.796s!

wspr commented 4 years ago

Thanks for the detailed analysis! I am embarrassed to admit that I thought at some point I did eliminate the inefficiency here — but either I didn't do it correctly or it is a false memory.

I'll do what I can to correct this quickly; I have a few fontspec issues piling up at the moment...

hmenke commented 4 years ago

If possible you should add a switch to disable the caching. It might be that LuaTeX garbage-collects unused \fonts, so if you are using like a thousand different fonts in your document, the caching might actually hurt you. Not a common use-case for sure, but maybe something to keep in mind.

ArTourter commented 2 years ago

Has there been any progress on this? Sorry for poking. I am facing this with documents where I use old style num except for siunitx numbers and in tables where lining numbers are used. However, with this configuration, it adds at least 50% to the compilation time. It would be great to have some update.

kwand commented 2 years ago

@wspr Apologies for poking as well, but has there been any progress on this - since it's been almost a year and a half since the last reply?

Would appreciate it if a reply could be made here, even if there has been no progress yet.

wspr commented 2 years ago

@kwand — apologies, I'm having a hard time juggling family and work at the moment and TeX work has slipped behind. I couldn't promise a timeframe for an update on this, I'm sorry.

ddedded commented 8 months ago

I know you guys are busy and have lives, but I am hoping to give this a bump in case it's been forgotten. I'm the asker of the original question on tex.stackexchange that prompted @hmenke to figure out this issue and report it. The performance hit I see on real documents is annoying but not unlivable, but it is annoying.