Fontations benchmark on different font technology
behdad
March 10, 2025
Methodology
I’m using hb/perf/benchmark-font.cc using the new hb-fontations work. The overhead of the hb-fontations font funcs seems to be negligible as I will argue in the text. I’m testing against Chad’s gvar speedup branch. (PR) Sample run:
$ ninja -Cbuild perf/benchmark-font && \
build/perf/benchmark-font Roboto-Regular.ttf --benchmark_filter=draw
I’ve configured my CPU scaling to performance:
echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
CPU Results
Simple TrueType font
Testing Roboto-Regular.ttf.
Benchmark Time CPU Iterations
-----------------------------------------------------------------------------------------------
BM_Font/draw_glyph/Roboto-Regular.ttf/ot 0.376 ms 0.374 ms 1859
BM_Font/draw_glyph/Roboto-Regular.ttf/ft 0.650 ms 0.647 ms 1065
BM_Font/draw_glyph/Roboto-Regular.ttf/fontations 0.600 ms 0.598 ms 1165
BM_Font/draw_glyph/Roboto-Regular.ttf/var/ot 0.377 ms 0.375 ms 1877
BM_Font/draw_glyph/Roboto-Regular.ttf/var/ft 0.649 ms 0.647 ms 1078
BM_Font/draw_glyph/Roboto-Regular.ttf/var/fontations 0.602 ms 0.600 ms 1166
Notably, fontations is faster than FreeType now.
Roboto-Regular.ttf is not a variable font. So, would be nice to gain the wasted time setting up location and variations in fontations.
Simple variable TrueType (glyf+gvar) font
We test NotoSans-VF.ttf.
Benchmark Time CPU Iterations
--------------------------------------------------------------------------------------------
BM_Font/draw_glyph/NotoSans-VF.ttf/ot 1.19 ms 1.18 ms 576
BM_Font/draw_glyph/NotoSans-VF.ttf/ft 2.11 ms 2.10 ms 332
BM_Font/draw_glyph/NotoSans-VF.ttf/fontations 1.86 ms 1.85 ms 375
BM_Font/draw_glyph/NotoSans-VF.ttf/var/ot 1.78 ms 1.77 ms 398
BM_Font/draw_glyph/NotoSans-VF.ttf/var/ft 3.78 ms 3.77 ms 185
BM_Font/draw_glyph/NotoSans-VF.ttf/var/fontations 2.70 ms 2.69 ms 260
Looks like with the PR branch of fontations, it’s now splitting the difference between FreeType and HarfBuzz. Great start.
I like to note that the hb-fontations overhead is not significant, as Roboto-Regular.ttf runs very fast.
Monster variable TrueType font
Where we test RobotoFlex-VF.ttf, which has 81 masters.
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------
BM_Font/draw_glyph/RobotoFlex.ttf/ot 0.285 ms 0.284 ms 2467
BM_Font/draw_glyph/RobotoFlex.ttf/ft 0.738 ms 0.735 ms 949
BM_Font/draw_glyph/RobotoFlex.ttf/fontations 0.469 ms 0.468 ms 1496
BM_Font/draw_glyph/RobotoFlex.ttf/var/ot 1.52 ms 1.51 ms 460
BM_Font/draw_glyph/RobotoFlex.ttf/var/ft 3.03 ms 3.01 ms 231
BM_Font/draw_glyph/RobotoFlex.ttf/var/fontations 2.20 ms 2.19 ms 321
Again, new fontations PR seems to split the time between FreeType and Harfbuzz. Great work, since before this PR fontations was 3 times slower than FreeType for the variable case.
Simple non-variable CFF font
We test NotoSansCJKkr-Regular.otf.
Benchmark Time CPU Iterations
------------------------------------------------------------------------------------------------------
BM_Font/draw_glyph/NotoSansCJKkr-Regular.otf/ot 108 ms 108 ms 6
BM_Font/draw_glyph/NotoSansCJKkr-Regular.otf/ft 348 ms 347 ms 2
BM_Font/draw_glyph/NotoSansCJKkr-Regular.otf/fontations 160 ms 160 ms 4
BM_Font/draw_glyph/NotoSansCJKkr-Regular.otf/var/ot 109 ms 108 ms 6
BM_Font/draw_glyph/NotoSansCJKkr-Regular.otf/var/ft 350 ms 349 ms 2
BM_Font/draw_glyph/NotoSansCJKkr-Regular.otf/var/fontations 160 ms 160 ms 4
The CFF loader in fontations seems to be significantly faster than FreeType, and kinda close to HarfBuzz. Fontations also is not paying any significant cost setting up the non-existing variables.
Variable CFF2 font
We test AdobeVFPrototype.otf.
Benchmark Time CPU Iterations
-------------------------------------------------------------------------------------------------
BM_Font/draw_glyph/AdobeVFPrototype.otf/ot 0.888 ms 0.885 ms 763
BM_Font/draw_glyph/AdobeVFPrototype.otf/ft 1.22 ms 1.22 ms 577
BM_Font/draw_glyph/AdobeVFPrototype.otf/fontations 1.33 ms 1.33 ms 533
BM_Font/draw_glyph/AdobeVFPrototype.otf/var/ot 0.923 ms 0.919 ms 763
BM_Font/draw_glyph/AdobeVFPrototype.otf/var/ft 1.22 ms 1.22 ms 579
BM_Font/draw_glyph/AdobeVFPrototype.otf/var/fontations 1.33 ms 1.33 ms 522
Fontations is doing an okay job. Still slower than FreeType. Also, looks like it’s doing var processing even for the default instance case.
COLRv1 painting
For this benchmark, I’m using --benchmark_filter=paint. Font used is NotoColorEmoji-Regular.ttf.
Benchmark Time CPU Iterations
--------------------------------------------------------------------------------------------------------
BM_Font/paint_glyph/NotoColorEmoji-Regular.ttf/ot 4.29 ms 4.27 ms 167
BM_Font/paint_glyph/NotoColorEmoji-Regular.ttf/ft 20.8 ms 20.7 ms 34
BM_Font/paint_glyph/NotoColorEmoji-Regular.ttf/fontations 16.3 ms 16.3 ms 43
BM_Font/paint_glyph/NotoColorEmoji-Regular.ttf/var/ot 4.18 ms 4.16 ms 165
BM_Font/paint_glyph/NotoColorEmoji-Regular.ttf/var/ft 20.8 ms 20.7 ms 34
BM_Font/paint_glyph/NotoColorEmoji-Regular.ttf/var/fontations 16.3 ms 16.2 ms 43
While faster than FreeType, Fontations has a lot of room to match HarfBuzz. That said, Fontations does color-line resolving that HarfBuzz does not currently do. I would add it to HarfBuzz if the Fontations code for it was not so complicated. We also have that code in hb-cairo, but not sure if it matches the one in Fontations.
Memory results
Whereas we count number of mallocs performed by hb-fontations. I’m hoping that the rust allocations show up as malloc.
Methodology
I’m using my malloc-stats tool. There’s a PR open there to make it work on Mac.
Sample run:
ninja -Cbuild perf/benchmark-font &&
LD_PRELOAD=~/malloc-stats/malloc-stats.so \
build/perf/benchmark-font Roboto-Regular.ttf --benchmark_filter=draw.*ttf/ot --benchmark_repetitions=1 --benchmark_min_time=.0001s |
grep harfbuzz |
tail
Note that the ot there is the HarfBuzz-internal implementation of FontFuncs.
This will produce an output like this (headers added for readability):
TOTAL MALLOC REALLOC
num size num size num size
1 96 0 0 1 96 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(+0x76efb) [0x7f6854ac4efb]
1 152 1 152 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(+0x62d96) [0x7f6854ab0d96]
1 184 1 184 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_font_funcs_create+0x13) [0x7f6854a8d4e3]
1 192 0 0 1 192 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_face_set_user_data+0x30d) [0x7f6854a8170d]
1 208 1 208 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(+0x41e81) [0x7f6854a8fe81]
1 472 1 472 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_face_create_for_tables+0x2c) [0x7f6854a8118c]
1 1,024 1 1,024 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_ot_font_set_funcs+0x11a) [0x7f6854aacd0a]
2 80 2 80 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_ot_font_set_funcs+0x20) [0x7f6854aacc10]
5 4,716 0 0 5 4,716 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(+0x67e73) [0x7f6854ab5e73]
7 336 7 336 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_blob_create_or_fail+0x33) [0x7f6854a72dc3]
So, HarfBuzz ot funcs are making a dozen or so allocations.
To see where the allocations are happening, I use the excellent addr2line tool, with the addresses reported by malloc-stats. For example, the most memory is allocated by HarfBuzz at relative address 0x67e73. Passing that to addr2line, asking to show inline functions as well:
$ addr2line -i --demangle -e build/src/libharfbuzz.so +0x67e73
/home/behdad/harfbuzz/build/../src/hb-vector.hh:461 (discriminator 1)
/home/behdad/harfbuzz/build/../src/OT/glyf/SimpleGlyph.hh:191 (discriminator 1)
which shows the SimpleGlyph scratch warming up.
Simple non-variable TrueType font
With Roboto-Regular.ttf.
HarfBuzz tends to be malloc-free per draw call. This was done recently by caching a glyf-scratch area on the face (threadsafely) and reuse it for each draw call, to avoid mallocations as the vectors there are already hot and allocated.
We only saw the HarfBuzz hb-ot-font results above. Now with FreeType:
1 472 1 472 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_face_create_for_tables+0x2c) [0x7f6bd135318c]
1 1,024 1 1,024 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_ot_font_set_funcs+0x11a) [0x7f6bd137ed0a]
1 1,088 1 1,088 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(+0x114c03) [0x7f6bd1434c03]
2 368 2 368 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_font_funcs_create+0x13) [0x7f6bd135f4e3]
4 192 4 192 0 0 /home/behdad/harfbuzz/build/perf/../src/libharfbuzz.so.0(hb_blob_create_or_fail+0x33) [0x7f6bd1344dc3]
5 120 5 120 0 0 /home/behdad/freetype/objs/.libs/libfreetype.so.6(FT_Add_Module+0x15a) [0x7f6bd0c89a3a]
15 2,929 15 2,929 0 0 /home/behdad/freetype/objs/.libs/libfreetype.so.6(+0x17d34) [0x7f6bd0c86d34]
20 28,552 0 0 20 28,552 /home/behdad/freetype/objs/.libs/libfreetype.so.6(+0x17d83) [0x7f6bd0c86d83]
34 5,016 34 5,016 0 0 /home/behdad/freetype/objs/.libs/libfreetype.so.6(+0x16444) [0x7f6bd0c85444]
709 17,000 709 17,000 0 0 /home/behdad/freetype/objs/.libs/libfreetype.so.6(+0x17cac) [0x7f6bd0c86cac]
We can deduce that FreeType does a significant number of allocations, but still less than the number of glyphs, which hb-info tells us is 1294 in this font.
Now onto Fontations: Unfortunately I cannot see the Rust memory allocations. So, this experiment is a failure, and I’m not repeating it for different font formats. Suffice it to say that HarfBuzz is malloc-free per draw call for all of them (including VARC as well).
UPDATE: See follow-up experimentation with memory consumption here.