Skip to content

Commit f392c46

Browse files
committed
use slotmap
1 parent b0b25fa commit f392c46

File tree

8 files changed

+143
-68
lines changed

8 files changed

+143
-68
lines changed

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/luars/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ itoa = "1.0" # Fast integer formatting (10x faster than format!)
1717
ryu = "1.0" # Fast float formatting
1818
tempfile = "3.10" # For io.tmpfile()
1919
rowan = "0.16.1"
20+
slotmap = "1.0" # Fast O(1) object pool with generational keys

crates/luars/src/gc/mod.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ mod object_pool;
1313

1414
use crate::lua_value::LuaValue;
1515
pub use object_pool::{
16-
Arena, FunctionId, GcFunction, GcHeader, GcString, GcTable, GcThread, GcUpvalue,
17-
ObjectPoolV2 as ObjectPool, StringId, TableId, ThreadId, UpvalueId, UpvalueState, UserdataId,
16+
Arena, FunctionId, GcFunction, GcHeader, GcString, GcTable, GcThread, GcUpvalue, ObjectPool,
17+
StringId, TableId, ThreadId, UpvalueId, UpvalueKey, UpvalueState, UserdataId,
1818
};
1919

2020
// Re-export for compatibility
@@ -309,15 +309,15 @@ impl GC {
309309
self.record_deallocation(128);
310310
}
311311

312-
// Collect unmarked upvalues (skip fixed ones)
313-
let upvals_to_free: Vec<u32> = pool
312+
// Collect unmarked upvalues (skip fixed ones) - using SlotMap
313+
let upvals_to_free: Vec<UpvalueKey> = pool
314314
.upvalues
315315
.iter()
316316
.filter(|(_, u)| !u.header.marked && !u.header.fixed)
317317
.map(|(id, _)| id)
318318
.collect();
319319
for id in upvals_to_free {
320-
pool.upvalues.free(id);
320+
pool.upvalues.remove(id);
321321
collected += 1;
322322
}
323323

crates/luars/src/gc/object_pool.rs

Lines changed: 68 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@
22
//
33
// Key Design Principles:
44
// 1. LuaValueV2 stores type tag + object ID (no pointers - Vec may relocate)
5-
// 2. All GC objects accessed via ID lookup in Arena
6-
// 3. ChunkedArena uses fixed-size chunks - never reallocates existing data!
5+
// 2. All GC objects accessed via ID lookup in Arena/SlotMap
6+
// 3. slotmap provides O(1) access with Vec-based storage (no chunking overhead)
77
// 4. No Rc/RefCell overhead - direct access via &mut self
88
// 5. GC headers embedded in objects for mark-sweep
99
//
1010
// Memory Layout:
11-
// - ChunkedArena stores objects in fixed-size chunks (Box<[Option<T>; CHUNK_SIZE]>)
12-
// - Each chunk is allocated once and never moved
13-
// - New chunks are added as needed, existing chunks stay in place
14-
// - This eliminates Vec resize overhead and improves cache locality
11+
// - SlotMap stores objects in flat Vec with generational keys
12+
// - O(1) insert, access, and remove operations
13+
// - Arena still used for some types for compatibility
1514

1615
use crate::lua_value::{Chunk, LuaThread, LuaUserdata};
1716
use crate::{LuaString, LuaTable, LuaValue};
17+
use slotmap::{new_key_type, SlotMap};
1818
use std::hash::Hash;
1919
use std::rc::Rc;
2020

21+
// Define custom key types for slotmap
22+
new_key_type! {
23+
/// Key for upvalues in the slotmap
24+
pub struct UpvalueKey;
25+
}
26+
2127
// ============ GC Header ============
2228

2329
/// GC object header - embedded in every GC-managed object
@@ -47,9 +53,17 @@ pub struct TableId(pub u32);
4753
#[repr(transparent)]
4854
pub struct FunctionId(pub u32);
4955

50-
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
51-
#[repr(transparent)]
52-
pub struct UpvalueId(pub u32);
56+
/// UpvalueId now wraps slotmap's UpvalueKey for O(1) access
57+
/// The slotmap key provides generational safety and direct Vec indexing
58+
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
59+
pub struct UpvalueId(pub UpvalueKey);
60+
61+
impl Default for UpvalueId {
62+
fn default() -> Self {
63+
// Create a null/invalid key - this should never be used for actual lookups
64+
UpvalueId(UpvalueKey::default())
65+
}
66+
}
5367

