explicit default values for a leaner codegen, .cursor variables for optimization

This commit is contained in:
Andreas Rumpf 2019-03-07 00:37:23 +01:00
commit f8bf24623c
7 changed files with 172 additions and 46 deletions

View file

@ -287,6 +287,11 @@ const
sfImmediate* = sfDispatcher sfImmediate* = sfDispatcher
# macro or template is immediately expanded # macro or template is immediately expanded
# without considering any possible overloads # without considering any possible overloads
sfCursor* = sfDispatcher
# local variable has been computed to be a "cursor".
# see cursors.nim for details about what that means.
sfAllUntyped* = sfVolatile # macro or template is immediately expanded \ sfAllUntyped* = sfVolatile # macro or template is immediately expanded \
# in a generic context # in a generic context

72
compiler/cursors.nim Normal file
View file

@ -0,0 +1,72 @@
#
#
# The Nim Compiler
# (c) Copyright 2019 Andreas Rumpf
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
import
intsets, ast, astalgo, msgs, renderer, magicsys, types, idents, trees,
strutils, options, dfa, lowerings, tables, modulegraphs, msgs,
lineinfos, parampatterns
##[
This module implements "cursor" detection. A cursor is a local variable
that is used for navigation in a datastructure, it does not "own" the
data it aliases but it might update the underlying datastructure.
Two primary examples for cursors that I have in mind and that are critical
for optimization:
1. Local string variable introduced by ``for x in a``::
var i = 0
while i < a.len:
let cursor = a[i]
use cursor
inc i
2. Local ``ref`` variable for navigation::
var cursor = listHead
while cursor != nil:
use cursor
cursor = cursor.next
Cursors are very interesting for the optimizer because they can be copyMem'ed
and don't need a destructor.
More formally, a cursor is a variable that is set on all paths to
a *location* or a proc call that produced a ``lent/var`` type. All statements
that come after these assignments MUST not mutate what the cursor aliases.
Mutations *through* the cursor are allowed if the cursor has ref semantics.
Look at this complex real world example taken from the compiler itself:
.. code-block:: Nim
proc getTypeName(m: BModule; typ: PType; sig: SigHash): Rope =
var t = typ
while true:
if t.sym != nil and {sfImportc, sfExportc} * t.sym.flags != {}:
return t.sym.loc.r
if t.kind in irrelevantForBackend:
t = t.lastSon
else:
break
let typ = if typ.kind in {tyAlias, tySink, tyOwned}: typ.lastSon else: typ
if typ.loc.r == nil:
typ.loc.r = typ.typeName & $sig
result = typ.loc.r
if result == nil: internalError(m.config, "getTypeName: " & $typ.kind)
Here `t` is a cursor but without a control flow based analysis we are unlikely
to detect it.
]##
# Araq: I owe you an implementation. For now use the .cursor pragma. :-/

View file

