Skip to content

Commit 33ef167

Browse files
committed
Share BM25 fieldnorm caches per thread
Reuse BM25 TF normalization caches for weights with the same average fieldnorm using a bounded thread-local LRU. This avoids recomputing and duplicating the cache for many terms on the same field without adding cross-thread contention.
1 parent bf8b263 commit 33ef167

1 file changed

Lines changed: 46 additions & 2 deletions

File tree

src/query/bm25.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
use std::cell::RefCell;
2+
use std::num::NonZeroUsize;
13
use std::sync::Arc;
24

5+
use lru::LruCache;
6+
37
use crate::fieldnorm::FieldNormReader;
48
use crate::query::Explanation;
59
use crate::schema::Field;
@@ -59,7 +63,9 @@ fn cached_tf_component(fieldnorm: u32, average_fieldnorm: Score) -> Score {
5963
K1 * (1.0 - B + B * fieldnorm as Score / average_fieldnorm)
6064
}
6165

62-
fn compute_tf_cache(average_fieldnorm: Score) -> Arc<[Score; 256]> {
66+
const BM25_TF_CACHE_CAPACITY: usize = 64;
67+
68+
fn compute_tf_cache_uncached(average_fieldnorm: Score) -> Arc<[Score; 256]> {
6369
let mut cache: [Score; 256] = [0.0; 256];
6470
for (fieldnorm_id, cache_mut) in cache.iter_mut().enumerate() {
6571
let fieldnorm = FieldNormReader::id_to_fieldnorm(fieldnorm_id as u8);
@@ -68,6 +74,36 @@ fn compute_tf_cache(average_fieldnorm: Score) -> Arc<[Score; 256]> {
6874
Arc::new(cache)
6975
}
7076

77+
thread_local! {
78+
static TF_CACHES: RefCell<LruCache<u32, Arc<[Score; 256]>>> = RefCell::new(LruCache::new(
79+
NonZeroUsize::new(BM25_TF_CACHE_CAPACITY).unwrap(),
80+
));
81+
}
82+
83+
/// The cache is shared across all [Bm25Weight] with the same average fieldnorm on the same thread.
84+
/// It is stored in a thread local LRU cache.
85+
///
86+
/// On one query all terms on the same field will share the same average fieldnorm, and thus the same cache.
87+
/// This will lower cache pressure.
88+
///
89+
/// Even between queries (on the same thread), the cache will be reused, which allows the cache to
90+
/// better learn the memory address of the cache and access patterns.
91+
///
92+
/// Thread local is used in order to be defensive about potential contention on the cache.
93+
fn compute_tf_cache(average_fieldnorm: Score) -> Arc<[Score; 256]> {
94+
let cache_key = average_fieldnorm.to_bits();
95+
TF_CACHES.with(|cache_by_average_fieldnorm| {
96+
let mut cache_by_average_fieldnorm = cache_by_average_fieldnorm.borrow_mut();
97+
if let Some(cache) = cache_by_average_fieldnorm.get(&cache_key) {
98+
return cache.clone();
99+
}
100+
101+
let cache = compute_tf_cache_uncached(average_fieldnorm);
102+
cache_by_average_fieldnorm.put(cache_key, cache.clone());
103+
cache
104+
})
105+
}
106+
71107
/// A struct used for computing BM25 scores.
72108
#[derive(Clone)]
73109
pub struct Bm25Weight {
@@ -229,12 +265,20 @@ impl Bm25Weight {
229265
#[cfg(test)]
230266
mod tests {
231267

232-
use super::idf;
268+
use super::{idf, Bm25Weight};
233269
use crate::{assert_nearly_equals, Score};
234270

235271
#[test]
236272
fn test_idf() {
237273
let score: Score = 2.0;
238274
assert_nearly_equals!(idf(1, 2), score.ln());
239275
}
276+
277+
#[test]
278+
fn test_bm25_tf_cache_is_shared_for_same_average_fieldnorm() {
279+
let weight1 = Bm25Weight::for_one_term(1, 10, 3.0);
280+
let weight2 = Bm25Weight::for_one_term(2, 10, 3.0);
281+
282+
assert!(std::sync::Arc::ptr_eq(&weight1.cache, &weight2.cache));
283+
}
240284
}

0 commit comments

Comments
 (0)