Skip to content

Commit fa4ee74

Browse files
committed
Optimize DAG-CBOR map decode with key-intern cache and KnownHash insert
1 parent ee2735d commit fa4ee74

2 files changed

Lines changed: 146 additions & 3 deletions

File tree

build.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
fn main() {
2+
pyo3_build_config::use_pyo3_cfgs();
23
if matches!(
34
pyo3_build_config::get().implementation,
45
pyo3_build_config::PythonImplementation::CPython

src/lib.rs

Lines changed: 145 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ use cid::{multibase, Cid};
1010
use pyo3::pybacked::PyBackedStr;
1111
use pyo3::{ffi, prelude::*, types::*, BoundObject, Python};
1212

13-
// Private CPython symbol; not provided by pyo3-ffi and CPython-only.
13+
// Private CPython symbols; not provided by pyo3-ffi and CPython-only.
1414
#[cfg(CPython)]
1515
extern "C" {
1616
fn _PyDict_NewPresized(minused: ffi::Py_ssize_t) -> *mut ffi::PyObject;
17+
fn _PyDict_SetItem_KnownHash(
18+
op: *mut ffi::PyObject,
19+
key: *mut ffi::PyObject,
20+
value: *mut ffi::PyObject,
21+
hash: ffi::Py_hash_t,
22+
) -> std::os::raw::c_int;
1723
}
1824

1925
// Empty CPython dicts already have 8 slots, so presizing below that buys
@@ -204,6 +210,120 @@ fn pystring_from_bytes_fast<'py>(
204210
PyString::from_bytes(py, bytes)
205211
}
206212

213+
// Direct-mapped intern cache for short map keys. atproto-shape payloads
214+
// reuse a small vocabulary (`$type`, `did`, `cid`, `uri`, `text`, ...) per
215+
// record; caching the constructed `PyUnicode` + its `Py_hash_t` skips both
216+
// the rebuild and the rehash inside `PyDict_SetItem`
217+
#[cfg(all(CPython, not(Py_GIL_DISABLED)))]
218+
mod key_cache {
219+
use super::pystring_from_bytes_fast;
220+
use pyo3::{ffi, prelude::*};
221+
222+
const CAP: usize = 2048;
223+
const MAX_KEY_LEN: usize = 64;
224+
225+
struct Entry {
226+
len: u16,
227+
bytes: [u8; MAX_KEY_LEN],
228+
obj: *mut ffi::PyObject,
229+
hash: ffi::Py_hash_t,
230+
}
231+
232+
impl Entry {
233+
const fn empty() -> Self {
234+
Self {
235+
len: 0,
236+
bytes: [0; MAX_KEY_LEN],
237+
obj: std::ptr::null_mut(),
238+
hash: 0,
239+
}
240+
}
241+
}
242+
243+
static mut SLOTS: [Entry; CAP] = [const { Entry::empty() }; CAP];
244+
245+
#[inline]
246+
fn fx_hash(bytes: &[u8]) -> usize {
247+
const K: u64 = 0x517c_c1b7_2722_0a95;
248+
let mut h: u64 = 0;
249+
for &b in bytes {
250+
h = (h.rotate_left(5) ^ b as u64).wrapping_mul(K);
251+
}
252+
h as usize
253+
}
254+
255+
/// Returns `(strong-ref PyUnicode*, Py_hash_t)`. Caller owns one ref.
256+
/// Caller must hold the GIL (we are always called from a `Python<'_>`).
257+
#[inline]
258+
pub(super) unsafe fn intern_key(
259+
py: Python<'_>,
260+
bytes: &[u8],
261+
) -> PyResult<(*mut ffi::PyObject, ffi::Py_hash_t)> {
262+
if bytes.len() > MAX_KEY_LEN {
263+
return build(py, bytes);
264+
}
265+
266+
let slot_idx = fx_hash(bytes) & (CAP - 1);
267+
let slot = &mut *(&raw mut SLOTS[slot_idx]);
268+
269+
if slot.len as usize == bytes.len()
270+
&& !slot.obj.is_null()
271+
&& slot.bytes[..bytes.len()] == *bytes
272+
{
273+
ffi::Py_INCREF(slot.obj);
274+
return Ok((slot.obj, slot.hash));
275+
}
276+
277+
let (obj, hash) = build(py, bytes)?;
278+
// Evict the previous occupant before claiming the slot.
279+
if !slot.obj.is_null() {
280+
ffi::Py_DECREF(slot.obj);
281+
}
282+
// One ref for the cache, one for the caller.
283+
ffi::Py_INCREF(obj);
284+
slot.obj = obj;
285+
slot.hash = hash;
286+
slot.len = bytes.len() as u16;
287+
slot.bytes[..bytes.len()].copy_from_slice(bytes);
288+
Ok((obj, hash))
289+
}
290+
291+
#[inline]
292+
unsafe fn build(
293+
py: Python<'_>,
294+
bytes: &[u8],
295+
) -> PyResult<(*mut ffi::PyObject, ffi::Py_hash_t)> {
296+
let s = pystring_from_bytes_fast(py, bytes)?;
297+
let ptr = s.as_ptr();
298+
let hash = ffi::PyObject_Hash(ptr);
299+
if hash == -1 {
300+
return Err(PyErr::fetch(py));
301+
}
302+
Ok((s.into_ptr(), hash))
303+
}
304+
}
305+
306+
// Non-CPython / free-threaded fallback: no cache, just build the string and compute its hash inline
307+
#[cfg(not(all(CPython, not(Py_GIL_DISABLED))))]
308+
mod key_cache {
309+
use super::pystring_from_bytes_fast;
310+
use pyo3::{ffi, prelude::*};
311+
312+
#[inline]
313+
pub(super) unsafe fn intern_key(
314+
py: Python<'_>,
315+
bytes: &[u8],
316+
) -> PyResult<(*mut ffi::PyObject, ffi::Py_hash_t)> {
317+
let s = pystring_from_bytes_fast(py, bytes)?;
318+
let ptr = s.as_ptr();
319+
let hash = ffi::PyObject_Hash(ptr);
320+
if hash == -1 {
321+
return Err(PyErr::fetch(py));
322+
}
323+
Ok((s.into_ptr(), hash))
324+
}
325+
}
326+
207327
fn get_bytes_from_py_any<'py>(obj: &'py Bound<'py, PyAny>) -> PyResult<&'py [u8]> {
208328
if let Ok(b) = obj.cast::<PyBytes>() {
209329
Ok(b.as_bytes())
@@ -314,11 +434,33 @@ where
314434
}
315435
}
316436

317-
let key_py = pystring_from_bytes_fast(py, key)?;
318437
prev_key = Some(key);
319438

439+
let (key_ptr, key_hash) = unsafe { key_cache::intern_key(py, key)? };
440+
let key_bound: Bound<'_, PyAny> =
441+
unsafe { Bound::from_owned_ptr(py, key_ptr) };
442+
320443
let value_py = decode_dag_cbor_to_pyobject(py, r, depth + 1)?;
321-
dict.set_item(key_py, value_py)?;
444+
445+
#[cfg(CPython)]
446+
unsafe {
447+
let value_ptr = value_py.into_ptr();
448+
let rc = _PyDict_SetItem_KnownHash(
449+
dict.as_ptr(),
450+
key_bound.as_ptr(),
451+
value_ptr,
452+
key_hash,
453+
);
454+
ffi::Py_DECREF(value_ptr);
455+
if rc != 0 {
456+
return Err(anyhow!(PyErr::fetch(py)));
457+
}
458+
}
459+
#[cfg(not(CPython))]
460+
{
461+
let _ = key_hash;
462+
dict.set_item(&key_bound, value_py)?;
463+
}
322464
}
323465

324466
dict.into_pyobject(py)?.into()

0 commit comments

Comments
 (0)