[MLton-user] Destructive update
Stephen Weeks
MLton-user@mlton.org
Wed, 8 Feb 2006 12:13:07 -0800
Hi Joe. Here's a few thoughts on your situation.
You have, of course, profiled your program to determine that the
dictionaries are the hot code? Assuming yes, you may want to try a
trick to get even more precise profiling information. If you create
your dictionary via a functor, you can instantiate that functor for
each dictionary of interest, and thus get separate profiling
information for separate uses. That may help to cut down on the
number of candidates for data structure replacement.
For the truly hot uses, you can dynamically measure whether or not a
data structure is used in a single-threaded way, and hence could be
replaced by a mutable data structure. The idea is to add two extra
fields to (the root of) your immutable structure. These fields keep
track of the version of the data structure as well as what the
"current" version is. Operations that would mutate the data structure
(like insert) create a new version and change the current version,
while operations that observe the structure (like peek and foldl)
check that they are dealing with the current version. If all the
operations deal with the current version, it can be replaced. Of
course, this isn't a proof, but it will tell you where to look.
Here's an implementation of the idea that I hope makes it clear.
--------------------------------------------------------------------------------
signature IS_SINGLE_THREADED_ARG =
sig
structure Data:
sig
type t
end
end
signature IS_SINGLE_THREADED =
sig
include IS_SINGLE_THREADED_ARG
type t
val data: t -> Data.t
val isSingleThreaded: t -> bool
val make: Data.t -> t
val mutate: t * Data.t -> t
end
functor IsSingleThreaded (S: IS_SINGLE_THREADED_ARG): IS_SINGLE_THREADED =
struct
open S
datatype t = T of {current: unit ref ref,
data: Data.t,
isSingleThreaded: bool ref,
me: unit ref}
local
fun make f (T r) = f r
in
val isSingleThreaded = ! o (make #isSingleThreaded)
end
fun make (d: Data.t): t =
let
val me = ref ()
in
T {current = ref me,
data = d,
isSingleThreaded = ref true,
me = me}
end
fun ensureCurrent (T {current, isSingleThreaded, me, ...}) =
if me = !current then
()
else
isSingleThreaded := false
fun data (i as T {data, ...}) = (ensureCurrent i; data)
fun mutate (i as T {current, isSingleThreaded, ...}, d: Data.t) =
let
val () = ensureCurrent i
val me = ref ()
val () = current := me
in
T {current = current,
data = d,
isSingleThreaded = isSingleThreaded,
me = me}
end
end
--------------------------------------------------------------------------------
I'll also mention that for the data structures you want to replace,
you can leave the functional interface in place so you don't have to
change any of your client code. Just use a mutable implementation
underneath, wrapped in a (slightly) immutable veneer, and put in
dynamic checks (similar to the version tracking above) to make sure
the data structure is used in a single-threaded way. If it isn't,
report an error. That way you can be sure that if a run completes,
the data structure was used correctly.
A couple thoughts on foldl and traversing the elements in order.
First, you can statically check to see if a given use needs foldl,
simply by eliminating the foldl from the signature and seeing if
things type check. Second, the MLton library has a fast quicksort
that you might want to try. Depending on the usage, you might be able
to get away with quicksorting the HashSet elements whenever you need
to visit them in order.