Improve performance of difflib. (#1)

* Improve performance of difflib, primarily by avoiding heap allocations.

* Add doc comment to real_quick_ratio.
This commit is contained in:
David Stern
2024-06-03 14:11:35 -04:00
committed by GitHub
parent f035fb8e65
commit 114177e7d8
4 changed files with 40 additions and 8 deletions
+4
View File
@@ -12,8 +12,12 @@ include = [
"**/*.rs", "**/*.rs",
"Cargo.toml", "Cargo.toml",
] ]
edition = "2021"
[[test]] [[test]]
name = "tests" name = "tests"
[dependencies]
rustc-hash = "1.1.0"
+3 -2
View File
@@ -1,6 +1,7 @@
use sequencematcher::SequenceMatcher;
use std::cmp; use std::cmp;
use utils::{count_leading, str_with_similar_chars};
use crate::sequencematcher::SequenceMatcher;
use crate::utils::{count_leading, str_with_similar_chars};
#[derive(Default)] #[derive(Default)]
pub struct Differ { pub struct Differ {
+32 -5
View File
@@ -1,7 +1,10 @@
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
use utils::calculate_ratio;
use rustc_hash::FxHashMap;
use crate::utils::calculate_ratio;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)] #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
pub struct Match { pub struct Match {
@@ -57,6 +60,12 @@ pub struct SequenceMatcher<'a, T: 'a + Sequence> {
opcodes: Option<Vec<Opcode>>, opcodes: Option<Vec<Opcode>>,
is_junk: Option<fn(&T) -> bool>, is_junk: Option<fn(&T) -> bool>,
second_sequence_elements: HashMap<&'a T, Vec<usize>>, second_sequence_elements: HashMap<&'a T, Vec<usize>>,
// These hash maps are used by `find_longest_match`; we store them here
// to avoid making many unnecessary heap allocations; that function
// gets called many times as part of `get_close_matches`.
longest_match_j2len: FxHashMap<usize, usize>,
longest_match_new_j2len: FxHashMap<usize, usize>,
} }
impl<'a, T: Sequence> SequenceMatcher<'a, T> { impl<'a, T: Sequence> SequenceMatcher<'a, T> {
@@ -71,6 +80,8 @@ impl<'a, T: Sequence> SequenceMatcher<'a, T> {
opcodes: None, opcodes: None,
is_junk: None, is_junk: None,
second_sequence_elements: HashMap::new(), second_sequence_elements: HashMap::new(),
longest_match_j2len: FxHashMap::default(),
longest_match_new_j2len: FxHashMap::default(),
}; };
matcher.set_seqs(first_sequence, second_sequence); matcher.set_seqs(first_sequence, second_sequence);
matcher matcher
@@ -138,7 +149,7 @@ impl<'a, T: Sequence> SequenceMatcher<'a, T> {
} }
pub fn find_longest_match( pub fn find_longest_match(
&self, &mut self,
first_start: usize, first_start: usize,
first_end: usize, first_end: usize,
second_start: usize, second_start: usize,
@@ -148,14 +159,19 @@ impl<'a, T: Sequence> SequenceMatcher<'a, T> {
let second_sequence = &self.second_sequence; let second_sequence = &self.second_sequence;
let second_sequence_elements = &self.second_sequence_elements; let second_sequence_elements = &self.second_sequence_elements;
let (mut best_i, mut best_j, mut best_size) = (first_start, second_start, 0); let (mut best_i, mut best_j, mut best_size) = (first_start, second_start, 0);
let mut j2len: HashMap<usize, usize> = HashMap::new(); let mut j2len = &mut self.longest_match_j2len;
let mut new_j2len = &mut self.longest_match_new_j2len;
// Clear out any old data in our reusable hash map allocations.
j2len.clear();
new_j2len.clear();
for (i, item) in first_sequence for (i, item) in first_sequence
.iter() .iter()
.enumerate() .enumerate()
.take(first_end) .take(first_end)
.skip(first_start) .skip(first_start)
{ {
let mut new_j2len: HashMap<usize, usize> = HashMap::new();
if let Some(indexes) = second_sequence_elements.get(item) { if let Some(indexes) = second_sequence_elements.get(item) {
for j in indexes { for j in indexes {
let j = *j; let j = *j;
@@ -180,7 +196,8 @@ impl<'a, T: Sequence> SequenceMatcher<'a, T> {
} }
} }
} }
j2len = new_j2len; std::mem::swap(&mut j2len, &mut new_j2len);
new_j2len.clear();
} }
for _ in 0..2 { for _ in 0..2 {
while best_i > first_start while best_i > first_start
@@ -343,4 +360,14 @@ impl<'a, T: Sequence> SequenceMatcher<'a, T> {
self.first_sequence.len() + self.second_sequence.len(), self.first_sequence.len() + self.second_sequence.len(),
) )
} }
/// Return an upper bound on ratio() very quickly.
///
/// This isn't defined beyond that it is an upper bound on .ratio(), and
/// is faster to compute than either .ratio() or .quick_ratio().
pub fn real_quick_ratio(&self) -> f32 {
let la = self.first_sequence.len();
let lb = self.second_sequence.len();
calculate_ratio(min(la, lb), la + lb)
}
} }
+1 -1
View File
@@ -5,7 +5,7 @@ use difflib::sequencematcher::{Match, Opcode, SequenceMatcher};
#[test] #[test]
fn test_longest_match() { fn test_longest_match() {
let matcher = SequenceMatcher::new(" abcd", "abcd abcd"); let mut matcher = SequenceMatcher::new(" abcd", "abcd abcd");
let m = matcher.find_longest_match(0, 5, 0, 9); let m = matcher.find_longest_match(0, 5, 0, 9);
assert_eq!(m.first_start, 0); assert_eq!(m.first_start, 0);
assert_eq!(m.second_start, 4); assert_eq!(m.second_start, 4);