Skip to content
This repository was archived by the owner on Jan 21, 2026. It is now read-only.

Commit e826905

Browse files
author
Vicent Marti
committed
tags: Use a perfect hash for lookups
The previous version using `strcasecmp` over an array was a bottleneck on the library. This version uses a simple, minimal perfect hash table (computed via `mph`) to convert tag names into strings. Since we're now hashing tag names, we can pass in the length of the tag name explicitly, and avoid the superfluous allocations that the tokenizer was performing in order to NULL-terminate the tag. This is implemented on the new `gumbo_tagn_enum` API. The old `gumbo_tag_enum` API has been left as a thin wrapper to keep backwards compatibility -- it is not used internally by the library. `mph` was chosen for the perfect hash function because it generates hashes that are slightly slower than GPerf but significantly simpler, and occuppying an order of magnitude less memory (as they don't need a full copy of all the strings in the set for hashing). If the tag lookup function proves to be a bottleneck, this decision can be re-evaluated in the future.
1 parent e1b3158 commit e826905

4 files changed

Lines changed: 269 additions & 11 deletions

File tree

src/gumbo.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,10 @@ const char* gumbo_normalize_svg_tagname(const GumboStringPiece* tagname);
365365

366366
/**
367367
* Converts a tag name string (which may be in upper or mixed case) to a tag
368-
* enum.
368+
* enum. The `tag` version expects `tagname` to be NULL-terminated
369369
*/
370370
GumboTag gumbo_tag_enum(const char* tagname);
371+
GumboTag gumbo_tagn_enum(const char* tagname, int length);
371372