5468
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
5569
#[repr(transparent)]
@@ -123,6 +137,25 @@ impl GcUpvalue {
123137
_ => None,
124138
}
125139
}
140+
141+
/// Set closed upvalue value directly without checking state
142+
/// SAFETY: Must only be called when upvalue is in Closed state
143+
#[inline(always)]
144+
pub unsafe fn set_closed_value_unchecked(&mut self, value: LuaValue) {
145+
if let UpvalueState::Closed(ref mut v) = self.state {
146+
*v = value;
147+
}
148+
}
149+
150+
/// Get closed value reference directly without Option
151+
/// SAFETY: Must only be called when upvalue is in Closed state
152+
#[inline(always)]
153+
pub unsafe fn get_closed_value_ref_unchecked(&self) -> &LuaValue {
154+
match &self.state {
155+
UpvalueState::Closed(v) => v,
156+
_ => unsafe { std::hint::unreachable_unchecked() },
157+
}
158+
}
126159
}
127160

128161
/// String with embedded GC header
@@ -354,11 +387,12 @@ impl<T> Default for Arena<T> {
354387

355388
/// High-performance object pool for the Lua VM
356389
/// All objects are stored in typed arenas and accessed by ID
357-
pub struct ObjectPoolV2 {
390+
pub struct ObjectPool {
358391
pub strings: Arena<GcString>,
359392
pub tables: Arena<GcTable>,
360393
pub functions: Arena<GcFunction>,
361-
pub upvalues: Arena<GcUpvalue>,
394+
/// Upvalues use SlotMap for O(1) access (no chunking overhead)
395+
pub upvalues: SlotMap<UpvalueKey, GcUpvalue>,
362396
pub userdata: Arena<LuaUserdata>,
363397
pub threads: Arena<GcThread>,
364398

@@ -586,13 +620,13 @@ impl StringInternTable {
586620
}
587621
}
588622

589-
impl ObjectPoolV2 {
623+
impl ObjectPool {
590624
pub fn new() -> Self {
591625
let mut pool = Self {
592626
strings: Arena::with_capacity(256),
593627
tables: Arena::with_capacity(64),
594628
functions: Arena::with_capacity(32),
595-
upvalues: Arena::with_capacity(32),
629+
upvalues: SlotMap::with_capacity_and_key(32), // SlotMap for O(1) access
596630
userdata: Arena::new(),
597631
threads: Arena::with_capacity(8),
598632
string_intern: StringInternTable::with_capacity(256),
@@ -1037,15 +1071,15 @@ impl ObjectPoolV2 {
10371071
self.functions.get_mut(id.0)
10381072
}
10391073

1040-
// ==================== Upvalue Operations ====================
1074+
// ==================== Upvalue Operations (using SlotMap) ====================
10411075

10421076
#[inline]
10431077
pub fn create_upvalue_open(&mut self, stack_index: usize) -> UpvalueId {
10441078
let gc_uv = GcUpvalue {
10451079
header: GcHeader::default(),
10461080
state: UpvalueState::Open { stack_index },
10471081
};
1048-
UpvalueId(self.upvalues.alloc(gc_uv))
1082+
UpvalueId(self.upvalues.insert(gc_uv))
10491083
}
10501084

10511085
#[inline]
@@ -1054,7 +1088,7 @@ impl ObjectPoolV2 {
10541088
header: GcHeader::default(),
10551089
state: UpvalueState::Closed(value),
10561090
};
1057-
UpvalueId(self.upvalues.alloc(gc_uv))
1091+
UpvalueId(self.upvalues.insert(gc_uv))
10581092
}
10591093

10601094
#[inline(always)]
@@ -1074,6 +1108,13 @@ impl ObjectPoolV2 {
10741108
self.upvalues.get_mut(id.0)
10751109
}
10761110

1111+
/// Get mutable upvalue without bounds checking
1112+
/// SAFETY: id must be a valid UpvalueId
1113+
#[inline(always)]
1114+
pub unsafe fn get_upvalue_mut_unchecked(&mut self, id: UpvalueId) -> &mut GcUpvalue {
1115+
unsafe { self.upvalues.get_unchecked_mut(id.0) }
1116+
}
1117+
10771118
// ==================== Userdata Operations ====================
10781119

10791120
#[inline]
@@ -1164,7 +1205,7 @@ impl ObjectPoolV2 {
11641205
.filter(|(_, gf)| !gf.header.marked)
11651206
.map(|(id, _)| id)
11661207
.collect();
1167-
let upvalues_to_free: Vec<u32> = self
1208+
let upvalues_to_free: Vec<UpvalueKey> = self
11681209
.upvalues
11691210
.iter()
11701211
.filter(|(_, gu)| !gu.header.marked)
@@ -1193,7 +1234,7 @@ impl ObjectPoolV2 {
11931234
self.functions.free(id);
11941235
}
11951236
for id in upvalues_to_free {
1196-
self.upvalues.free(id);
1237+
self.upvalues.remove(id);
11971238
}
11981239
for id in threads_to_free {
11991240
self.threads.free(id);
@@ -1204,7 +1245,7 @@ impl ObjectPoolV2 {
12041245
self.strings.shrink_to_fit();
12051246
self.tables.shrink_to_fit();
12061247
self.functions.shrink_to_fit();
1207-
self.upvalues.shrink_to_fit();
1248+
// SlotMap doesn't have shrink_to_fit, skip upvalues
12081249
self.threads.shrink_to_fit();
12091250
self.string_intern.shrink_to_fit();
12101251
}
@@ -1228,7 +1269,7 @@ impl ObjectPoolV2 {
12281269

12291270
#[inline]
12301271
pub fn remove_upvalue(&mut self, id: UpvalueId) {
1231-
self.upvalues.free(id.0);
1272+
self.upvalues.remove(id.0);
12321273
}
12331274

12341275
#[inline]
@@ -1269,7 +1310,7 @@ impl ObjectPoolV2 {
12691310
}
12701311
}
12711312

1272-
impl Default for ObjectPoolV2 {
1313+
impl Default for ObjectPool {
12731314
fn default() -> Self {
12741315
Self::new()
12751316
}
@@ -1318,7 +1359,7 @@ mod tests {
13181359

13191360
#[test]
13201361
fn test_string_interning() {
1321-
let mut pool = ObjectPoolV2::new();
1362+
let mut pool = ObjectPool::new();
13221363

13231364
let id1 = pool.create_string("hello");
13241365
let id2 = pool.create_string("hello");
@@ -1336,7 +1377,7 @@ mod tests {
13361377

13371378
#[test]
13381379
fn test_table_operations() {
1339-
let mut pool = ObjectPoolV2::new();
1380+
let mut pool = ObjectPool::new();
13401381

13411382
let tid = pool.create_table(4, 4);
13421383

@@ -1360,14 +1401,15 @@ mod tests {
13601401
assert_eq!(std::mem::size_of::<StringId>(), 4);
13611402
assert_eq!(std::mem::size_of::<TableId>(), 4);
13621403
assert_eq!(std::mem::size_of::<FunctionId>(), 4);
1363-
assert_eq!(std::mem::size_of::<UpvalueId>(), 4);
1404+
// UpvalueId is now 8 bytes (slotmap key with version)
1405+
assert_eq!(std::mem::size_of::<UpvalueId>(), 8);
13641406
}
13651407

13661408
#[test]
13671409
fn test_string_interning_many_strings() {
13681410
// Test that many different strings with potential hash collisions
13691411
// are all stored correctly
1370-
let mut pool = ObjectPoolV2::new();
1412+
let mut pool = ObjectPool::new();
13711413
let mut ids = Vec::new();
13721414

13731415
// Create 1000 different strings
@@ -1398,7 +1440,7 @@ mod tests {
13981440
#[test]
13991441
fn test_string_interning_similar_strings() {
14001442
// Test strings that might have similar hashes
1401-
let mut pool = ObjectPoolV2::new();
1443+
let mut pool = ObjectPool::new();
14021444

14031445
let strings = vec![
14041446
"a", "b", "c", "aa", "ab", "ba", "bb", "aaa", "aab", "aba", "abb", "baa", "bab", "bba",

crates/luars/src/lua_value/lua_value.rs

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
// │ - GC objects: unused (ID is in tag) │
2020
// └──────────────────────────────────────────────────┘
2121

22-
use crate::gc::{FunctionId, StringId, TableId, ThreadId, UpvalueId, UserdataId};
22+
use crate::gc::{FunctionId, StringId, TableId, ThreadId, UserdataId};
2323
use crate::lua_value::CFunction;
2424

2525
// Type tags (high 16 bits of tag field)
@@ -158,13 +158,8 @@ impl LuaValue {
158158
}
159159
}
160160

161-
#[inline(always)]
162-
pub fn upvalue(id: UpvalueId) -> Self {
163-
Self {
164-
primary: TAG_UPVALUE | (id.0 as u64),
165-
secondary: 0,
166-
}
167-
}
161+
// Note: LuaValue::upvalue removed - upvalues are never stored as LuaValues,
162+
// they are only referenced via UpvalueId in GcFunction::upvalues
168163

169164
#[inline(always)]
170165
pub fn thread(id: ThreadId) -> Self {
@@ -359,14 +354,7 @@ impl LuaValue {
359354
}
360355
}
361356

362-
#[inline(always)]
363-
pub fn as_upvalue_id(&self) -> Option<UpvalueId> {
364-
if (self.primary & TAG_MASK) == TAG_UPVALUE {
365-
Some(UpvalueId((self.primary & ID_MASK) as u32))
366-
} else {
367-
None
368-
}
369-
}
357+
// Note: as_upvalue_id removed - upvalues are never stored as LuaValues
370358

371359
#[inline(always)]
372360
pub fn as_thread_id(&self) -> Option<ThreadId> {

0 commit comments

Comments
 (0)