@ -161,6 +161,8 @@ type
graph: ModuleGraph graph: ModuleGraph
emptyNode: PNode emptyNode: PNode
otherRead: PNode otherRead: PNode
uninit: IntSet # set of uninit'ed vars
uninitComputed: bool
proc isLastRead(s: PSym; c: var Con; pc, comesFrom: int): int = proc isLastRead(s: PSym; c: var Con; pc, comesFrom: int): int =
var pc = pc var pc = pc
@ -247,6 +249,42 @@ proc isLastRead(n: PNode; c: var Con): bool =
#echo c.graph.config $ n.info, " last read here!" #echo c.graph.config $ n.info, " last read here!"
return true return true
proc initialized(code: ControlFlowGraph; pc: int,
init, uninit: var IntSet; comesFrom: int): int =
## Computes the set of definitely initialized variables accross all code paths
## as an IntSet of IDs.
var pc = pc
while pc < code.len:
case code[pc].kind
of goto:
pc = pc + code[pc].dest
of fork:
let target = pc + code[pc].dest
var initA = initIntSet()
var initB = initIntSet()
let pcA = initialized(code, pc+1, initA, uninit, pc)
discard initialized(code, target, initB, uninit, pc)
# we add vars if they are in both branches:
for v in initA:
if v in initB:
init.incl v
pc = pcA+1
of InstrKind.join:
let target = pc + code[pc].dest
if comesFrom == target: return pc
inc pc
of use:
let v = code[pc].sym
if v.kind != skParam and v.id notin init:
# attempt to read an uninit'ed variable
uninit.incl v.id
inc pc
of def:
let v = code[pc].sym
init.incl v.id
inc pc
return pc
template interestingSym(s: PSym): bool = template interestingSym(s: PSym): bool =
s.owner == c.owner and s.kind in InterestingSyms and hasDestructor(s.typ) s.owner == c.owner and s.kind in InterestingSyms and hasDestructor(s.typ)
@ -353,6 +391,11 @@ proc genWasMoved(n: PNode; c: var Con): PNode =
# The mWasMoved builtin does not take the address. # The mWasMoved builtin does not take the address.
result = genMagicCall(n, c, "wasMoved", mWasMoved) result = genMagicCall(n, c, "wasMoved", mWasMoved)
proc genDefaultCall(t: PType; c: Con; info: TLineInfo): PNode =
result = newNodeI(nkCall, info)
result.add(newSymNode(createMagic(c.graph, "default", mDefault)))
result.typ = t
proc destructiveMoveVar(n: PNode; c: var Con): PNode = proc destructiveMoveVar(n: PNode; c: var Con): PNode =
# generate: (let tmp = v; reset(v); tmp) # generate: (let tmp = v; reset(v); tmp)
# XXX: Strictly speaking we can only move if there is a ``=sink`` defined # XXX: Strictly speaking we can only move if there is a ``=sink`` defined
@ -572,6 +615,38 @@ proc moveOrCopy(dest, ri: PNode; c: var Con): PNode =
result = genCopy(c, dest.typ, dest, ri) result = genCopy(c, dest.typ, dest, ri)
result.add p(ri, c) result.add p(ri, c)
proc computeUninit(c: var Con) =
if not c.uninitComputed:
c.uninitComputed = true
c.uninit = initIntSet()
var init = initIntSet()
discard initialized(c.g, pc = 0, init, c.uninit, comesFrom = -1)
proc injectDefaultCalls(n: PNode, c: var Con) =
case n.kind
of nkVarSection, nkLetSection:
for i in 0..<n.len:
let it = n[i]
let L = it.len-1
let ri = it[L]
if it.kind == nkIdentDefs and ri.kind == nkEmpty:
computeUninit(c)
for j in 0..L-2:
let v = it[j]
doAssert v.kind == nkSym
if c.uninit.contains(v.sym.id):
it[L] = genDefaultCall(v.sym.typ, c, v.info)
break
of nkNone..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef, nkMethodDef,
nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo, nkFuncDef:
discard
else:
for i in 0..<safeLen(n):
injectDefaultCalls(n[i], c)
proc isCursor(n: PNode): bool {.inline.} =
result = n.kind == nkSym and sfCursor in n.sym.flags
proc p(n: PNode; c: var Con): PNode = proc p(n: PNode; c: var Con): PNode =
case n.kind case n.kind
of nkVarSection, nkLetSection: of nkVarSection, nkLetSection:
@ -585,7 +660,7 @@ proc p(n: PNode; c: var Con): PNode =
if it.kind == nkVarTuple and hasDestructor(ri.typ): if it.kind == nkVarTuple and hasDestructor(ri.typ):
let x = lowerTupleUnpacking(c.graph, it, c.owner) let x = lowerTupleUnpacking(c.graph, it, c.owner)
result.add p(x, c) result.add p(x, c)
elif it.kind == nkIdentDefs and hasDestructor(it[0].typ): elif it.kind == nkIdentDefs and hasDestructor(it[0].typ) and not isCursor(it[0]):
for j in 0..L-2: for j in 0..L-2:
let v = it[j] let v = it[j]
doAssert v.kind == nkSym doAssert v.kind == nkSym
@ -669,6 +744,8 @@ proc injectDestructorCalls*(g: ModuleGraph; owner: PSym; n: PNode): PNode =
if param.typ.kind == tySink and hasDestructor(param.typ.sons[0]): if param.typ.kind == tySink and hasDestructor(param.typ.sons[0]):
c.destroys.add genDestroy(c, param.typ.skipTypes({tyGenericInst, tyAlias, tySink}), params[i]) c.destroys.add genDestroy(c, param.typ.skipTypes({tyGenericInst, tyAlias, tySink}), params[i])
if optNimV2 in c.graph.config.globalOptions:
injectDefaultCalls(n, c)
let body = p(n, c) let body = p(n, c)
result = newNodeI(nkStmtList, n.info) result = newNodeI(nkStmtList, n.info)
if c.topLevelVars.len > 0: if c.topLevelVars.len > 0:

