@@ -10,10 +10,16 @@ use cid::{multibase, Cid};
1010use pyo3:: pybacked:: PyBackedStr ;
1111use 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 ) ]
1515extern "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
@@ -198,6 +204,124 @@ fn pystring_from_bytes_fast<'py>(py: Python<'py>, bytes: &[u8]) -> PyResult<Boun
198204 PyString :: from_bytes ( py, bytes)
199205}
200206
207+ // Direct-mapped intern cache for short map keys. atproto-shape payloads
208+ // reuse a small vocabulary (`$type`, `did`, `cid`, `uri`, `text`, ...) per
209+ // record; caching the constructed `PyUnicode` + its `Py_hash_t` skips both
210+ // the rebuild and the rehash inside `PyDict_SetItem`
211+ #[ cfg( all( CPython , not( Py_GIL_DISABLED ) ) ) ]
212+ mod key_cache {
213+ use super :: pystring_from_bytes_fast;
214+ use pyo3:: { ffi, prelude:: * } ;
215+
216+ const CAP : usize = 2048 ;
217+ const MAX_KEY_LEN : usize = 64 ;
218+
219+ struct Entry {
220+ len : u16 ,
221+ bytes : [ u8 ; MAX_KEY_LEN ] ,
222+ obj : * mut ffi:: PyObject ,
223+ hash : ffi:: Py_hash_t ,
224+ }
225+
226+ impl Entry {
227+ const fn empty ( ) -> Self {
228+ Self {
229+ len : 0 ,
230+ bytes : [ 0 ; MAX_KEY_LEN ] ,
231+ obj : std:: ptr:: null_mut ( ) ,
232+ hash : 0 ,
233+ }
234+ }
235+ }
236+
237+ static mut SLOTS : [ Entry ; CAP ] = [ const { Entry :: empty ( ) } ; CAP ] ;
238+
239+ #[ inline]
240+ fn fx_hash ( bytes : & [ u8 ] ) -> usize {
241+ const K : u64 = 0x517c_c1b7_2722_0a95 ;
242+ let mut h: u64 = 0 ;
243+ for & b in bytes {
244+ h = ( h. rotate_left ( 5 ) ^ b as u64 ) . wrapping_mul ( K ) ;
245+ }
246+ h as usize
247+ }
248+
249+ /// Returns `(strong-ref PyUnicode*, Py_hash_t)`. Caller owns one ref.
250+ /// Caller must hold the GIL (we are always called from a `Python<'_>`).
251+ #[ inline]
252+ pub ( super ) unsafe fn intern_key (
253+ py : Python < ' _ > ,
254+ bytes : & [ u8 ] ,
255+ ) -> PyResult < ( * mut ffi:: PyObject , ffi:: Py_hash_t ) > {
256+ if bytes. len ( ) > MAX_KEY_LEN {
257+ return build ( py, bytes) ;
258+ }
259+
260+ let slot_idx = fx_hash ( bytes) & ( CAP - 1 ) ;
261+ // `&raw mut` is the supported path to a `static mut`; the explicit
262+ // re-borrow keeps the field accesses readable. Clippy's `deref_addrof`
263+ // suggestion would re-introduce `static_mut_refs`.
264+ #[ allow( clippy:: deref_addrof) ]
265+ let slot = & mut * ( & raw mut SLOTS [ slot_idx] ) ;
266+
267+ if slot. len as usize == bytes. len ( )
268+ && !slot. obj . is_null ( )
269+ && slot. bytes [ ..bytes. len ( ) ] == * bytes
270+ {
271+ ffi:: Py_INCREF ( slot. obj ) ;
272+ return Ok ( ( slot. obj , slot. hash ) ) ;
273+ }
274+
275+ let ( obj, hash) = build ( py, bytes) ?;
276+ // Evict the previous occupant before claiming the slot.
277+ if !slot. obj . is_null ( ) {
278+ ffi:: Py_DECREF ( slot. obj ) ;
279+ }
280+ // One ref for the cache, one for the caller.
281+ ffi:: Py_INCREF ( obj) ;
282+ slot. obj = obj;
283+ slot. hash = hash;
284+ slot. len = bytes. len ( ) as u16 ;
285+ slot. bytes [ ..bytes. len ( ) ] . copy_from_slice ( bytes) ;
286+ Ok ( ( obj, hash) )
287+ }
288+
289+ #[ inline]
290+ unsafe fn build (
291+ py : Python < ' _ > ,
292+ bytes : & [ u8 ] ,
293+ ) -> PyResult < ( * mut ffi:: PyObject , ffi:: Py_hash_t ) > {
294+ let s = pystring_from_bytes_fast ( py, bytes) ?;
295+ let ptr = s. as_ptr ( ) ;
296+ let hash = ffi:: PyObject_Hash ( ptr) ;
297+ if hash == -1 {
298+ return Err ( PyErr :: fetch ( py) ) ;
299+ }
300+ Ok ( ( s. into_ptr ( ) , hash) )
301+ }
302+ }
303+
304+ // Non-CPython / free-threaded fallback: no cache, just build the string and compute its hash inline
305+ #[ cfg( not( all( CPython , not( Py_GIL_DISABLED ) ) ) ) ]
306+ mod key_cache {
307+ use super :: pystring_from_bytes_fast;
308+ use pyo3:: { ffi, prelude:: * } ;
309+
310+ #[ inline]
311+ pub ( super ) unsafe fn intern_key (
312+ py : Python < ' _ > ,
313+ bytes : & [ u8 ] ,
314+ ) -> PyResult < ( * mut ffi:: PyObject , ffi:: Py_hash_t ) > {
315+ let s = pystring_from_bytes_fast ( py, bytes) ?;
316+ let ptr = s. as_ptr ( ) ;
317+ let hash = ffi:: PyObject_Hash ( ptr) ;
318+ if hash == -1 {
319+ return Err ( PyErr :: fetch ( py) ) ;
320+ }
321+ Ok ( ( s. into_ptr ( ) , hash) )
322+ }
323+ }
324+
201325fn get_bytes_from_py_any < ' py > ( obj : & ' py Bound < ' py , PyAny > ) -> PyResult < & ' py [ u8 ] > {
202326 if let Ok ( b) = obj. cast :: < PyBytes > ( ) {
203327 Ok ( b. as_bytes ( ) )
@@ -308,11 +432,32 @@ where
308432 }
309433 }
310434
311- let key_py = pystring_from_bytes_fast ( py, key) ?;
312435 prev_key = Some ( key) ;
313436
437+ let ( key_ptr, key_hash) = unsafe { key_cache:: intern_key ( py, key) ? } ;
438+ let key_bound: Bound < ' _ , PyAny > = unsafe { Bound :: from_owned_ptr ( py, key_ptr) } ;
439+
314440 let value_py = decode_dag_cbor_to_pyobject ( py, r, depth + 1 ) ?;
315- dict. set_item ( key_py, value_py) ?;
441+
442+ #[ cfg( CPython ) ]
443+ unsafe {
444+ let value_ptr = value_py. into_ptr ( ) ;
445+ let rc = _PyDict_SetItem_KnownHash (
446+ dict. as_ptr ( ) ,
447+ key_bound. as_ptr ( ) ,
448+ value_ptr,
449+ key_hash,
450+ ) ;
451+ ffi:: Py_DECREF ( value_ptr) ;
452+ if rc != 0 {
453+ return Err ( anyhow ! ( PyErr :: fetch( py) ) ) ;
454+ }
455+ }
456+ #[ cfg( not( CPython ) ) ]
457+ {
458+ let _ = key_hash;
459+ dict. set_item ( & key_bound, value_py) ?;
460+ }
316461 }
317462
318463 dict. into_pyobject ( py) ?. into ( )
0 commit comments