Skip to content

Commit

Permalink
Improve table search speed through lookups
Browse files Browse the repository at this point in the history
Prior to this change table search would have to do a binary search over
about 1000 entries which resulted in around 10 memory loads on average.
In this commit we reduce the search space by doing a pre-lookup in a
generated table to get a smaller (often zero-length) slice of the full
sorted range list. On average this gives us just one entry of the range
list to perform binary search on, which reduces the average number of
memory loads to 2.
  • Loading branch information
indutny committed Jan 30, 2023
1 parent 243af2c commit bb6b82a
Show file tree
Hide file tree
Showing 2 changed files with 486 additions and 26 deletions.
82 changes: 76 additions & 6 deletions scripts/unicode.py
Expand Up @@ -274,13 +274,36 @@ def emit_break_module(f, break_table, break_cats, name):
pub enum %sCat {
""" % (name, Name, Name))

# We don't want the lookup table to be too large so choose a reasonable
# cutoff. 0x20000 is selected because most of the range table entries are
# within the interval of [0x0, 0x20000]
lookup_value_cutoff = 0x20000

# Length of lookup table. It has to be a divisor of `lookup_value_cutoff`.
lookup_table_len = 0x400

lookup_interval = round(lookup_value_cutoff / lookup_table_len)

# Lookup table is a mapping from `character code / lookup_interval` to
# the index in the range table that covers the `character code`.
lookup_table = [0] * lookup_table_len
j = 0
for i in range(0, lookup_table_len):
lookup_from = i * lookup_interval
while j < len(break_table):
(_, entry_to, _) = break_table[j]
if entry_to >= lookup_from:
break
j += 1
lookup_table[i] = j

break_cats.append("Any")
break_cats.sort()
for cat in break_cats:
f.write((" %sC_" % Name[0]) + cat + ",\n")
f.write(""" }
fn bsearch_range_value_table(c: char, r: &'static [(char, char, %sCat)]) -> (u32, u32, %sCat) {
fn bsearch_range_value_table(c: char, r: &'static [(char, char, %sCat)]) -> (Option<u32>, Option<u32>, %sCat) {
use core::cmp::Ordering::{Equal, Less, Greater};
match r.binary_search_by(|&(lo, hi, _)| {
if lo <= c && c <= hi { Equal }
Expand All @@ -289,23 +312,70 @@ def emit_break_module(f, break_table, break_cats, name):
}) {
Ok(idx) => {
let (lower, upper, cat) = r[idx];
(lower as u32, upper as u32, cat)
(Some(lower as u32), Some(upper as u32), cat)
}
Err(idx) => {
(
if idx > 0 { r[idx-1].1 as u32 + 1 } else { 0 },
r.get(idx).map(|c|c.0 as u32 - 1).unwrap_or(core::u32::MAX),
if idx > 0 { Some(r[idx-1].1 as u32 + 1) } else { None },
r.get(idx).map(|c|c.0 as u32 - 1),
%sC_Any,
)
}
}
}
pub fn %s_category(c: char) -> (u32, u32, %sCat) {
bsearch_range_value_table(c, %s_cat_table)
// Perform a quick O(1) lookup in a precomputed table to determine
// the slice of the range table to search in.
let lookup_interval = 0x%x;
let idx = (c as u32 / lookup_interval) as usize;
let range = %s_cat_lookup.get(idx..(idx + 2)).map_or(
// If the `idx` is outside of the precomputed table - use the slice
// starting from the last covered index in the precomputed table and
// ending with the length of the range table.
%d..%d,
|r| (r[0] as usize)..((r[1] + 1) as usize)
);
let (lower, upper, cat) = bsearch_range_value_table(c, &%s_cat_table[range]);
(
lower.unwrap_or_else(|| {
if idx == 0 {
0
} else {
// Use an entry just before the lookup index to find the lower
// bound for Any category.
let i = *%s_cat_lookup.get(idx - 1).unwrap_or(&%d) as usize;
%s_cat_table[i].1 as u32 + 1
}
}),
upper.unwrap_or_else(|| {
%s_cat_lookup.get(idx + 1).map_or_else(|| {
// If idx was outside of the lookup table - upper bound is either
// already found in the ranges table, or has to be a u32::MAX.
core::u32::MAX
}, |&i| {
// Otherwise use an entry just after the lookup index to find the
// lower bound for Any category.
%s_cat_table[i as usize].0 as u32 - 1
})
}),
cat
)
}
""" % (Name, Name, Name[0], name, Name, name))
""" % (Name, Name, Name[0], name, Name, lookup_interval, name, j, len(break_table), name, name, j, name, name, name))

if len(break_table) <= 0xff:
lookup_type = "u8"
elif len(break_table) <= 0xffff:
lookup_type = "u16"
else:
lookup_type = "u32"

emit_table(f, "%s_cat_lookup" % name, lookup_table, "&'static [%s]" % lookup_type,
pfun=lambda x: "%d" % x,
is_pub=False, is_const=True)

emit_table(f, "%s_cat_table" % name, break_table, "&'static [(char, char, %sCat)]" % Name,
pfun=lambda x: "(%s,%s,%sC_%s)" % (escape_char(x[0]), escape_char(x[1]), Name[0], x[2]),
Expand Down

0 comments on commit bb6b82a

Please sign in to comment.