Simple worked examples
"Jon Harrop" <[email protected]> Sun, 4 Mar 2012 16:52:24 -0000
| Newsgroups | gmane.comp.programming.garbage-collection.general |
|---|---|
| Message-ID | <[email protected]> |
For anyone who is interested, I recently published an article about my
implementation of the Very Concurrent Garbage Collector (VCGC) in F#:
http://fsharpnews.blogspot.com/2012/02/very-concurrent-garbage-collector.htm
l
The idea of prototyping GCs using high-level languages this way seems to be
quite novel. I adopted an allocationless style of programming where the
(preallocated) heap is represented as a value type array so it is completely
unboxed and, consequently, the results will be much more representative of a
production GC implementation. Note that the heap is specialized for the
n-queens solver but the GC algorithm itself is generic.
Regarding the algorithm, the complete VCGC implementation is under 300 lines
of F# code and it achieves submillisecond pause times and throughput
comparable to .NET itself. I instrumented the code with time stamping in
order to visualize the phases the mutator and GC thread were going through,
which proved invaluable when debugging performance issues.
The exchange of information between the mutator and GC threads proved
interesting. My first solutions used message passing but this proved to be
too inefficient. So I replaced this with pairs of collections and the
exchange at barriers would swap over collections in each pair. For example,
there are two free lists with the mutator popping heap blocks off one while
the GC pushes heap blocks onto the other. There are two newly-allocated
collections that the mutator pushes freshly allocated heap blocks onto one
while the GC copies heap blocks from the other newly-allocated collection
(obtained from the mutator at the last synchronization) to its local
collection of allocated heap blocks. Now, the only collection that gets
copied at synchronization is the stack and performance is great.
Here is the source code for the GC:
module VCGC
let timer = System.Diagnostics.Stopwatch.StartNew()
[<Struct>]
type Coord =
val x : int
val y : int
new(x, y) = {x=x; y=y}
[<Measure>] type IsRefn
type Refn = int<IsRefn>
let Null = -1<IsRefn>
let isNull r =
r = -1<IsRefn>
let derefn (n: Refn) = int n
let refn (n: int) : Refn = 1<_> * n
[<Struct>]
type HeapBlock =
val x : Coord
val xs : Refn
new (x, xs) = {x=x; xs=xs}
type Refns() =
let mutable n = 0
let mutable xs : Refn [] = Array.zeroCreate 1000000
member __.Item
with get i = xs.[i]
member this.CopyFrom (ys: Refn []) m =
if n+m < xs.Length then
for i=0 to m-1 do
xs.[n+i] <- ys.[i]
n <- n + m
else
for i=0 to m-1 do
this.Push ys.[i]
member private this.SetIndexAndCopyFrom (epoch: sbyte<_> []) age (ys: Refn
[]) m =
if n+m < xs.Length then
for i=0 to m-1 do
let y = ys.[i]
epoch.[derefn y] <- age
xs.[n+i] <- y
n <- n + m
else
for i=0 to m-1 do
let y = ys.[i]
epoch.[derefn y] <- age
this.Push y
member this.SetIndexAndMoveTo epoch age (ys: Refns) =
ys.SetIndexAndCopyFrom epoch age xs n
this.Count <- 0
static member Range(i0, i1) =
let xs = Refns()
for i=i0 to i1-1 do
xs.Push (refn i)
xs
member this.Push x =
if n=xs.Length then
xs <- Array.append xs xs
xs.[n] <- x
n <- n + 1
member __.Pop() =
if n = 0 then failwith "Attempt to pop from empty extensible array"
n <- n - 1
xs.[n]
member __.Count
with get() = n
and set n' = n <- n'
member __.SetIndex (ys: int []) y =
for i=0 to n-1 do
ys.[derefn xs.[i]] <- y
member this.FilterTo (epoch: sbyte<_> []) dead (freeList: Refns) =
let rec loop src dst =
if src = n then
n <- dst
else
let x = xs.[src]
if epoch.[derefn x] = dead then
freeList.Push x
loop (src+1) dst
else
xs.[dst] <- x
loop (src+1) (dst+1)
loop 0 0
[<Struct>]
type MutatorToGC =
val stack : Refns
val newlyAllocated : Refns
new(stack, newlyAllocated) =
{stack=stack; newlyAllocated=newlyAllocated}
[<Struct>]
type GCToMutator =
val freeList : Refns
new(freeList) = {freeList=freeList}
[<Measure>]
type Epoch
type age = sbyte<Epoch>
type Heap() =
let mutable quota = 1000
let gcMarkAndYoungify (heap: HeapBlock []) young (stack: Refns) (epoch:
age []) =
let old = young - 1y<Epoch>
let rec mark (blk: Refn) =
if epoch.[derefn blk] = old then
epoch.[derefn blk] <- young // Youngify as we mark
let blk = heap.[derefn blk].xs
if not(isNull blk) then mark blk
for i=0 to stack.Count - 1 do
let blk = stack.[i]
if not(isNull blk) then mark blk
let gcSweep young (allocated: Refns) (gcFreeList: Refns) (epoch: age []) =
let dead = young - 2y<Epoch>
allocated.FilterTo epoch dead gcFreeList
let heapSize = 1000000
let mutable mutatorToGC = MutatorToGC(Refns(), Refns())
let mkGCFreeList() = Refns.Range(heapSize/2, heapSize)
let mutable mutatorFreeList = Refns.Range(0, heapSize/2)
let gcSend oldFreeList =
let newFreeList = mutatorFreeList
mutatorFreeList <- oldFreeList
newFreeList
let gcReceive young (allocated: Refns) (epoch: age []) =
let old = young - 1y<Epoch>
mutatorToGC.newlyAllocated.SetIndexAndMoveTo epoch old allocated
if mutatorToGC.newlyAllocated.Count>0 then
printfn "%d" mutatorToGC.newlyAllocated.Count
mutatorToGC.stack
let startBarrier = new System.Threading.Barrier(2)
let finishBarrier = new System.Threading.Barrier(2)
let heap = Array.create heapSize Unchecked.defaultof<HeapBlock>
let rec gcLoop young (allocated: Refns) (freeList: Refns) (epoch: sbyte<_>
[]) () =
gcPhase GCWaiting
startBarrier.SignalAndWait()
gcPhase GCPreparing
let freeList = gcSend freeList
let young = young + 1y<Epoch>
finishBarrier.SignalAndWait()
let stack = gcReceive young allocated epoch
gcPhase GCMarking
gcMarkAndYoungify heap young stack epoch
gcPhase GCSweeping
let oldHeapSize = allocated.Count
gcSweep young allocated freeList epoch
quota <- oldHeapSize - allocated.Count |> max 1000 |> min 1000
gcLoop young allocated freeList epoch ()
let gcThread =
let allocated = Refns()
let epoch = Array.create heap.Length 0y<Epoch>
System.Threading.Thread(gcLoop 2y<Epoch> allocated (mkGCFreeList())
epoch)
do
gcThread.IsBackground <- true
gcThread.Start()
let mutatorSend (stack: Refn []) sp (newlyAllocated: Refns) =
mutatorToGC.stack.Count <- 0
mutatorToGC.stack.CopyFrom stack sp
let msg = MutatorToGC(mutatorToGC.stack, newlyAllocated)
let newlyAllocated = mutatorToGC.newlyAllocated
mutatorToGC <- msg
newlyAllocated
let mutable mutatorNewlyAllocated = Refns()
member __.Item
with get i = heap.[derefn i]
member __.Alloc(h, t) =
let blk = mutatorFreeList.Pop()
heap.[derefn blk] <- HeapBlock(h, t)
mutatorNewlyAllocated.Push blk
blk
member __.GCReady =
mutatorNewlyAllocated.Count > quota
member __.GC (stack: Refn []) (sp: int) =
mutatorPhase MutatorWaiting
startBarrier.SignalAndWait()
mutatorPhase MutatorSnapshotting
mutatorNewlyAllocated <- mutatorSend stack sp mutatorNewlyAllocated
finishBarrier.SignalAndWait()
mutatorPhase MutatorRunning
let heap = Heap()
let stack = Array.create 1000 Null
let mutable sp = 0
module List =
let empty = Null
let cons x xs : Refn =
heap.Alloc(x, xs)
let isEmpty xs = xs=empty
let safe (q0: Coord) (q1: Coord) =
q0.x<>q1.x && q0.y<>q1.y && q0.x-q0.y<>q1.x-q1.y && q0.x+q0.y<>q1.x+q1.y
let rec filter q xs =
if List.isEmpty xs then List.empty else
let blk = heap.[xs]
if safe q blk.x then List.cons blk.x (filter q blk.xs) else filter q
blk.xs
let rec search n nqs qs ps a =
if List.isEmpty ps then
if nqs=n then a+1 else a
else
let osp = sp
stack.[sp] <- qs
stack.[sp+1] <- ps
sp <- sp + 2
if heap.GCReady then heap.GC stack sp
let blk = heap.[ps]
let q, ps = blk.x, blk.xs
let qs' = List.cons q qs
let ps' = filter q ps
let a = search n (nqs+1) qs' ps' a
sp <- osp
search n nqs qs ps a
let ps n =
let mutable list = List.empty
for i in 1..n do
for j in 1..n do
list <- List.cons (Coord(i, j)) list
list
let solve n =
let sols = search n 0 List.empty (ps n) 0
heap.GC [||] 0
heap.GC [||] 0
sols
let main n =
printfn "Very Concurrent Garbage Collector"
printfn "Warming up..."
for i=1 to 1000 do
heap.GC [||] 0
printfn "Running..."
printfn "%d gen-0, %d gen-1, %d gen-2"
(System.GC.CollectionCount 0) (System.GC.CollectionCount 1)
(System.GC.CollectionCount 2)
let timer = System.Diagnostics.Stopwatch.StartNew()
try
printfn "%d solutions to the %d-queens problem" (solve n) n
with _ ->
printfn "ERROR"
printfn "Took %fs" timer.Elapsed.TotalSeconds
printfn "%d gen-0, %d gen-1, %d gen-2"
(System.GC.CollectionCount 0) (System.GC.CollectionCount 1)
(System.GC.CollectionCount 2)
mutatorTimes, gcTimes
--
Dr Jon Harrop, Flying Frog Consultancy Ltd.
http://www.ffconsultancy.com