diff --git a/Cargo.toml b/Cargo.toml index 6889675..bf3a612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ phf = { version = "0.8.0", features = ["macros"] } structopt = "0.3.5" [build-dependencies] -fastrie = "0.0.6" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" diff --git a/bench/bench.sh b/bench/bench.sh index 76d59ed..70cf909 100755 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -4,7 +4,7 @@ set -e shopt -s nullglob for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do - echo performance | sudo tee /sys/devices/system/cpu/cpu"$i"/cpufreq/scaling_governor + echo performance | sudo dd status=none of="$i" done node sizes.js diff --git a/bench/hyperbuild-bench/Cargo.toml b/bench/hyperbuild-bench/Cargo.toml index 76b466d..e1fe315 100644 --- a/bench/hyperbuild-bench/Cargo.toml +++ b/bench/hyperbuild-bench/Cargo.toml @@ -10,3 +10,6 @@ hyperbuild = { path = "../.." } structopt = "0.3.5" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" + +[profile.release] +debug = true diff --git a/bench/profile.sh b/bench/profile.sh index e55835e..3d3ac9b 100755 --- a/bench/profile.sh +++ b/bench/profile.sh @@ -4,7 +4,7 @@ set -e shopt -s nullglob for i in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do - echo performance | sudo tee /sys/devices/system/cpu/cpu"$i"/cpufreq/scaling_governor + echo performance | sudo dd status=none of="$i" done rm -f perf.data diff --git a/build.rs b/build.rs index 44e5bea..a42bad3 100644 --- a/build.rs +++ b/build.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use std::env; use std::fs::File; use std::io::Write; +use std::ops::{Index, IndexMut}; use std::path::Path; -use fastrie::{FastrieBuild, FastrieBuilderNode}; use serde::{Deserialize, Serialize}; fn create_byte_string_literal(bytes: &[u8]) -> String { @@ -47,44 +47,61 @@ fn camel_case(n: &Vec) -> String { )).collect::>().join("") } +pub struct TwoDimensionalArray { + data: Vec, + cols: usize, +} + +impl TwoDimensionalArray { + pub fn new(rows: usize, cols: usize) -> TwoDimensionalArray { + TwoDimensionalArray { + data: vec![0usize; rows * cols], + cols, + } + } + + pub fn prebuilt(data: Vec, cols: usize) -> TwoDimensionalArray { + TwoDimensionalArray { data, cols } + } +} + +type TwoDimensionalArrayIndex = (usize, usize); + +impl Index for TwoDimensionalArray { + type Output = usize; + + fn index(&self, (row, col): TwoDimensionalArrayIndex) -> &Self::Output { + &self.data[row * self.cols + col] + } +} + +impl IndexMut for TwoDimensionalArray { + fn index_mut(&mut self, (row, col): TwoDimensionalArrayIndex) -> &mut Self::Output { + &mut self.data[row * self.cols + col] + } +} + fn build_pattern(pattern: String) -> String { assert!(pattern.is_ascii()); let seq = pattern.as_bytes(); - let mut max_prefix_len = 0usize; - let mut table = vec![0usize; seq.len()]; + let dfa = &mut TwoDimensionalArray::new(256, seq.len()); - let mut i = 1; - while i < seq.len() { - if seq[i] == seq[max_prefix_len] { - max_prefix_len += 1; - table[i] = max_prefix_len; - i += 1; - } else { - if max_prefix_len != 0 { - max_prefix_len = table[max_prefix_len - 1]; - } else { - table[i] = 0; - i += 1; - }; + dfa[(seq[0] as usize, 0)] = 1; + let mut x = 0; + let mut j = 1; + while j < seq.len() { + for c in 0..256 { + dfa[(c, j)] = dfa[(c, x)]; }; + dfa[(seq[j] as usize, j)] = j + 1; + x = dfa[(seq[j] as usize, x)]; + j += 1; }; - format!("crate::pattern::SinglePattern {{ seq: {}, table: &[{}] }}", - create_byte_string_literal(pattern.as_bytes()), - table.iter().map(|v| v.to_string()).collect::>().join(", ")) -} - -fn generate_fastrie_code(var_name: &str, value_type: &str, built: &FastrieBuild) -> String { - format!(r" - pub static {var_name}: &fastrie::Fastrie<{value_type}> = &fastrie::Fastrie::<{value_type}>::from_prebuilt( - &[{values}], - &[{data}], - ); - ", - var_name = var_name, - value_type = value_type, - values = built.values.join(", "), - data = built.data.iter().map(|v| v.to_string()).collect::>().join(", "), + format!( + "crate::pattern::SinglePattern::prebuilt(&[{}], {})", + dfa.data.iter().map(|v| v.to_string()).collect::>().join(", "), + seq.len(), ) } @@ -173,12 +190,90 @@ struct Entity { characters: String, } +pub struct TrieBuilderNode { + value: Option, + children: Vec>, +} + +struct TrieBuilderGenerationContext<'t, 'v, 'o> { + trie_name: &'t str, + value_type: &'v str, + next_id: usize, + out: &'o mut String, +} + +impl<'t, 'v, 'o> TrieBuilderGenerationContext<'t, 'v, 'o> { + pub fn id(&mut self) -> usize { + let next = self.next_id; + self.next_id += 1; + next + } +} + +impl TrieBuilderNode { + pub fn new() -> TrieBuilderNode { + let mut children = Vec::new(); + for _ in 0..256 { + children.push(None); + }; + TrieBuilderNode { value: None, children } + } + + pub fn add(&mut self, seq: &[u8], value: String) -> () { + let mut current = self; + for c in seq.iter() { + current = current.children[*c as usize].get_or_insert_with(|| TrieBuilderNode::new()); + }; + current.value.replace(value); + } + + fn _generated_node_var_name(&self, trie_name: &str, node_id: usize) -> String { + format!("{trie_name}_NODE_{node_id}", trie_name = trie_name, node_id = node_id) + } + + fn _generate(&self, ctx: &mut TrieBuilderGenerationContext) -> usize { + let children = self.children.iter().map(|c| match c { + None => "None".to_string(), + Some(c) => { + let child_id = c._generate(ctx); + format!("Some({})", self._generated_node_var_name(ctx.trie_name, child_id)) + } + }).collect::>().join(", "); + let id = ctx.id(); + let code = format!( + "static {var_name}: &'static crate::pattern::TrieNode<{value_type}> = &crate::pattern::TrieNode {{\n\tvalue: {value},\n\tchildren: [{children}],\n}};\n\n", + var_name = self._generated_node_var_name(ctx.trie_name, id), + value_type = ctx.value_type, + value = self.value.as_ref().map_or("None".to_string(), |v| format!("Some({})", v)), + children = children, + ); + ctx.out.push_str(code.as_str()); + id + } + + pub fn generate(&self, trie_name: &str, value_type: &str) -> String { + let mut out = String::new(); + let mut ctx = TrieBuilderGenerationContext { + trie_name, + value_type, + next_id: 0, + out: &mut out, + }; + let root_id = self._generate(&mut ctx); + // Make root node public and use proper name. + ctx.out.replace( + format!("static {}", self._generated_node_var_name(trie_name, root_id)).as_str(), + format!("pub static {}", trie_name).as_str() + ) + } +} + fn generate_entities() { // Read named entities map from JSON file. let entities: HashMap = read_json("entities"); // Add entities to trie builder. - let mut trie_builder: FastrieBuilderNode = FastrieBuilderNode::new(); + let mut trie_builder: TrieBuilderNode = TrieBuilderNode::new(); for (rep, entity) in entities { let val = if rep.as_bytes().len() < entity.characters.as_bytes().len() { // Since we're minifying in place, we need to guarantee we'll never write something longer than source. @@ -191,10 +286,9 @@ fn generate_entities() { trie_builder.add(&(rep.as_bytes())[1..], val); }; // Write trie code to output Rust file. - write_rs("entities", generate_fastrie_code( + write_rs("entities", trie_builder.generate( "ENTITY_REFERENCES", "&'static [u8]", - &trie_builder.prebuild(), )); } @@ -208,33 +302,8 @@ fn generate_patterns() { }; } -#[derive(Serialize, Deserialize)] -struct Trie { - value_type: String, - values: HashMap, -} - -fn generate_tries() { - let tries: HashMap = read_json("value_tries"); - - for (name, trie) in tries { - let mut trie_builder = FastrieBuilderNode::new(); - for (seq, value_code) in trie.values { - trie_builder.add(seq.as_bytes(), value_code); - }; - let var_name = snake_case(&name_words(name.as_str())); - let trie_code = generate_fastrie_code( - var_name.as_str(), - trie.value_type.as_str(), - &trie_builder.prebuild(), - ); - write_rs(format!("trie_{}", var_name).as_str(), trie_code); - }; -} - fn main() { generate_attr_map(); generate_entities(); generate_patterns(); - generate_tries(); } diff --git a/gen/match_tries.json b/gen/match_tries.json deleted file mode 100644 index 0967ef4..0000000 --- a/gen/match_tries.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/gen/value_tries.json b/gen/value_tries.json deleted file mode 100644 index 0967ef4..0000000 --- a/gen/value_tries.json +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/src/pattern.rs b/src/pattern.rs index 911c1e4..3903f06 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -1,35 +1,62 @@ pub struct SinglePattern { - pub seq: &'static [u8], - pub table: &'static [usize], + dfa: &'static [usize], + length: usize, } impl SinglePattern { - pub fn len(&self) -> usize { - self.table.len() + pub const fn prebuilt(dfa: &'static [usize], length: usize) -> SinglePattern { + SinglePattern { + dfa, length + } } + pub fn len(&self) -> usize { + self.length + } + + #[inline(always)] pub fn match_against(&self, haystack: &[u8]) -> Option { - let mut hay_idx = 0usize; - let mut pat_idx = 0usize; - while hay_idx < haystack.len() { - if self.seq[pat_idx] == haystack[hay_idx] { - pat_idx += 1; - hay_idx += 1; - }; - - if pat_idx == self.seq.len() { - return Some(hay_idx - pat_idx); - }; - - if hay_idx < haystack.len() && self.seq[pat_idx] != haystack[hay_idx] { - if pat_idx != 0 { - pat_idx = self.table[pat_idx - 1]; - } else { - hay_idx += 1; - }; - }; + let mut i = 0; + let mut j = 0; + while i < haystack.len() && j < self.length { + j = self.dfa[haystack[i] as usize * self.length + j]; + i += 1; }; - - None + if j == self.length { + Some(i - self.length) + } else { + None + } } } + +// Can't use pub const fn constructor due to Copy trait, so allow directly creating struct publicly for now. +pub struct TrieNode { + pub value: Option, + pub children: [Option<&'static TrieNode>; 256], +} + +pub struct TrieNodeMatch { + pub end: usize, + pub value: V, +} + +impl TrieNode { + #[inline(always)] + pub fn longest_matching_prefix(&self, text: &[u8]) -> Option> { + let mut node: &TrieNode = self; + let mut value: Option> = None; + for (i, &c) in text.iter().enumerate() { + match node.children[c as usize] { + Some(child) => node = child, + None => break, + }; + match node.value { + Some(v) => value = Some(TrieNodeMatch { end: i, value: v }), + None => {} + }; + }; + value + } +} + diff --git a/src/proc/mod.rs b/src/proc/mod.rs index 16ddbc1..84214ef 100644 --- a/src/proc/mod.rs +++ b/src/proc/mod.rs @@ -2,10 +2,8 @@ use core::fmt; use std::fmt::{Debug, Formatter}; use std::ops::{Index, IndexMut}; -use fastrie::Fastrie; - use crate::err::{ErrorType, ProcessingResult}; -use crate::pattern::SinglePattern; +use crate::pattern::{SinglePattern, TrieNode}; use crate::proc::MatchAction::*; use crate::proc::MatchMode::*; use crate::proc::range::ProcessorRange; @@ -95,6 +93,7 @@ impl<'d> Processor<'d> { /// Move next `amount` characters to output. /// Panics. Does not check bounds for performance (e.g. already checked). + #[inline(always)] fn _shift(&mut self, amount: usize) -> () { // Optimisation: Don't shift if already there (but still update offsets). if self.read_next != self.write_next { @@ -104,6 +103,7 @@ impl<'d> Processor<'d> { self.write_next += amount; } + #[inline(always)] fn _replace(&mut self, start: usize, end: usize, data: &[u8]) -> usize { debug_assert!(start <= end); let added = data.len() - (end - start); @@ -116,15 +116,18 @@ impl<'d> Processor<'d> { added } + #[inline(always)] fn _insert(&mut self, at: usize, data: &[u8]) -> usize { self._replace(at, at, data) } // Matching. + #[inline(always)] fn _one bool>(&mut self, cond: C) -> usize { self._maybe_read_offset(0).filter(|n| cond(*n)).is_some() as usize } + #[inline(always)] fn _many bool>(&mut self, cond: C) -> usize { let mut count = 0usize; while self._maybe_read_offset(count).filter(|c| cond(*c)).is_some() { @@ -133,7 +136,7 @@ impl<'d> Processor<'d> { count } - // Make expectation explicit, even for Maybe. + #[inline(always)] pub fn m(&mut self, mode: MatchMode, action: MatchAction) -> ProcessorRange { let count = match mode { IsChar(c) => self._one(|n| n == c), @@ -167,7 +170,8 @@ impl<'d> Processor<'d> { ProcessorRange { start, end: start + count } } - pub fn m_trie(&mut self, trie: &Fastrie, action: MatchAction) -> Option { + #[inline(always)] + pub fn m_trie(&mut self, trie: &TrieNode, action: MatchAction) -> Option { trie.longest_matching_prefix(&self.code[self.read_next..]).map(|m| { let count = m.end + 1; match action { @@ -175,7 +179,7 @@ impl<'d> Processor<'d> { Keep => self._shift(count), MatchOnly => {} }; - *m.value + m.value }) } diff --git a/src/unit/tag.rs b/src/unit/tag.rs index 5618dec..8bab95a 100644 --- a/src/unit/tag.rs +++ b/src/unit/tag.rs @@ -60,12 +60,14 @@ impl MaybeClosingTag { MaybeClosingTag(None) } + #[inline(always)] pub fn write(&mut self, proc: &mut Processor) -> () { proc.write_slice(b"'); } + #[inline(always)] pub fn write_if_exists(&mut self, proc: &mut Processor) -> bool { self.0.take().filter(|tag| { proc.write_slice(b" bool { self.0.is_some() } + #[inline(always)] pub fn exists_and bool>(&self, pred: F) -> bool { match self.0 { Some(range) => pred(range), @@ -86,6 +90,7 @@ impl MaybeClosingTag { } } + #[inline(always)] pub fn replace(&mut self, tag: MaybeClosingTag) -> () { self.0 = tag.0; }