@@ -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
@@ -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+
207327fn 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