-
Notifications
You must be signed in to change notification settings - Fork 121
[wip] escape analysis 2 #3213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
aleksisch
wants to merge
2
commits into
master
Choose a base branch
from
aleksisch/escape-analysis-2
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
[wip] escape analysis 2 #3213
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,16 +62,46 @@ namespace das { | |
| return nullptr; | ||
| } | ||
|
|
||
| // Passing the pointer to such a call cannot let it escape: the callee is a built-in (C++) that is | ||
| // fully pure (no declared side effects, not unsafe, so it can't store the argument anywhere) and | ||
| // whose return can't carry the pointer back out (non-ref, void-or-workhorse). Restricted to | ||
| // built-ins because their SideEffects are declared at bind time and reliable here; script-function | ||
| // side effects are only inferred in a later pass (ast_unused), so script calls stay conservative. | ||
| static bool isEscapeNeutralCall ( ExprCall * call ) { | ||
| // positional index of `arg` in the call's argument list (~0 if not found) | ||
| static size_t callArgIndex ( ExprLooksLikeCall * call, Expression * arg ) { | ||
| for ( size_t i=0; i!=call->arguments.size(); ++i ) { | ||
| if ( call->arguments[i]==arg ) return i; | ||
| } | ||
| return ~size_t(0); | ||
| } | ||
|
|
||
| // a by-value (non-ref) pointer to a daslang struct - the value whose escape we can track | ||
| static bool isPointerToStruct ( const TypeDeclPtr & typ ) { | ||
| return typ && !typ->ref && typ->baseType==Type::tPointer && !typ->smartPtr | ||
| && typ->firstType && typ->firstType->baseType==Type::tStructure; | ||
| } | ||
|
|
||
| static bool isParamEscapeCandidate ( const VariablePtr & var ) { | ||
| return isPointerToStruct(var->type); | ||
| } | ||
|
|
||
| // a function whose body we can soundly analyze for parameter escape: visible body, no hidden | ||
| // aliasing via unsafe, not a generated / generator / lambda shape the field-base analysis can't model | ||
| static bool isParamAnalyzableFunc ( Function * func ) { | ||
| return func && !func->builtIn && !func->stub && !func->isTemplate | ||
| && !func->generated && !func->generator && !func->lambda && !func->hasUnsafe; | ||
| } | ||
|
|
||
| // can a pointer passed at positional `argIndex` of this call escape through the callee? returns | ||
| // true when it CANNOT (escape-neutral for that one argument). built-ins are judged by their | ||
| // declared side effects + a return that can't carry the pointer out; script functions by the | ||
| // interprocedural per-parameter result computed in ParamEscapeAnalysis (which already folds in the | ||
| // return / global-store / store-into-another-arg / transitive-call channels). | ||
| static bool isArgEscapeNeutral ( ExprCallFunc * call, size_t argIndex ) { | ||
| auto fn = call->func; | ||
| if ( !fn || !fn->builtIn || fn->sideEffectFlags != 0 || fn->unsafeOperation ) return false; | ||
| auto res = fn->result; | ||
| return res && !res->ref && (res->isVoid() || res->isWorkhorseType()); | ||
| if ( !fn || fn->unsafeOperation ) return false; | ||
| if ( fn->builtIn ) { | ||
| if ( fn->sideEffectFlags != 0 ) return false; | ||
| auto res = fn->result; | ||
| return res && !res->ref && (res->isVoid() || res->isWorkhorseType()); | ||
| } | ||
| if ( argIndex >= fn->arguments.size() ) return false; | ||
| return fn->arguments[argIndex]->does_not_escape; | ||
| } | ||
|
|
||
| static bool escapeDecided ( Variable * var ) { | ||
|
|
@@ -135,6 +165,109 @@ namespace das { | |
| << " in '" << func->module->name << "::" << func->name << "'\n"; | ||
| } | ||
|
|
||
| // ===== Pass 0: interprocedural parameter escape (fixpoint) ===== | ||
| // For every analyzable function, decide per by-value pointer parameter whether that pointer can | ||
| // escape the function. Optimistic fixpoint: seed all candidate params as escape-free, then revoke | ||
| // any whose body leaks the pointer (return / store / capture into a closure / pass to a non-neutral | ||
| // arg), iterating until stable so transitive and mutually-recursive calls converge. Result lands on | ||
| // the parameter Variable::does_not_escape and is consumed by isArgEscapeNeutral at call sites. | ||
| class ParamEscapeAnalysis { | ||
| public: | ||
| bool anyChanged = false; | ||
| ParamEscapeAnalysis ( TextWriter * logs_ ) : logs(logs_) {} | ||
| void run ( Program * prog ) { | ||
| prog->thisModule->functions.foreach([&](auto & fn){ | ||
| bool ok = isParamAnalyzableFunc(fn); | ||
| for ( auto & arg : fn->arguments ) { | ||
| if ( isParamEscapeCandidate(arg) ) arg->does_not_escape = ok; // seed optimistic | ||
| } | ||
| }); | ||
| bool changed = true; | ||
| while ( changed ) { | ||
| changed = false; | ||
| prog->thisModule->functions.foreach([&](auto & fn){ | ||
| if ( !isParamAnalyzableFunc(fn) ) return; | ||
| bool anyLive = false; | ||
| for ( auto & arg : fn->arguments ) { | ||
| if ( isParamEscapeCandidate(arg) && arg->does_not_escape ) { anyLive = true; break; } | ||
| } | ||
| if ( !anyLive ) return; | ||
| ClassifyVisitor cv(fn); | ||
| fn->visit(cv); | ||
| for ( auto & arg : fn->arguments ) { | ||
| if ( !isParamEscapeCandidate(arg) || !arg->does_not_escape ) continue; | ||
| if ( cv.escaped.find(arg)!=cv.escaped.end() ) { | ||
| arg->does_not_escape = false; | ||
| changed = true; | ||
| anyChanged = true; | ||
| if ( logs ) logParam(fn, arg); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| protected: | ||
| void logParam ( Function * fn, Variable * var ) { | ||
| if ( !var->at.empty() && var->at.fileInfo ) { | ||
| *logs << var->at.fileInfo->name << ":" << var->at.line << ":" << var->at.column << " "; | ||
| } | ||
| *logs << "escape analysis: parameter '" << var->name << "' escapes in '" | ||
| << fn->module->name << "::" << fn->name << "'\n"; | ||
| } | ||
| // visits a function body, collecting which candidate parameters leak the pointer value | ||
| class ClassifyVisitor : public Visitor { | ||
| public: | ||
| ClassifyVisitor ( Function * f ) : fn(f) {} | ||
| das_set<Variable *> escaped; | ||
| protected: | ||
| bool isCandidateParam ( Variable * v ) { | ||
| for ( auto & a : fn->arguments ) if ( a==v ) return isParamEscapeCandidate(a); | ||
| return false; | ||
| } | ||
| void escapeByName ( const string & name ) { | ||
| for ( auto & a : fn->arguments ) { | ||
| if ( a->name==name && isParamEscapeCandidate(a) ) escaped.insert(a); | ||
| } | ||
| } | ||
| virtual void preVisit ( ExprField * expr ) override { | ||
| Visitor::preVisit(expr); | ||
| if ( auto v = derefBaseVar(expr->value) ) safeBase.insert(v); | ||
| } | ||
| virtual void preVisitCallArg ( ExprCall * call, Expression * arg, bool last ) override { | ||
| Visitor::preVisitCallArg(call, arg, last); | ||
| if ( isArgEscapeNeutral(call, callArgIndex(call, arg)) ) { | ||
| if ( auto v = derefBaseVar(arg) ) safeBase.insert(v); | ||
| } | ||
| } | ||
| // operator operands are the operator function's args 0/1; a comparison (==/!=) is an | ||
| // escape-neutral builtin, so a null-guard `p == null` does not leak p | ||
| virtual void preVisit ( ExprOp2 * expr ) override { | ||
| Visitor::preVisit(expr); | ||
| if ( isArgEscapeNeutral(expr, 0) ) { if ( auto v = derefBaseVar(expr->left) ) safeBase.insert(v); } | ||
| if ( isArgEscapeNeutral(expr, 1) ) { if ( auto v = derefBaseVar(expr->right) ) safeBase.insert(v); } | ||
| } | ||
| // a parameter captured into a closure / generator can leak through it - flag conservatively | ||
| virtual void preVisit ( ExprMakeBlock * expr ) override { | ||
| Visitor::preVisit(expr); | ||
| for ( auto & cap : expr->capture ) escapeByName(cap.name); | ||
| } | ||
| virtual void preVisit ( ExprMakeGenerator * expr ) override { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lambdas are also ExprMakeStruct via move semantics. |
||
| Visitor::preVisit(expr); | ||
| for ( auto & cap : expr->capture ) escapeByName(cap.name); | ||
| } | ||
| virtual ExpressionPtr visit ( ExprVar * expr ) override { | ||
| if ( expr->variable && safeBase.find(expr)==safeBase.end() | ||
| && isCandidateParam(expr->variable) ) { | ||
| escaped.insert(expr->variable); | ||
| } | ||
| return Visitor::visit(expr); | ||
| } | ||
| Function * fn; | ||
| das_set<Expression *> safeBase; | ||
| }; | ||
| TextWriter * logs = nullptr; | ||
| }; | ||
|
|
||
| // ===== Pass 1: escape analysis (classifies each candidate into the escape-kind result) ===== | ||
| class EscapeAnalysisVisitor : public Visitor { | ||
| public: | ||
|
|
@@ -177,8 +310,8 @@ namespace das { | |
| virtual ExpressionPtr visit ( ExprReturn * expr ) override { returnDepth--; return Visitor::visit(expr); } | ||
| virtual void preVisitCallArg ( ExprCall * call, Expression * arg, bool last ) override { | ||
| Visitor::preVisitCallArg(call, arg, last); argDepth++; | ||
| // a pointer passed directly to a pure, non-aliasing-return call can't escape through it | ||
| if ( isEscapeNeutralCall(call) ) { | ||
| // a pointer passed to an escape-neutral argument position can't escape through it | ||
| if ( isArgEscapeNeutral(call, callArgIndex(call, arg)) ) { | ||
| if ( auto v = derefBaseVar(arg) ) safeBase.insert(v); | ||
| } | ||
| } | ||
|
|
@@ -189,6 +322,12 @@ namespace das { | |
| Visitor::preVisit(expr); | ||
| if ( auto v = derefBaseVar(expr->value) ) safeBase.insert(v); | ||
| } | ||
| // a comparison (==/!=) is an escape-neutral builtin, so a null-guard `p == null` doesn't leak p | ||
| virtual void preVisit ( ExprOp2 * expr ) override { | ||
| Visitor::preVisit(expr); | ||
| if ( isArgEscapeNeutral(expr, 0) ) { if ( auto v = derefBaseVar(expr->left) ) safeBase.insert(v); } | ||
| if ( isArgEscapeNeutral(expr, 1) ) { if ( auto v = derefBaseVar(expr->right) ) safeBase.insert(v); } | ||
| } | ||
| virtual ExpressionPtr visit ( ExprVar * expr ) override { | ||
| // a use that is not a field-access base leaks the pointer value: classify how | ||
| if ( expr->variable && candidates.find(expr->variable)!=candidates.end() | ||
|
|
@@ -341,6 +480,10 @@ namespace das { | |
| auto forceStack = options.getBoolOption("force_allocate_on_stack", policies.force_allocate_on_stack); | ||
| if ( !options.getBoolOption("force_escape_free", policies.force_escape_free) && !forceStack ) return false; | ||
| auto logEscape = options.getBoolOption("log_escape_analysis", policies.log_escape_analysis); | ||
| // pass 0 first: interprocedural per-parameter escape, so pass 1 can free a local passed to a | ||
| // script function whose matching parameter provably does not escape | ||
| ParamEscapeAnalysis pe(logEscape ? &logs : nullptr); | ||
| pe.run(this); | ||
| EscapeAnalysisVisitor ev(logEscape ? &logs : nullptr); | ||
| visit(ev); | ||
| return ev.anyChanged; | ||
|
Comment on lines
+483
to
489
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ExprSafeField?