View file

@ -686,42 +686,3 @@ proc constructCfg*(s: PSym; body: PNode): ControlFlowGraph =
gen(c, body) gen(c, body)
genImplicitReturn(c) genImplicitReturn(c)
shallowCopy(result, c.code) shallowCopy(result, c.code)
proc interpret(code: ControlFlowGraph; pc: int, state: seq[PSym], comesFrom: int; threadId: int): (seq[PSym], int) =
var res = state
var pc = pc
while pc < code.len:
#echo threadId, " ", code[pc].kind
case code[pc].kind
of goto:
pc = pc + code[pc].dest
of fork:
let target = pc + code[pc].dest
let (branchA, pcA) = interpret(code, pc+1, res, pc, threadId+1)
let (branchB, _) = interpret(code, target, res, pc, threadId+2)
# we add vars if they are in both branches:
for v in branchB:
if v in branchA:
if v notin res:
res.add v
pc = pcA+1
of join:
let target = pc + code[pc].dest
if comesFrom == target: return (res, pc)
inc pc
of use:
let v = code[pc].sym
if v notin res and v.kind != skParam:
echo "attempt to read uninitialized variable ", v.name.s
inc pc
of def:
let v = code[pc].sym
if v notin res:
res.add v
inc pc
return (res, pc)
proc dataflowAnalysis*(s: PSym; body: PNode) =
let c = constructCfg(s, body)
#echoCfg c
discard interpret(c, 0, @[], -1, 1)

View file

