Skip to content

Commit b17de11

Browse files
authored
Merge pull request #995 from hvitved/csharp/split-guards-performance
C#: Speedup guards predicates
2 parents b3d9350 + 4cbbe37 commit b17de11

File tree

6 files changed

+153
-51
lines changed

6 files changed

+153
-51
lines changed

csharp/ql/src/semmle/code/csharp/commons/Assertions.qll

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,41 +42,81 @@ class Assertion extends MethodCall {
4242
/** Gets the expression that this assertion pertains to. */
4343
Expr getExpr() { result = this.getArgumentForParameter(target.getAssertedParameter()) }
4444

45+
/**
46+
* Holds if basic block `succ` is immediately dominated by this assertion.
47+
* That is, `succ` can only be reached from the callable entry point by
48+
* going via *some* basic block `pred` containing this assertion, and `pred`
49+
* is an immediate predecessor of `succ`.
50+
*
51+
* Moreover, this assertion corresponds to multiple control flow nodes,
52+
* which is why
53+
*
54+
* ```
55+
* exists(BasicBlock bb |
56+
* bb.getANode() = this.getAControlFlowNode() |
57+
* bb.immediatelyDominates(succ)
58+
* )
59+
* ```
60+
*
61+
* does not work.
62+
*/
4563
pragma[nomagic]
46-
private JoinBlockPredecessor getAPossiblyDominatedPredecessor(JoinBlock jb) {
64+
private predicate immediatelyDominatesBlockSplit(BasicBlock succ) {
4765
// Only calculate dominance by explicit recursion for split nodes;
4866
// all other nodes can use regular CFG dominance
4967
this instanceof ControlFlow::Internal::SplitControlFlowElement and
50-
exists(BasicBlock bb | bb = this.getAControlFlowNode().getBasicBlock() |
51-
result = bb.getASuccessor*()
52-
) and
53-
result.getASuccessor() = jb and
54-
not jb.dominates(result)
68+
exists(BasicBlock bb | bb.getANode() = this.getAControlFlowNode() |
69+
succ = bb.getASuccessor() and
70+
forall(BasicBlock pred | pred = succ.getAPredecessor() and pred != bb |
71+
succ.dominates(pred)
72+
or
73+
// `pred` might be another split of this element
74+
pred.getANode().getElement() = this
75+
)
76+
)
5577
}
5678

57-
pragma[nomagic]
58-
private predicate isPossiblyDominatedJoinBlock(JoinBlock jb) {
59-
exists(this.getAPossiblyDominatedPredecessor(jb)) and
60-
forall(BasicBlock pred | pred = jb.getAPredecessor() |
61-
pred = this.getAPossiblyDominatedPredecessor(jb)
79+
pragma[noinline]
80+
private predicate strictlyDominatesJoinBlockPredecessor(JoinBlock jb, int i) {
81+
this.strictlyDominatesSplit(jb.getJoinBlockPredecessor(i))
82+
}
83+
84+
private predicate strictlyDominatesJoinBlockSplit(JoinBlock jb, int i) {
85+
i = -1 and
86+
this.strictlyDominatesJoinBlockPredecessor(jb, _)
87+
or
88+
this.strictlyDominatesJoinBlockSplit(jb, i - 1) and
89+
(
90+
this.strictlyDominatesJoinBlockPredecessor(jb, i)
6291
or
63-
jb.dominates(pred)
92+
this.getAControlFlowNode().getBasicBlock() = jb.getJoinBlockPredecessor(i)
6493
)
6594
}
6695

6796
pragma[nomagic]
6897
private predicate strictlyDominatesSplit(BasicBlock bb) {
69-
this.getAControlFlowNode().getBasicBlock().immediatelyDominates(bb)
98+
this.immediatelyDominatesBlockSplit(bb)
7099
or
71-
if bb instanceof JoinBlock
72-
then
73-
this.isPossiblyDominatedJoinBlock(bb) and
74-
forall(BasicBlock pred | pred = this.getAPossiblyDominatedPredecessor(bb) |
75-
this.strictlyDominatesSplit(pred)
76-
or
77-
this.getAControlFlowNode().getBasicBlock() = pred
78-
)
79-
else this.strictlyDominatesSplit(bb.getAPredecessor())
100+
// Equivalent with
101+
//
102+
// ```
103+
// exists(JoinBlockPredecessor pred | pred = bb.getAPredecessor() |
104+
// this.strictlyDominatesSplit(pred)
105+
// ) and
106+
// forall(JoinBlockPredecessor pred | pred = bb.getAPredecessor() |
107+
// this.strictlyDominatesSplit(pred)
108+
// or
109+
// this.getAControlFlowNode().getBasicBlock() = pred
110+
// )
111+
// ```
112+
//
113+
// but uses no universal recursion for better performance.
114+
exists(int last | last = max(int i | exists(bb.(JoinBlock).getJoinBlockPredecessor(i))) |
115+
this.strictlyDominatesJoinBlockSplit(bb, last)
116+
)
117+
or
118+
not bb instanceof JoinBlock and
119+
this.strictlyDominatesSplit(bb.getAPredecessor())
80120
}
81121

82122
/**

csharp/ql/src/semmle/code/csharp/controlflow/BasicBlocks.qll

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,47 @@ class ExitBasicBlock extends BasicBlock {
407407
/** Holds if `bb` is an exit basic block. */
408408
private predicate exitBB(BasicBlock bb) { bb.getLastNode() instanceof ControlFlow::Nodes::ExitNode }
409409

410+
private module JoinBlockPredecessors {
411+
private import ControlFlow::Nodes
412+
413+
private class CallableOrCFE extends Element {
414+
CallableOrCFE() { this instanceof Callable or this instanceof ControlFlowElement }
415+
}
416+
417+
private predicate id(CallableOrCFE x, CallableOrCFE y) { x = y }
418+
419+
private predicate idOf(CallableOrCFE x, int y) = equivalenceRelation(id/2)(x, y)
420+
421+
int getId(JoinBlockPredecessor jbp) {
422+
idOf(jbp.getFirstNode().(ElementNode).getElement(), result)
423+
or
424+
idOf(jbp.(EntryBasicBlock).getCallable(), result)
425+
}
426+
427+
string getSplitString(JoinBlockPredecessor jbp) {
428+
result = jbp.getFirstNode().(ElementNode).getSplitsString()
429+
or
430+
not exists(jbp.getFirstNode().(ElementNode).getSplitsString()) and
431+
result = ""
432+
}
433+
}
434+
410435
/** A basic block with more than one predecessor. */
411436
class JoinBlock extends BasicBlock {
412437
JoinBlock() { getFirstNode().isJoin() }
438+
439+
/**
440+
* Gets the `i`th predecessor of this join block, with respect to some
441+
* arbitrary order.
442+
*/
443+
cached
444+
JoinBlockPredecessor getJoinBlockPredecessor(int i) {
445+
result = rank[i + 1](JoinBlockPredecessor jbp |
446+
jbp = this.getAPredecessor()
447+
|
448+
jbp order by JoinBlockPredecessors::getId(jbp), JoinBlockPredecessors::getSplitString(jbp)
449+
)
450+
}
413451
}
414452

415453
/** A basic block that is an immediate predecessor of a join block. */

csharp/ql/src/semmle/code/csharp/controlflow/ControlFlowElement.qll

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -101,46 +101,56 @@ class ControlFlowElement extends ExprOrStmtParent, @control_flow_element {
101101
|
102102
succ.dominates(pred)
103103
or
104-
// `pred` might be another split of `cfe`
104+
// `pred` might be another split of this element
105105
pred.getLastNode().getElement() = this and
106-
pred.getASuccessorByType(t) = succ and
107106
t = s
108107
)
109108
)
110109
}
111110

112-
pragma[nomagic]
113-
private JoinBlockPredecessor getAPossiblyControlledPredecessor(
114-
JoinBlock controlled, ConditionalSuccessor s
115-
) {
116-
exists(BasicBlock mid | this.immediatelyControlsBlockSplit(mid, s) |
117-
result = mid.getASuccessor*()
118-
) and
119-
result.getASuccessor() = controlled and
120-
not controlled.dominates(result)
111+
pragma[noinline]
112+
private predicate controlsJoinBlockPredecessor(JoinBlock controlled, ConditionalSuccessor s, int i) {
113+
this.controlsBlockSplit(controlled.getJoinBlockPredecessor(i), s)
121114
}
122115

123-
pragma[nomagic]
124-
private predicate isPossiblyControlledJoinBlock(JoinBlock controlled, ConditionalSuccessor s) {
125-
exists(this.getAPossiblyControlledPredecessor(controlled, s)) and
126-
forall(BasicBlock pred | pred = controlled.getAPredecessor() |
127-
pred = this.getAPossiblyControlledPredecessor(controlled, s)
116+
private predicate controlsJoinBlockSplit(JoinBlock controlled, ConditionalSuccessor s, int i) {
117+
i = -1 and
118+
this.controlsJoinBlockPredecessor(controlled, s, _)
119+
or
120+
this.controlsJoinBlockSplit(controlled, s, i - 1) and
121+
(
122+
this.controlsJoinBlockPredecessor(controlled, s, i)
128123
or
129-
controlled.dominates(pred)
124+
controlled.dominates(controlled.getJoinBlockPredecessor(i))
130125
)
131126
}
132127

133128
cached
134129
private predicate controlsBlockSplit(BasicBlock controlled, ConditionalSuccessor s) {
135130
this.immediatelyControlsBlockSplit(controlled, s)
136131
or
137-
if controlled instanceof JoinBlock
138-
then
139-
this.isPossiblyControlledJoinBlock(controlled, s) and
140-
forall(BasicBlock pred | pred = this.getAPossiblyControlledPredecessor(controlled, s) |
141-
this.controlsBlock(pred, s)
142-
)
143-
else this.controlsBlockSplit(controlled.getAPredecessor(), s)
132+
// Equivalent with
133+
//
134+
// ```
135+
// exists(JoinBlockPredecessor pred | pred = controlled.getAPredecessor() |
136+
// this.controlsBlockSplit(pred, s)
137+
// ) and
138+
// forall(JoinBlockPredecessor pred | pred = controlled.getAPredecessor() |
139+
// this.controlsBlockSplit(pred, s)
140+
// or
141+
// controlled.dominates(pred)
142+
// )
143+
// ```
144+
//
145+
// but uses no universal recursion for better performance.
146+
exists(int last |
147+
last = max(int i | exists(controlled.(JoinBlock).getJoinBlockPredecessor(i)))
148+
|
149+
this.controlsJoinBlockSplit(controlled, s, last)
150+
)
151+
or
152+
not controlled instanceof JoinBlock and
153+
this.controlsBlockSplit(controlled.getAPredecessor(), s)
144154
}
145155

146156
/**

csharp/ql/src/semmle/code/csharp/controlflow/Guards.qll

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,18 +1271,22 @@ module Internal {
12711271
exists(ConditionOnExprComparisonConfig c | c.same(sub, guarded.getElement()))
12721272
}
12731273

1274+
pragma[noinline]
1275+
private predicate isGuardedByNode2(ControlFlow::Nodes::ElementNode guarded, Ssa::Definition def) {
1276+
isGuardedByNode1(guarded, _, _, _) and
1277+
exists(BasicBlock bb | bb = guarded.getBasicBlock() |
1278+
def = guarded.getElement().(AccessOrCallExpr).getAnSsaQualifier(bb.getANode())
1279+
)
1280+
}
1281+
12741282
cached
12751283
predicate isGuardedByNode(
12761284
ControlFlow::Nodes::ElementNode guarded, Guard g, AccessOrCallExpr sub, AbstractValue v
12771285
) {
12781286
isGuardedByNode1(guarded, g, sub, v) and
12791287
sub = g.getAChildExpr*() and
12801288
forall(Ssa::Definition def | def = sub.getAnSsaQualifier(_) |
1281-
exists(ControlFlow::Node cfn |
1282-
def = guarded.getElement().(AccessOrCallExpr).getAnSsaQualifier(cfn)
1283-
|
1284-
cfn.getBasicBlock() = guarded.getBasicBlock()
1285-
)
1289+
isGuardedByNode2(guarded, def)
12861290
)
12871291
}
12881292

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
| comments2.cs:118:5:118:21 | // ... | comments2.cs:119:11:119:25 | GenericClass<> | NestedType |
22
| comments2.cs:118:5:118:21 | // ... | comments2.cs:119:11:119:25 | GenericClass<> | UnboundGenericClass |
3+
| comments2.cs:124:5:124:16 | // ... | comments2.cs:125:9:125:20 | GenericFn | CallableOrCFE |
34
| comments2.cs:124:5:124:16 | // ... | comments2.cs:125:9:125:20 | GenericFn | InstanceCallable |
45
| comments2.cs:124:5:124:16 | // ... | comments2.cs:125:9:125:20 | GenericFn | UnboundGenericMethod |
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,33 @@
11
| VisualStudio.cs:9:11:9:21 | MyTestSuite | TestClass | LeafType |
22
| VisualStudio.cs:9:11:9:21 | MyTestSuite | TestClass | VSTestClass |
3+
| VisualStudio.cs:12:21:12:25 | Test1 | TestMethod | CallableOrCFE |
34
| VisualStudio.cs:12:21:12:25 | Test1 | TestMethod | InstanceCallable |
45
| VisualStudio.cs:12:21:12:25 | Test1 | TestMethod | VSTestMethod |
6+
| VisualStudio.cs:17:21:17:25 | Test2 | TestMethod | CallableOrCFE |
57
| VisualStudio.cs:17:21:17:25 | Test2 | TestMethod | InstanceCallable |
68
| VisualStudio.cs:17:21:17:25 | Test2 | TestMethod | VSTestMethod |
79
| XUnit.cs:22:11:22:21 | MyTestSuite | TestClass | LeafType |
810
| XUnit.cs:22:11:22:21 | MyTestSuite | TestClass | XUnitTestClass |
11+
| XUnit.cs:25:21:25:25 | Test1 | TestMethod | CallableOrCFE |
912
| XUnit.cs:25:21:25:25 | Test1 | TestMethod | InstanceCallable |
1013
| XUnit.cs:25:21:25:25 | Test1 | TestMethod | XUnitTestMethod |
14+
| XUnit.cs:30:21:30:25 | Test2 | TestMethod | CallableOrCFE |
1115
| XUnit.cs:30:21:30:25 | Test2 | TestMethod | InstanceCallable |
1216
| XUnit.cs:30:21:30:25 | Test2 | TestMethod | XUnitTestMethod |
1317
| nunit.cs:42:11:42:21 | MyTestSuite | TestClass | LeafType |
1418
| nunit.cs:42:11:42:21 | MyTestSuite | TestClass | NUnitFixture |
19+
| nunit.cs:52:21:52:25 | Test1 | TestMethod | CallableOrCFE |
1520
| nunit.cs:52:21:52:25 | Test1 | TestMethod | InstanceCallable |
1621
| nunit.cs:52:21:52:25 | Test1 | TestMethod | NUnitTestMethod |
22+
| nunit.cs:57:21:57:25 | Test2 | TestMethod | CallableOrCFE |
1723
| nunit.cs:57:21:57:25 | Test2 | TestMethod | InstanceCallable |
1824
| nunit.cs:57:21:57:25 | Test2 | TestMethod | NUnitTestMethod |
25+
| nunit.cs:62:21:62:25 | Test3 | TestMethod | CallableOrCFE |
1926
| nunit.cs:62:21:62:25 | Test3 | TestMethod | InstanceCallable |
2027
| nunit.cs:62:21:62:25 | Test3 | TestMethod | NUnitTestMethod |
28+
| nunit.cs:67:21:67:25 | Test4 | TestMethod | CallableOrCFE |
2129
| nunit.cs:67:21:67:25 | Test4 | TestMethod | InstanceCallable |
2230
| nunit.cs:67:21:67:25 | Test4 | TestMethod | NUnitTestMethod |
31+
| nunit.cs:72:21:72:25 | Test5 | TestMethod | CallableOrCFE |
2332
| nunit.cs:72:21:72:25 | Test5 | TestMethod | InstanceCallable |
2433
| nunit.cs:72:21:72:25 | Test5 | TestMethod | NUnitTestMethod |

0 commit comments

Comments
 (0)