372373
/**
373374
* Attribute namespaces.

src/tag.c

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <assert.h>
2020
#include <ctype.h>
2121
#include <strings.h> // For strcasecmp.
22+
#include <string.h> // For strcasecmp.
2223

2324
// NOTE(jdtang): Keep this in sync with the GumboTag enum in the header.
2425
// TODO(jdtang): Investigate whether there're efficiency benefits to putting the
@@ -213,6 +214,7 @@ void gumbo_tag_from_original_text(GumboStringPiece* text) {
213214
}
214215
}
215216

217+
#ifdef SLOW_TAG_LOOKUP
216218
GumboTag gumbo_tag_enum(const char* tagname) {
217219
for (int i = 0; i < GUMBO_TAG_LAST; ++i) {
218220
// TODO(jdtang): strcasecmp is non-portable, so if we want to support
@@ -224,3 +226,114 @@ GumboTag gumbo_tag_enum(const char* tagname) {
224226
}
225227
return GUMBO_TAG_UNKNOWN;
226228
}
229+
#else
230+
231+
/*
232+
* Generated with `mph`
233+
* ./mph -d2 -m2 -c1.33 < tag.in | emitc -s -l
234+
*/
235+
static int hash_tag(const unsigned char *kp, int len)
236+
{
237+
static short g[] = {
238+
87, -1, -1, 54, 37, -1, 0, 63, -1, 4,
239+
87, 132, 149, -1, 43, 103, 78, 89, 126, 74,
240+
9, -1, 32, 68, 46, 132, 14, -1, -1, 147,
241+
77, 120, 101, 138, 38, -1, 135, 24, 94, -1,
242+
36, 88, 101, 29, -1, 83, 122, -1, 126, 148,
243+
145, 46, 90, 94, 83, 140, -1, 4, -1, 103,
244+
25, 0, 0, 129, 138, 0, 138, 53, -1, 0,
245+
77, 43, 0, -1, 90, 22, 30, 109, 71, 1,
246+
-1, 94, 20, -1, 27, 56, 0, 21, 72, 122,
247+
-1, -1, 0, 142, 72, 5, 11, 7, 43, 111,
248+
89, 96, 81, 48, 65, 27, 5, 73, -1, 57,
249+
137, 52, 0, 60, -1, 3, -1, 100, 149, 41,
250+
98, 118, 81, 0, 50, 30, -1, -1, 83, 10,
251+
20, 25, 2, 0, 118, 9, 39, 94, 35, 42,
252+
23, 75, 89, 31, 0, 148, 86, 6, 115, -1,
253+
49, 107, 5, 90, 4, 12, -1, 21, 16, -1,
254+
29, 39, -1, 96, 111, 96, 43, 43, 120, -1,
255+
46, 84, -1, 0, 146, 126, 24, -1, 28, 110,
256+
82, 42, 12, 84, -1, -1, -1, 0, 33, 12,
257+
86, 93, -1, 147, 95, 58, 90, 145, -1, -1,
258+
};
259+
260+
static unsigned char T0[] = {
261+
196, 103, 27, 185, 60, 0, 58, 36, 180, 118,
262+
101, 180, 61, 125, 144, 167, 140, 104, 131, 195,
263+
176, 62, 79, 175, 195, 103, 116, 194, 122, 73,
264+
44, 119, 128, 23, 56, 188, 23, 114, 24, 156,
265+
32, 78, 136, 46, 3, 32, 165, 95, 136, 97,
266+
90, 65, 111, 121, 40, 106, 25, 108, 53, 99,
267+
181, 49, 18, 110, 72, 74, 50, 48, 141, 27,
268+
4, 125, 105, 92, 171, 60, 124, 1, 72, 96,
269+
178, 59, 58, 61, 0, 185, 12, 176, 111, 121,
270+
49, 170, 70, 48, 43, 82, 178, 157, 34, 62,
271+
137, 148, 110, 160, 96, 11, 50, 22, 12, 74,
272+
71, 143, 133, 129, 4, 86, 67, 168, 62, 130,
273+
41, 63, 101, 63, 112, 96, 146, 90, 5, 132,
274+
153, 95, 32, 15, 7, 80, 26, 57, 103, 191,
275+
83, 126, 134, 169, 55, 90, 55, 74, 58, 69,
276+
5, 99, 132, 58,
277+
};
278+
279+
static unsigned char T1[] = {
280+
87, 14, 91, 162, 194, 198, 131, 1, 89, 2,
281+
154, 17, 98, 25, 7, 121, 145, 178, 28, 70,
282+
94, 135, 77, 129, 134, 137, 69, 128, 88, 126,
283+
114, 175, 92, 5, 89, 87, 3, 20, 88, 44,
284+
174, 194, 14, 73, 171, 21, 194, 117, 151, 175,
285+
139, 45, 110, 17, 127, 196, 106, 148, 124, 194,
286+
26, 190, 169, 118, 195, 59, 157, 150, 31, 197,
287+
147, 6, 143, 161, 79, 67, 134, 68, 163, 61,
288+
104, 124, 56, 39, 115, 99, 140, 101, 63, 91,
289+
124, 4, 134, 110, 132, 61, 150, 96, 116, 167,
290+
80, 174, 115, 169, 14, 184, 24, 47, 4, 188,
291+
60, 109, 64, 68, 148, 179, 168, 41, 80, 183,
292+
84, 156, 187, 18, 18, 119, 79, 169, 168, 148,
293+
88, 0, 122, 3, 169, 88, 139, 146, 88, 144,
294+
86, 148, 5, 150, 17, 105, 81, 137, 98, 113,
295+
120, 182, 69, 107,
296+
};
297+
298+
int i, n;
299+
unsigned int f0, f1;
300+
301+
if (len < 1 || len > 14)
302+
return -1;
303+
304+
for (i=-45, f0=f1=0, n=0; n < len; ++n) {
305+
int c = tolower(kp[n]);
306+
if (c < 45 || c > 121)
307+
return -1;
308+
f0 += T0[i + c];
309+
f1 += T1[i + c];
310+
i += 77;
311+
if (i >= 109)
312+
i = -45;
313+
}
314+
return (g[f0 % 200] + g[f1 % 200]) % 150;
315+
}
316+
317+
static int
318+
case_memcmp(const char *s1, const char *s2, int n)
319+
{
320+
while (n--) {
321+
unsigned char c1 = tolower(*s1++);
322+
unsigned char c2 = tolower(*s2++);
323+
if (c1 != c2)
324+
return (int)c1 - (int)c2;
325+
}
326+
return 0;
327+
}
328+
329+
GumboTag gumbo_tagn_enum(const char* tagname, int length) {
330+
int position = hash_tag((const unsigned char *)tagname, length);
331+
if (position >= 0 && !case_memcmp(tagname, kGumboTagNames[position], length))
332+
return (GumboTag)position;
333+
return GUMBO_TAG_UNKNOWN;
334+
}
335+
336+
GumboTag gumbo_tag_enum(const char* tagname) {
337+
return gumbo_tagn_enum(tagname, strlen(tagname));
338+
}
339+
#endif