@ -24,7 +24,7 @@ const
wCompilerProc, wNonReloadable, wCore, wProcVar, wDeprecated, wVarargs, wCompileTime, wMerge, wCompilerProc, wNonReloadable, wCore, wProcVar, wDeprecated, wVarargs, wCompileTime, wMerge,
wBorrow, wExtern, wImportCompilerProc, wThread, wImportCpp, wImportObjC, wBorrow, wExtern, wImportCompilerProc, wThread, wImportCpp, wImportObjC,
wAsmNoStackFrame, wError, wDiscardable, wNoInit, wCodegenDecl, wAsmNoStackFrame, wError, wDiscardable, wNoInit, wCodegenDecl,
wGensym, wInject, wRaises, wTags, wLocks, wDelegator, wGcSafe, wOverride, wGensym, wInject, wRaises, wTags, wLocks, wDelegator, wGcSafe,
wConstructor, wExportNims, wUsed, wLiftLocals, wStacktrace, wLinetrace} wConstructor, wExportNims, wUsed, wLiftLocals, wStacktrace, wLinetrace}
converterPragmas* = procPragmas converterPragmas* = procPragmas
methodPragmas* = procPragmas+{wBase}-{wImportCpp} methodPragmas* = procPragmas+{wBase}-{wImportCpp}
@ -64,7 +64,7 @@ const
varPragmas* = {wImportc, wExportc, wVolatile, wRegister, wThreadVar, wNodecl, varPragmas* = {wImportc, wExportc, wVolatile, wRegister, wThreadVar, wNodecl,
wMagic, wHeader, wDeprecated, wCompilerProc, wCore, wDynlib, wExtern, wMagic, wHeader, wDeprecated, wCompilerProc, wCore, wDynlib, wExtern,
wImportCpp, wImportObjC, wError, wNoInit, wCompileTime, wGlobal, wImportCpp, wImportObjC, wError, wNoInit, wCompileTime, wGlobal,
wGensym, wInject, wCodegenDecl, wGuard, wGoto, wExportNims, wUsed} wGensym, wInject, wCodegenDecl, wGuard, wGoto, wExportNims, wUsed, wCursor}
constPragmas* = {wImportc, wExportc, wHeader, wDeprecated, wMagic, wNodecl, constPragmas* = {wImportc, wExportc, wHeader, wDeprecated, wMagic, wNodecl,
wExtern, wImportCpp, wImportObjC, wError, wGensym, wInject, wExportNims, wExtern, wImportCpp, wImportObjC, wError, wGensym, wInject, wExportNims,
wIntDefine, wStrDefine, wBoolDefine, wUsed, wCompilerProc, wCore} wIntDefine, wStrDefine, wBoolDefine, wUsed, wCompilerProc, wCore}
@ -863,8 +863,6 @@ proc singlePragma(c: PContext, sym: PSym, n: PNode, i: var int,
incl(sym.loc.flags, lfNoDecl) incl(sym.loc.flags, lfNoDecl)
# implies nodecl, because otherwise header would not make sense # implies nodecl, because otherwise header would not make sense
if sym.loc.r == nil: sym.loc.r = rope(sym.name.s) if sym.loc.r == nil: sym.loc.r = rope(sym.name.s)
of wOverride:
sym.flags.incl sfOverriden
of wNosideeffect: of wNosideeffect:
noVal(c, it) noVal(c, it)
if sym != nil: if sym != nil:
@ -1091,6 +1089,11 @@ proc singlePragma(c: PContext, sym: PSym, n: PNode, i: var int,
invalidPragma(c, it) invalidPragma(c, it)
else: else:
sym.flags.incl sfGoto sym.flags.incl sfGoto
of wCursor:
if sym == nil or sym.kind notin {skVar, skLet}:
invalidPragma(c, it)
else:
sym.flags.incl sfCursor
of wExportNims: of wExportNims:
if sym == nil: invalidPragma(c, it) if sym == nil: invalidPragma(c, it)
else: magicsys.registerNimScriptSymbol(c.graph, sym) else: magicsys.registerNimScriptSymbol(c.graph, sym)

View file

@ -37,7 +37,7 @@ type
wMagic, wThread, wFinal, wProfiler, wMemTracker, wObjChecks, wMagic, wThread, wFinal, wProfiler, wMemTracker, wObjChecks,
wIntDefine, wStrDefine, wBoolDefine wIntDefine, wStrDefine, wBoolDefine
wDestroy, wCursor,
wImmediate, wConstructor, wDestructor, wDelegator, wOverride, wImmediate, wConstructor, wDestructor, wDelegator, wOverride,
wImportCpp, wImportObjC, wImportCpp, wImportObjC,
@ -126,7 +126,7 @@ const
"magic", "thread", "final", "profiler", "memtracker", "objchecks", "magic", "thread", "final", "profiler", "memtracker", "objchecks",
"intdefine", "strdefine", "booldefine", "intdefine", "strdefine", "booldefine",
"destroy", "cursor",
"immediate", "constructor", "destructor", "delegator", "override", "immediate", "constructor", "destructor", "delegator", "override",
"importcpp", "importobjc", "importcpp", "importobjc",

View file

@ -517,6 +517,14 @@ on Linux::
nim c --dynlibOverride:lua --passL:liblua.lib program.nim nim c --dynlibOverride:lua --passL:liblua.lib program.nim
Cursor pragma
=============
The ``.cursor`` pragma is a temporary tool for optimization purposes
and this property will be computed by Nim's optimizer eventually. Thus it
remains undocumented.
Backend language options Backend language options
======================== ========================