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.