Skip to content

Commit 07dfdc6

Browse files
authored
Merge pull request #98 from JuliaData/jps/faster-poolset
Optimize poolset and pool deletion
2 parents 31231bf + 2f48b82 commit 07dfdc6

3 files changed

Lines changed: 60 additions & 13 deletions

File tree

src/MemPool.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ end
137137
function __init__()
138138
SESSION[] = "sess-" * randstring(6)
139139

140+
# Ensure the shared born-ready Event is set at runtime, independent of how
141+
# precompilation serialized it.
142+
notify(ALWAYS_READY)
143+
140144
DISKCACHE_CONFIG[] = diskcache_config = DiskCacheConfig()
141145
setup_global_device!(diskcache_config)
142146

src/datastore.jl

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -463,9 +463,13 @@ function poolset(@nospecialize(x), pid=myid(); size=approx_size(x),
463463

464464
id = atomic_add!(id_counter, 1)
465465
sstate = if !restore
466+
# Data is already in memory, so this state is born ready: share the
467+
# set `ALWAYS_READY` Event and the empty `NO_LEAVES` vector instead
468+
# of allocating fresh ones.
466469
StorageState(Some{Any}(x),
467-
Vector{StorageLeaf}(),
468-
device)
470+
NO_LEAVES,
471+
device,
472+
ALWAYS_READY)
469473
else
470474
@assert !isa(leaf_device, CPURAMDevice) "Cannot use `CPURAMDevice()` as leaf device when `restore=true`"
471475
StorageState(nothing,
@@ -562,7 +566,7 @@ end
562566

563567
function _getlocal(f, id, remote, args...; local_only::Bool, from::Int)
564568
state = with_lock(()->datastore[id], datastore_lock)
565-
lock_read(state.lock) do
569+
lock_read(getlock!(state)) do
566570
if state.redirect !== nothing
567571
return RedirectTo(state.redirect)
568572
end
@@ -574,6 +578,13 @@ function _getlocal(f, id, remote, args...; local_only::Bool, from::Int)
574578
end
575579
end
576580

581+
"Deferred device-deletion work item drained by the shared `SEND_QUEUE` task."
582+
function _delete_device_work(state::RefState, id::Int)
583+
device = storage_read(state).root
584+
device !== nothing && delete_from_device!(device, state, id)
585+
return
586+
end
587+
577588
function datastore_delete(id)
578589
@safe_lock_spin datastore_counters_lock begin
579590
DEBUG_REFCOUNTING[] && _enqueue_work(Core.print, "-- (", myid(), ", ", id, ") with ", string(datastore_counters[(myid(), id)]), "\n"; gc_context=true)
@@ -585,12 +596,13 @@ function datastore_delete(id)
585596
haskey(datastore, id) ? datastore[id] : nothing
586597
end
587598
(state === nothing) && return
588-
errormonitor(Threads.@spawn begin
589-
device = storage_read(state).root
590-
if device !== nothing
591-
delete_from_device!(device, state, id)
592-
end
593-
end)
599+
# Defer device deletion onto the shared serial work queue rather than
600+
# spawning a fresh task per teardown. datastore_delete frequently runs from
601+
# a GC/finalizer context where task switches are illegal, so the device read
602+
# and `delete_from_device!` (which may wait on an in-flight storage
603+
# transition) must not run inline here. `_enqueue_work` is finalizer-safe and
604+
# reuses one long-lived task, avoiding a Task allocation per ref teardown.
605+
_enqueue_work(_delete_device_work, state, id; gc_context=true)
594606
@safe_lock_spin datastore_lock begin
595607
haskey(datastore, id) && delete!(datastore, id)
596608
end
@@ -621,7 +633,7 @@ function migrate!(ref::DRef, to::Integer; pre_migration=nothing, dest_post_migra
621633
# Lock the ref against further accesses
622634
# FIXME: Below is racey w.r.t data mutation
623635
local new_ref
624-
@lock state.lock begin
636+
@lock getlock!(state) begin
625637
# Read the current value of the ref
626638
data = read_from_device(state, ref, true)
627639

src/storage.jl

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,17 @@ end
304304
Base.notify(sstate::StorageState) = notify(sstate.ready)
305305
Base.wait(sstate::StorageState) = wait(sstate.ready)
306306

307+
# Shared sentinels to avoid per-ref allocation on the common `poolset` path.
308+
# `ALWAYS_READY`: a permanently-set Event — refs whose data is in memory at
309+
# creation never need readers to block on `.ready`, so they can share one set
310+
# Event instead of allocating a fresh one. It is (re)notified in `__init__` so
311+
# it is set regardless of how precompilation serializes it.
312+
# `NO_LEAVES`: an empty leaves vector — `leaves` are only ever replaced via
313+
# copy-on-write (`vcat`/`filter`/`copy_leaves`), never mutated in place, so a
314+
# single shared empty vector is safe for any ref that has not been spilled.
315+
const ALWAYS_READY = Base.Event()
316+
const NO_LEAVES = StorageLeaf[]
317+
307318
mutable struct RefState
308319
# The storage state associated with the reference and its values
309320
@atomic storage::StorageState
@@ -314,8 +325,11 @@ mutable struct RefState
314325
leaf_tag::Tag
315326
# Destructor, if any
316327
destructor::Any
317-
# A Reader-Writer lock to protect access to this struct
318-
lock::ReadWriteLock
328+
# A Reader-Writer lock to protect access to this struct, created lazily on
329+
# first use (see `getlock!`): most refs are never read-locked or migrated,
330+
# so allocating the lock (a ReentrantLock + two Conditions) eagerly is pure
331+
# overhead on the ref-creation hot path.
332+
@atomic lock::Union{ReadWriteLock,Nothing}
319333
# The DRef that this value may be redirecting to
320334
redirect::Union{DRef,Nothing}
321335
end
@@ -325,7 +339,24 @@ RefState(storage::StorageState, size::Integer;
325339
RefState(storage, size,
326340
tag, leaf_tag,
327341
destructor,
328-
ReadWriteLock(), nothing)
342+
nothing, nothing)
343+
344+
"""
345+
getlock!(state::RefState) -> ReadWriteLock
346+
347+
Returns the reader-writer lock guarding `state`, creating and atomically
348+
installing it on first use. The lock is allocated lazily because most refs
349+
(small in-memory values, task handles, etc.) are never read-locked via
350+
`_getlocal`/`poolget` nor migrated, so eager allocation just wastes time and
351+
memory during ref creation.
352+
"""
353+
function getlock!(state::RefState)
354+
lk = @atomic :acquire state.lock
355+
lk !== nothing && return lk
356+
newlk = ReadWriteLock()
357+
repl = @atomicreplace :acquire_release :acquire state.lock nothing => newlk
358+
return repl.success ? newlk : (repl.old::ReadWriteLock)
359+
end
329360
function Base.getproperty(state::RefState, field::Symbol)
330361
if field === :storage
331362
throw(ArgumentError("Cannot directly read `:storage` field of `RefState`\nUse `storage_read(state)` instead"))

0 commit comments

Comments
 (0)