src/tag.in

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
html
2+
head
3+
title
4+
base
5+
link
6+
meta
7+
style
8+
script
9+
noscript
10+
template
11+
body
12+
article
13+
section
14+
nav
15+
aside
16+
h1
17+
h2
18+
h3
19+
h4
20+
h5
21+
h6
22+
hgroup
23+
header
24+
footer
25+
address
26+
p
27+
hr
28+
pre
29+
blockquote
30+
ol
31+
ul
32+
li
33+
dl
34+
dt
35+
dd
36+
figure
37+
figcaption
38+
main
39+
div
40+
a
41+
em
42+
strong
43+
small
44+
s
45+
cite
46+
q
47+
dfn
48+
abbr
49+
data
50+
time
51+
code
52+
var
53+
samp
54+
kbd
55+
sub
56+
sup
57+
i
58+
b
59+
u
60+
mark
61+
ruby
62+
rt
63+
rtc
64+
rp
65+
bdi
66+
bdo
67+
span
68+
br
69+
wbr
70+
ins
71+
del
72+
image
73+
img
74+
iframe
75+
embed
76+
object
77+
param
78+
video
79+
audio
80+
source
81+
track
82+
canvas
83+
map
84+
area
85+
math
86+
mi
87+
mo
88+
mn
89+
ms
90+
mtext
91+
mglyph
92+
malignmark
93+
annotation-xml
94+
svg
95+
foreignobject
96+
desc
97+
table
98+
caption
99+
colgroup
100+
col
101+
tbody
102+
thead
103+
tfoot
104+
tr
105+
td
106+
th
107+
form
108+
fieldset
109+
legend
110+
label
111+
input
112+
button
113+
select
114+
datalist
115+
optgroup
116+
option
117+
textarea
118+
keygen
119+
output
120+
progress
121+
meter
122+
details
123+
summary
124+
menu
125+
menuitem
126+
applet
127+
acronym
128+
bgsound
129+
dir
130+
frame
131+
frameset
132+
noframes
133+
isindex
134+
listing
135+
xmp
136+
nextid
137+
noembed
138+
plaintext
139+
rb
140+
strike
141+
basefont
142+
big
143+
blink
144+
center
145+
font
146+
marquee
147+
multicol
148+
nobr
149+
spacer
150+
tt

src/tokenizer.c

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -742,11 +742,9 @@ static void finish_tag_name(GumboParser* parser) {
742742
GumboTokenizerState* tokenizer = parser->_tokenizer_state;
743743
GumboTagState* tag_state = &tokenizer->_tag_state;
744744

745-
const char* temp;
746-
copy_over_tag_buffer(parser, &temp);
747-
tag_state->_tag = gumbo_tag_enum(temp);
745+
tag_state->_tag = gumbo_tagn_enum(
746+
tag_state->_buffer.data, tag_state->_buffer.length);
748747
reinitialize_tag_buffer(parser);
749-
gumbo_free((void*) temp);
750748
}
751749

752750
// Adds an ERR_DUPLICATE_ATTR parse error to the parser's error struct.
@@ -832,13 +830,9 @@ static void finish_attribute_value(GumboParser* parser) {
832830
static bool is_appropriate_end_tag(GumboParser* parser) {
833831
GumboTagState* tag_state = &parser->_tokenizer_state->_tag_state;
834832
assert(!tag_state->_is_start_tag);
835-
// Null terminate the current string buffer, so it can be passed to
836-
// gumbo_tag_enum, but don't increment the length in case we need to dump the
837-
// buffer as character tokens.
838-
gumbo_string_buffer_append_codepoint('\0', &tag_state->_buffer);
839-
--tag_state->_buffer.length;
840833
return tag_state->_last_start_tag != GUMBO_TAG_LAST &&
841-
tag_state->_last_start_tag == gumbo_tag_enum(tag_state->_buffer.data);
834+
tag_state->_last_start_tag ==
835+
gumbo_tagn_enum(tag_state->_buffer.data, tag_state->_buffer.length);
842836
}
843837

844838
void gumbo_tokenizer_state_init(

0 commit comments

Comments
 (0)