HarfBuzz Study: Memory Consumption
September 17, 2012
For TL;DR version, jump to Conclusions.
Introduction
I wanted to get a few numbers re new HarfBuzz's memory consumption behavior compared to old HarfBuzz, so I did some experiments, using a tool I have had developed for cairo years ago.
Methodology
Checkout cairo, configure, and go into the util/ subdir,
make malloc-stats.so backtrace-symbols.so
cp malloc-stats.so backtrace-symbols.so ~/.local/lib (Substitute somewhere convenient for ~/.local/lib.)
Build HarfBuzz, and go into util/. Now you can do (all in one line):
G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape /path/to/arial.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt
The backtrace-symbols.so object makes the allocation site annotations much more readable, but is much slower. You can try running without it and compare.
Now, running the above for the ot backend I get:
$ G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape arial.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt
TOTAL MALLOC REALLOC
num size num size num size
1 11 1 11 0 0 new_composite_name()
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 16 1 16 0 0 lang_find_or_insert()
1 32 1 32 0 0 FT_New_Memory()
1 38 1 38 0 0 _g_locale_get_charset_aliases()
1 88 1 88 0 0 __libc_calloc()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
1 1,600 0 0 1 1,600 extend_alias_table()
2 8 2 8 0 0 g_private_impl_new()
2 40 0 0 2 40 ft_mem_qrealloc()
2 3,072 0 0 2 3,072 read_alias_file()
3 1,000 3 1,000 0 0 _hb_ot_layout_create(hb_face_t*)()
3 1,704 3 1,704 0 0 __fopen_internal()
4 1,280 0 0 4 1,280 hb_buffer_t::enlarge(unsigned int)()
6 311 6 311 0 0 *__GI___vasprintf_chk()
9 1,344 9 1,344 0 0 hb_object_header_t::create(unsigned int)()
10 1,180 0 0 10 1,180 g_realloc()
12 132 12 132 0 0 *__GI___strndup()
12 3,344 12 3,344 0 0 _nl_intern_locale_data()
13 143 13 143 0 0 *__GI___strdup()
14 560 14 560 0 0 g_mutex_impl_new()
19 4,204 19 4,204 0 0 g_malloc0()
24 120 24 120 0 0 _nl_normalize_codeset()
30 487 30 487 0 0 g_malloc()
61 43,410 61 43,410 0 0 ft_mem_qalloc()
456 22,242 456 22,242 0 0 _nl_make_l10nflist()
691 87,358 672 80,186 19 7,172 (total)
There’s a lot of churn from libc’s locale routines. We can weed them out by forcing LC_ALL=C. With that adjustment we get:
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape arial.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt
TOTAL MALLOC REALLOC
num size num size num size
1 2 1 2 0 0 *__GI___strdup()
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 16 1 16 0 0 lang_find_or_insert()
1 32 1 32 0 0 FT_New_Memory()
1 38 1 38 0 0 _g_locale_get_charset_aliases()
1 88 1 88 0 0 __libc_calloc()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
2 8 2 8 0 0 g_private_impl_new()
2 40 0 0 2 40 ft_mem_qrealloc()
2 416 2 416 0 0 __gconv_lookup_cache()
2 1136 2 1136 0 0 __fopen_internal()
3 1000 3 1000 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
4 16576 4 16576 0 0 __gconv_open()
6 311 6 311 0 0 *__GI___vasprintf_chk()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
10 1180 0 0 10 1180 g_realloc()
14 560 14 560 0 0 g_mutex_impl_new()
19 4204 19 4204 0 0 g_malloc0()
30 511 30 511 0 0 g_malloc()
61 43410 61 43410 0 0 ft_mem_qalloc()
176 73144 160 70644 16 2500 (total)
This is clean and good. This shows that to render a single letter ‘a’ using Arial, hb-shape did a total of 176 allocation calls, for a total of ~70kb. The ones that really came from HarfBuzz itself can be grepped easily, and that is what we will do in the following sections.
Font Loading Overhead
Let’s run this on a few popular fonts using the new HarfBuzz shaper (ot backend) and the old HarfBuzz shaper (old backend). We use a single-character text as input. Our primary goal here is to measure the font loading overhead of the new vs old shapers when it comes to memory allocation. The overhead includes all memory allocations made by HarfBuzz excluding the hb_buffer_t allocations.
Arial
As shipped in Windows 7.
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape arial.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
3 1000 3 1000 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
That’s good and clean. As expected, HarfBuzz is only making a few small allocations for various object structs (face, font, shape plan, etc), and then the buffer object. As we expect, there is no font data being loaded, since new HarfBuzz uses the mmap()ed font tables directly. The tables are already mmap()ed by FreeType as readonly shared memory and hence they are free to use. The kernel is free to move those pages in and out of the filesystem cache on demand.
Now let’s repeat the same experiment with the old-HarfBuzz backend:
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape arial.ttf a --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
1074 336450 1074 336450 0 0 _hb_alloc()
Sweet. Most is the same, except that now old-HarfBuzz is parsing the GDEF/GSUB/GPOS tables into memory, performing over a thousand malloc()s for a total of over 300kb. This just evaporated with the new HarfBuzz design. Indeed, getting rid of this was my first motivation for redesigning HarfBuzz.
Let’s take a look at a few other fonts.
Tahoma
As shipped in Windows 7.
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shapetahoma.ttf a --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
992 329128 992 329128 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shapetahoma.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
3 1112 3 1112 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Surprisingly similar numbers. Old HarfBuzz making about a thousand calls and allocating over 300kb, nothing in new HarfBuzz, just a few kb of object allocations.
Times New Roman
As shipped in Windows 7.
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape times.ttf a --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached(()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
1058 343618 1058 343618 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape times.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
3 1000 3 1000 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Same story!
Let’s try a few popular Free fonts.
DejaVu Sans
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape DejaVuSans.ttf a --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
756 261448 756 261448 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape DejaVuSans.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
3 968 3 968 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Similar story: over 750 malloc()s, totaling over 250kb.
Droid Sans
As shipped in Android Jelly-Bean.
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape DroidSans.ttf a --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
515 1130938 515 1130938 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape DroidSans.ttf a --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
3 88 3 88 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Extreme case: over 500 allocations totaling over a megabyte!
IranNastaleeq
Let’s try with a fancy Nastaleeq-style Arabic font.
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape IranNastaliq2.ttf ب --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
7340 517316 7340 517316 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape IranNastaliq2.ttf ب --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 144 1 144 0 0 hb_prealloced_array_t::push()()
1 144 1 144 0 0 hb_prealloced_array_t::push()()
1 720 1 720 0 0 hb_prealloced_array_t::push()()
1 896 1 896 0 0 hb_prealloced_array_t::push()()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
3 7384 3 7384 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
5 6952 1 448 4 6504 hb_prealloced_array_t::push()()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Now, new HarfBuzz is making a bit more allocations here than the simple case of Latin. Nothing huge though, totaling to about 20kb and 20 allocations. Old shaper does more than 7000 allocations totaling over 500kb.
Mangal
Let’s look an Indic font now:
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape mangal.ttf क --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
5798 184416 5798 184416 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape mangal.ttf क --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 720 1 720 0 0 hb_prealloced_array_t::push()()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
2 480 1 144 1 336 hb_prealloced_array_t::push()()
2 480 1 144 1 336 hb_prealloced_array_t::push()()
2 2464 1 896 1 1568 hb_prealloced_array_t::push()()
3 1336 3 1336 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Same pattern: new HarfBuzz makes around 20 allocations totaling ~10kb. Old HarfBuzz does over 5000 allocations totaling over 180kb.
Lohit Devanagari
And a Free Indic font:
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape LohitDevanagari.ttf क --no-glyph-names --output-file /dev/null --shaper old | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 32 1 32 0 0 _hb_old_shaper_font_data_create()
1 112 1 112 0 0 HB_NewFace()
1 160 0 0 1 160 _hb_realloc()
2 6 0 0 2 6 HB_OpenTypeShape()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
12 1704 12 1704 0 0 hb_object_header_t::create(unsigned int)()
860 35202 860 35202 0 0 _hb_alloc()
$ LC_ALL=C G_SLICE=always-malloc LD_PRELOAD=~/.local/lib/malloc-stats.so:~/.local/lib/backtrace-symbols.so libtool --mode=execute ./hb-shape LohitDevanagari.ttf क --no-glyph-names --output-file /dev/null --shaper ot | c++filt | grep -i hb_
1 16 1 16 0 0 hb_shape_plan_create_cached()
1 16 1 16 0 0 _hb_face_for_data_closure_create()
1 720 1 720 0 0 hb_prealloced_array_t::push()()
1 960 1 960 0 0 _hb_ot_shaper_shape_plan_data_create()
2 480 1 144 1 336 hb_prealloced_array_t::push()()
2 480 1 144 1 336 hb_prealloced_array_t::push()()
2 1184 1 448 1 736 hb_prealloced_array_t::push()()
2 2464 1 896 1 1568 hb_prealloced_array_t::push()()
3 1544 3 1544 0 0 _hb_ot_layout_create(hb_face_t*)()
4 1280 0 0 4 1280 hb_buffer_t::enlarge(unsigned int)()
9 1344 9 1344 0 0 hb_object_header_t::create(unsigned int)()
Similar results, though less dramatic. New HarfBuzz makes ~25 allocation totaling ~10kb. Old HarfBuzz does over 800 allocations totaling over 35kb. Looks like Lohit fonts are fairly light in their OpenType tables.
Per Character Memory
New HarfBuzz uses 40 bytes per input character. This memory is managed by the hb_buffer_t, which can be reused for subsequent shaping calls and avoid recurring reallocations.
Old shaper has the same 40 byte requirement for the layout engine processing, plus the input arrays that the caller needs to manage, adding up to at least another 20 bytes per character.
Discussion
While the numbers (savings of over 99%) may sound surprising, they are not. Let’s take the extreme Droid Sans case for example. It has a simple GPOS table consisting of only a ‘kern’ feature, totalling about 42kb of raw font data.
OpenType tables consist of dozens of types of different structs, referencing each other using 16bit offset numbers. Now, when old HarfBuzz code tries to load that table into memory, it loads it into structs that closely resemble the original OpenType structs, but use real pointers instead of offsets. As a result, say, a simple struct that consists of a glyph ID and an offset in the font, consuming 4 bytes, will end up consuming 16 bytes on a 64-bit machine and struct packing enabled. More memory waste happens in parsing OpenType GPOS “Value” tables where wire encoding uses a bit field to encode which, out of sixteen different values, it encodes, but the C struct mapping needs to allocate for all the possible values. The result is that a simple kerning value taking two bytes in the font can take 32 bytes or more in memory the way old HarfBuzz works. With new HarfBuzz we simply access the two bytes in the shared readonly memory instead.
Now, one can reduce memory usage by using packed structs on architectures that allow it. Indeed, that used to be the case with some versions of HarfBuzz, but such code will be inefficient, and plain crashing on architectures that do not support it, so I removed it. At best, struct packing can reduce memory usage up to close to 50% (but never more than that).
All the measurements are on x64 systems. A struct that takes 16 bytes on this system may only take 8 bytes on 32-bit systems. So that is another point to keep in mind.
Still, new HarfBuzz showed over 99% memory savings in many cases. Even with struct packing, and on 32-bit architectures, that number will be at 96% or better.
Final note: the numbers involved do not include malloc() overhead. Every malloc also takes 2*sizeof(void*) overhead for bookkeeping purposes. While one can use a custom memory pool to avoid that cost, that has not been the case with old HarfBuzz. But we discount that.
Conclusions
New HarfBuzz is considerably more memory efficient than the old HarfBuzz. In general, new HarfBuzz makes about a 10 to 20 allocations per font-file, totaling a few kilobytes for simple scripts or up to a couple tens of kilobytes for more complex scripts. Old HarfBuzz however can easily make hundreds or thousands of allocations adding up to hundreds of kilobytes for very common fonts.
What this means for a browser like Chromium that uses multiple renderer processes, the savings, assuming two to three fonts used per web page, can easily add up to over 500kb per process. On a mobile device with 256MB of memory, that can add up to significant memory saving.
Here is a summary table of font loading overhead for a few popular fonts:
Font | New # allocs | New KBs | Old # allocs | Old KBs | Saving % |
Arial | 15 | 3 | >1000 | 330 | >99% |
Tahoma | 15 | 3 | >1000 | 323 | >99% |
Times New Roman | 15 | 3 | >1000 | 337 | >99% |
DejaVu Sans | 15 | 3 | >750 | 257 | >98% |
Droid Sans | 15 | 2 | >500 | 1100 | 99.8% |
IranNastaleeq | 20 | 12 | >7000 | 505 | >97% |
Mangal | 19 | 5 | >5800 | 180 | >97% |
Lohit Devanagari | 20 | 6 | >860 | 35 | 82% |
The numbers do not need any explanation. For many common OpenType fonts as shipped on Windows, ChromeOS, and Android, new HarfBuzz saves over 99% in font loading memory overhead compared to old HarfBuzz currently used in ChromeOS and Android.
All measurements are done on an x64 intel system. Memory consumption on x86 may be half that of x64 and the number may be adjusted accordingly. For example, a 99% memory saving may only be a 98% saving on x86 systems.
As for per-character memory, as we discussed, new HarfBuzz consumes at least 33% less memory than old HarfBuzz.