optimized writetracking

This commit is contained in:
Araq
2015-09-02 14:33:28 +02:00
parent 580855d2eb
commit aa8da7f031

View File

@@ -9,8 +9,13 @@
## This module implements the write tracking analysis. Read my block post for
## a basic description of the algorithm and ideas.
## The algorithm operates in 2 phases:
##
## * Collecting information about assignments (and pass-by-var calls).
## * Computing an aliasing relation based on the assignments. This relation
## is then used to compute the 'writes' and 'escapes' effects.
import idents, ast, astalgo, trees, renderer, msgs, types
import intsets, idents, ast, astalgo, trees, renderer, msgs, types
const
debug = false
@@ -24,106 +29,33 @@ type
newNone,
newLit,
newCall
RootInfo = enum
rootIsResultOrParam,
rootIsHeapAccess,
rootIsSym,
markAsWrittenTo,
markAsEscaping
Assignment = object # \
# Note that the transitive closures MUST be computed in
# phase 2 of the algorithm.
dest, src: seq[ptr TSym] # we use 'ptr' here to save RC ops and GC cycles
destNoTc, srcNoTc: int # length of 'dest', 'src' without the
# transitive closure
destInfo: set[RootInfo]
info: TLineInfo
W = object # WriteTrackContext
owner: PSym
returnsNew: AssignToResult # assignments to 'result'
markAsWrittenTo, markAsEscaping: PNode
assignments: seq[(PNode, PNode)] # list of all assignments in this proc
assignments: seq[Assignment] # list of all assignments in this proc
proc returnsNewExpr*(n: PNode): NewLocation =
case n.kind
of nkCharLit..nkInt64Lit, nkStrLit..nkTripleStrLit,
nkFloatLit..nkFloat64Lit, nkNilLit:
result = newLit
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv,
nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkOfBranch,
nkElifBranch, nkElse, nkExceptBranch, nkFinally, nkCast:
result = returnsNewExpr(n.lastSon)
of nkCurly, nkBracket, nkPar, nkObjConstr, nkClosure,
nkIfExpr, nkIfStmt, nkWhenStmt, nkCaseStmt, nkTryStmt:
result = newLit
for i in ord(n.kind == nkObjConstr) .. <n.len:
let x = returnsNewExpr(n.sons[i])
case x
of newNone: return newNone
of newLit: discard
of newCall: result = newCall
of nkCallKinds:
if n.sons[0].typ != nil and tfReturnsNew in n.sons[0].typ.flags:
result = newCall
else:
result = newNone
proc deps(w: var W; dest, src: PNode) =
# let x = (localA, localB)
# compute 'returnsNew' property:
let retNew = returnsNewExpr(src)
if dest.kind == nkSym and dest.sym.kind == skResult:
if retNew != newNone:
if w.returnsNew != asgnOther: w.returnsNew = asgnNew
else:
w.returnsNew = asgnOther
# mark the dependency, but
# rule out obviously innocent assignments like 'somebool = true'
if dest.kind == nkSym and retNew == newLit: discard
else: w.assignments.add((dest, src))
proc depsArgs(w: var W; n: PNode) =
if n.sons[0].typ.isNil: return
var typ = skipTypes(n.sons[0].typ, abstractInst)
if typ.kind != tyProc: return
# echo n.info, " ", n, " ", w.owner.name.s, " ", typeToString(typ)
assert(sonsLen(typ) == sonsLen(typ.n))
for i in 1 ..< n.len:
let it = n.sons[i]
if i < sonsLen(typ):
assert(typ.n.sons[i].kind == nkSym)
let paramType = typ.n.sons[i]
if paramType.typ.isCompileTimeOnly: continue
if sfWrittenTo in paramType.sym.flags or paramType.typ.kind == tyVar:
# p(f(x, y), X, g(h, z))
deps(w, it, w.markAsWrittenTo)
if sfEscapes in paramType.sym.flags:
deps(w, it, w.markAsEscaping)
proc deps(w: var W; n: PNode) =
case n.kind
of nkLetSection, nkVarSection:
for child in n:
let last = lastSon(child)
if last.kind == nkEmpty: continue
if child.kind == nkVarTuple and last.kind == nkPar:
internalAssert child.len-2 == last.len
for i in 0 .. child.len-3:
deps(w, child.sons[i], last.sons[i])
else:
for i in 0 .. child.len-3:
deps(w, child.sons[i], last)
of nkAsgn, nkFastAsgn:
deps(w, n.sons[0], n.sons[1])
else:
for i in 0 ..< n.safeLen:
deps(w, n.sons[i])
if n.kind in nkCallKinds:
if getMagic(n) in {mNew, mNewFinalize, mNewSeq}:
# may not look like an assignment, but it is:
deps(w, n.sons[1], newNodeIT(nkObjConstr, n.info, n.sons[1].typ))
else:
depsArgs(w, n)
type
RootInfo = enum
rootIsResultOrParam,
rootIsHeapAccess
proc allRoots(n: PNode; result: var seq[PSym]; info: var set[RootInfo]) =
proc allRoots(n: PNode; result: var seq[ptr TSym]; info: var set[RootInfo]) =
case n.kind
of nkSym:
if n.sym.kind in {skParam, skVar, skTemp, skLet, skResult, skForVar}:
if result.isNil: result = @[]
if n.sym notin result:
if n.sym.kind in {skResult, skParam}: incl(info, rootIsResultOrParam)
result.add n.sym
if n.sym.kind in {skResult, skParam}: incl(info, rootIsResultOrParam)
result.add(cast[ptr TSym](n.sym))
of nkHiddenDeref, nkDerefExpr:
incl(info, rootIsHeapAccess)
allRoots(n.sons[0], result, info)
@@ -161,102 +93,180 @@ proc allRoots(n: PNode; result: var seq[PSym]; info: var set[RootInfo]) =
for i in 0..<n.safeLen:
allRoots(n.sons[i], result, info)
proc allRoots(n: PNode; result: var seq[PSym]) =
var dummy: set[RootInfo]
allRoots(n, result, dummy)
proc addAsgn(a: var Assignment; dest, src: PNode; destInfo: set[RootInfo]) =
a.dest = @[]
a.src = @[]
a.destInfo = destInfo
allRoots(dest, a.dest, a.destInfo)
if dest.kind == nkSym: incl(a.destInfo, rootIsSym)
if src != nil:
var dummy: set[RootInfo]
allRoots(src, a.src, dummy)
a.destNoTc = a.dest.len
a.srcNoTc = a.src.len
a.info = dest.info
#echo "ADDING ", dest.info, " ", a.destInfo
proc hasSym(n: PNode; x: PSym): bool =
when false:
if n.kind == nkSym:
result = n.sym == x
else:
for i in 0..safeLen(n)-1:
if hasSym(n.sons[i], x): return true
proc srcHasSym(a: Assignment; x: ptr TSym): bool =
for i in 0 ..< a.srcNoTc:
if a.src[i] == x: return true
proc returnsNewExpr*(n: PNode): NewLocation =
case n.kind
of nkCharLit..nkInt64Lit, nkStrLit..nkTripleStrLit,
nkFloatLit..nkFloat64Lit, nkNilLit:
result = newLit
of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv,
nkStmtList, nkStmtListExpr, nkBlockStmt, nkBlockExpr, nkOfBranch,
nkElifBranch, nkElse, nkExceptBranch, nkFinally, nkCast:
result = returnsNewExpr(n.lastSon)
of nkCurly, nkBracket, nkPar, nkObjConstr, nkClosure,
nkIfExpr, nkIfStmt, nkWhenStmt, nkCaseStmt, nkTryStmt:
result = newLit
for i in ord(n.kind == nkObjConstr) .. <n.len:
let x = returnsNewExpr(n.sons[i])
case x
of newNone: return newNone
of newLit: discard
of newCall: result = newCall
of nkCallKinds:
if n.sons[0].typ != nil and tfReturnsNew in n.sons[0].typ.flags:
result = newCall
else:
var tmp: seq[PSym]
allRoots(n, tmp)
result = not tmp.isNil and x in tmp
result = newNone
when debug:
proc `$`*(x: PSym): string = x.name.s
proc deps(w: var W; dest, src: PNode; destInfo: set[RootInfo]) =
# let x = (localA, localB)
# compute 'returnsNew' property:
let retNew = if src.isNil: newNone else: returnsNewExpr(src)
if dest.kind == nkSym and dest.sym.kind == skResult:
if retNew != newNone:
if w.returnsNew != asgnOther: w.returnsNew = asgnNew
else:
w.returnsNew = asgnOther
# mark the dependency, but
# rule out obviously innocent assignments like 'somebool = true'
if dest.kind == nkSym and retNew == newLit: discard
else:
let L = w.assignments.len
w.assignments.setLen(L+1)
addAsgn(w.assignments[L], dest, src, destInfo)
proc possibleAliases(w: W; result: var seq[PSym]) =
var todo = 0
proc depsArgs(w: var W; n: PNode) =
if n.sons[0].typ.isNil: return
var typ = skipTypes(n.sons[0].typ, abstractInst)
if typ.kind != tyProc: return
# echo n.info, " ", n, " ", w.owner.name.s, " ", typeToString(typ)
assert(sonsLen(typ) == sonsLen(typ.n))
for i in 1 ..< n.len:
let it = n.sons[i]
if i < sonsLen(typ):
assert(typ.n.sons[i].kind == nkSym)
let paramType = typ.n.sons[i]
if paramType.typ.isCompileTimeOnly: continue
var destInfo: set[RootInfo] = {}
if sfWrittenTo in paramType.sym.flags or paramType.typ.kind == tyVar:
# p(f(x, y), X, g(h, z))
destInfo.incl markAsWrittenTo
if sfEscapes in paramType.sym.flags:
destInfo.incl markAsEscaping
if destInfo != {}:
deps(w, it, nil, destInfo)
proc deps(w: var W; n: PNode) =
case n.kind
of nkLetSection, nkVarSection:
for child in n:
let last = lastSon(child)
if last.kind == nkEmpty: continue
if child.kind == nkVarTuple and last.kind == nkPar:
internalAssert child.len-2 == last.len
for i in 0 .. child.len-3:
deps(w, child.sons[i], last.sons[i], {})
else:
for i in 0 .. child.len-3:
deps(w, child.sons[i], last, {})
of nkAsgn, nkFastAsgn:
deps(w, n.sons[0], n.sons[1], {})
else:
for i in 0 ..< n.safeLen:
deps(w, n.sons[i])
if n.kind in nkCallKinds:
if getMagic(n) in {mNew, mNewFinalize, mNewSeq}:
# may not look like an assignment, but it is:
deps(w, n.sons[1], newNodeIT(nkObjConstr, n.info, n.sons[1].typ), {})
else:
depsArgs(w, n)
proc possibleAliases(w: var W; result: var seq[ptr TSym]) =
# this is an expensive fixpoint iteration. We could speed up this analysis
# by a smarter data-structure but we wait until prolifing shows us it's
# expensive. Usually 'w.assignments' is small enough.
var alreadySeen = initIntSet()
template addNoDup(x) =
if not alreadySeen.containsOrIncl(x.id): result.add x
for x in result: alreadySeen.incl x.id
var todo = 0
while todo < result.len:
let x = result[todo]
inc todo
when debug:
if w.owner.name.s == "m3": echo "select ", x, " ", todo, " ", result.len
for dest, src in items(w.assignments):
if src.hasSym(x):
# dest = f(..., s, ...)
allRoots(dest, result)
when debug:
if w.owner.name.s == "m3": echo "A ", result
elif dest.kind == nkSym and dest.sym == x:
# s = f(..., x, ....)
allRoots(src, result)
when debug:
if w.owner.name.s == "m3": echo "B ", result
else:
when debug:
if w.owner.name.s == "m3": echo "C ", x, " ", todo, " ", result.len
for a in mitems(w.assignments):
#if a.srcHasSym(x):
# # y = f(..., x, ...)
# for i in 0 ..< a.destNoTc: addNoDup a.dest[i]
if a.destNoTc > 0 and a.dest[0] == x and rootIsSym in a.destInfo:
# x = f(..., y, ....)
for i in 0 ..< a.srcNoTc: addNoDup a.src[i]
proc markDirty(w: W) =
for dest, src in items(w.assignments):
var r: seq[PSym] = nil
var info: set[RootInfo]
allRoots(dest, r, info)
when debug:
if w.owner.info ?? "temp18":
echo "ASGN ", dest, " = ", src, " |", heapAccess, " ", r.name.s
if rootIsHeapAccess in info or src == w.markAsWrittenTo:
# we have an assignment like:
# local.foo = bar
# --> check which parameter it may alias and mark these parameters
# as dirty:
possibleAliases(w, r)
for a in r:
if a.kind == skParam and a.owner == w.owner:
incl(a.flags, sfWrittenTo)
proc markWriteOrEscape(w: var W) =
## Both 'writes' and 'escapes' effects ultimately only care
## about *parameters*.
## However, due to aliasing, even locals that might not look as parameters
## have to count as parameters if they can alias a parameter:
##
## .. code-block:: nim
## proc modifies(n: Node) {.writes: [n].} =
## let x = n
## x.data = "abc"
##
## We call a symbol *parameter-like* if it is a parameter or can alias a
## parameter.
## Let ``p``, ``q`` be *parameter-like* and ``x``, ``y`` be general
## expressions.
##
## A write then looks like ``p[] = x``.
## An escape looks like ``p[] = q`` or more generally
## like ``p[] = f(q)`` where ``f`` can forward ``q``.
for a in mitems(w.assignments):
if a.destInfo != {}:
possibleAliases(w, a.dest)
proc markEscaping(w: W) =
# let p1 = p
# let p2 = q
# p2.x = call(..., p1, ...)
for dest, src in items(w.assignments):
var r: seq[PSym] = nil
var info: set[RootInfo]
allRoots(dest, r, info)
if {rootIsHeapAccess, markAsWrittenTo} * a.destInfo != {}:
for p in a.dest:
if p.kind == skParam and p.owner == w.owner:
incl(p.flags, sfWrittenTo)
if (r.len > 0) and (info != {} or src == w.markAsEscaping):
possibleAliases(w, r)
if {rootIsResultOrParam, rootIsHeapAccess, markAsEscaping}*a.destInfo != {}:
var destIsParam = false
for a in r:
if a.kind in {skResult, skParam} and a.owner == w.owner:
for p in a.dest:
if p.kind in {skResult, skParam} and p.owner == w.owner:
destIsParam = true
break
if destIsParam:
var victims: seq[PSym] = @[]
allRoots(src, victims)
possibleAliases(w, victims)
for v in victims:
if v.kind == skParam and v.owner == w.owner:
incl(v.flags, sfEscapes)
possibleAliases(w, a.src)
for p in a.src:
if p.kind == skParam and p.owner == w.owner:
incl(p.flags, sfEscapes)
proc trackWrites*(owner: PSym; body: PNode) =
var w: W
w.owner = owner
w.markAsWrittenTo = newNodeI(nkArgList, body.info)
w.markAsEscaping = newNodeI(nkArgList, body.info)
w.assignments = @[]
# Phase 1: Collect and preprocess any assignments in the proc body:
deps(w, body)
markDirty(w)
markEscaping(w)
# Phase 2: Compute the 'writes' and 'escapes' effects:
markWriteOrEscape(w)
if w.returnsNew != asgnOther and not isEmptyType(owner.typ.sons[0]) and
containsGarbageCollectedRef(owner.typ.sons[0]):
incl(owner.typ.flags, tfReturnsNew)