Skip to content

Commit 9f8b155

Browse files
committed
PerformanceDiagnostics: a pass to print errors for performance violations in annotated functions.
The PerformanceDiagnostics pass issues performance diagnostics for functions which are annotated with performance annotations, like @_noLocks, @_noAllocation. This is done recursively for all functions which are called from performance-annotated functions. rdar://83882635
1 parent a5030a6 commit 9f8b155

File tree

6 files changed

+473
-0
lines changed

6 files changed

+473
-0
lines changed

include/swift/AST/DiagnosticsSIL.def

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,34 @@ WARNING(warn_dead_weak_store,none,
311311
"weak reference will always be nil because the referenced object is "
312312
"deallocated here", ())
313313

314+
// performance diagnostics
315+
ERROR(performance_annotations_not_enabled,none,
316+
"use -experimental-performance-annotations to enable performance annotations", ())
317+
ERROR(performance_dynamic_casting,none,
318+
"dynamic casting can lock or allocate", ())
319+
ERROR(performance_metadata,none,
320+
"%0 can cause metadata allocation or locks", (StringRef))
321+
ERROR(performance_metadata_type,none,
322+
"Using type %0 can cause metadata allocation or locks", (Type))
323+
ERROR(performance_allocating,none,
324+
"%0 can cause an allocation", (StringRef))
325+
ERROR(performance_deallocating,none,
326+
"%0 can cause an deallocation", (StringRef))
327+
ERROR(performance_deallocating_type,none,
328+
"%0 a value of type %1 can cause a deallocation", (StringRef, Type))
329+
ERROR(performance_locking,none,
330+
"%0 can cause locking", (StringRef))
331+
ERROR(performance_arc,none,
332+
"this code performs reference counting operations which can cause locking", ())
333+
ERROR(performance_objectivec,none,
334+
"calls of Objective-C methods can have unpredictable performance", ())
335+
ERROR(performance_unknown_callees,none,
336+
"called function is not known at compile time and can have unpredictable performance", ())
337+
ERROR(performance_callee_unavailable,none,
338+
"called function is not availbale in this module and can have unpredictable performance", ())
339+
NOTE(performance_called_from,none,
340+
"called from here", ())
341+
314342
// 'transparent' diagnostics
315343
ERROR(circular_transparent,none,
316344
"inlining 'transparent' functions forms circular loop", ())

include/swift/SILOptimizer/PassManager/Passes.def

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ PASS(PerfInliner, "inline",
325325
"Performance Function Inlining")
326326
PASS(PerformanceConstantPropagation, "performance-constant-propagation",
327327
"Constant Propagation for Performance without Diagnostics")
328+
PASS(PerformanceDiagnostics, "performance-diagnostics",
329+
"Constant Propagation for Performance without Diagnostics")
328330
PASS(PredictableMemoryAccessOptimizations, "predictable-memaccess-opts",
329331
"Predictable Memory Access Optimizations for Diagnostics")
330332
PASS(PredictableDeadAllocationElimination, "predictable-deadalloc-elim",

lib/SILOptimizer/Mandatory/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ target_sources(swiftSILOptimizer PRIVATE
1919
MandatoryInlining.cpp
2020
NestedSemanticFunctionCheck.cpp
2121
OptimizeHopToExecutor.cpp
22+
PerformanceDiagnostics.cpp
2223
PredictableMemOpt.cpp
2324
PMOMemoryUseCollector.cpp
2425
RawSILInstLowering.cpp
Lines changed: 330 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
//==-------- PerformanceDiagnostics.cpp - Diagnose performance issues ------==//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
#define DEBUG_TYPE "performance-diagnostics"
14+
#include "swift/AST/DiagnosticsSIL.h"
15+
#include "swift/AST/SemanticAttrs.h"
16+
#include "swift/SIL/BasicBlockDatastructures.h"
17+
#include "swift/SIL/InstructionUtils.h"
18+
#include "swift/SIL/ApplySite.h"
19+
#include "swift/SILOptimizer/Analysis/ArraySemantic.h"
20+
#include "swift/SILOptimizer/Analysis/BasicCalleeAnalysis.h"
21+
#include "swift/SILOptimizer/PassManager/Transforms.h"
22+
#include "swift/SILOptimizer/Utils/BasicBlockOptUtils.h"
23+
#include "llvm/Support/Debug.h"
24+
25+
using namespace swift;
26+
27+
namespace {
28+
29+
/// Issues performance diagnostics for functions which are annotated with
30+
/// performance annotations, like @_noLocks, @_noAllocation.
31+
///
32+
/// This is done recursively for all functions which are called from
33+
/// performance-annotated functions.
34+
class PerformanceDiagnostics {
35+
36+
/// A source location with a link to the location from the call-site.
37+
/// Used to print the whole back trace of a location.
38+
struct LocWithParent {
39+
SourceLoc loc;
40+
41+
/// Null if this is the top-leve location.
42+
LocWithParent *parent;
43+
44+
LocWithParent(SourceLoc loc, LocWithParent *parent) :
45+
loc(loc), parent(parent) {}
46+
};
47+
48+
SILModule &module;
49+
BasicCalleeAnalysis *bca;
50+
llvm::DenseMap<SILFunction *, PerformanceConstraints> visitedFuncs;
51+
52+
public:
53+
PerformanceDiagnostics(SILModule &module, BasicCalleeAnalysis *bca) :
54+
module(module), bca(bca) {}
55+
56+
bool visitFunction(SILFunction *function, PerformanceConstraints perfConstr) {
57+
return visitFunction(function, perfConstr, /*parentLoc*/ nullptr);
58+
}
59+
60+
private:
61+
bool visitFunction(SILFunction *function, PerformanceConstraints perfConstr,
62+
LocWithParent *parentLoc);
63+
64+
bool visitInst(SILInstruction *inst, PerformanceConstraints perfConstr,
65+
LocWithParent *parentLoc);
66+
67+
bool visitCallee(FullApplySite as, PerformanceConstraints perfConstr,
68+
LocWithParent *parentLoc);
69+
70+
template<typename ...ArgTypes>
71+
void diagnose(LocWithParent loc, Diag<ArgTypes...> ID,
72+
typename detail::PassArgument<ArgTypes>::type... Args) {
73+
diagnose(loc, Diagnostic(ID, std::move(Args)...));
74+
}
75+
76+
void diagnose(LocWithParent loc, Diagnostic &&D);
77+
};
78+
79+
static bool isEffectFreeArraySemanticCall(SILInstruction *inst) {
80+
ArraySemanticsCall semCall(inst);
81+
switch (semCall.getKind()) {
82+
case ArrayCallKind::kGetElement:
83+
return cast<ApplyInst>(inst)->getType().isTrivial(*inst->getFunction());
84+
default:
85+
return false;
86+
}
87+
}
88+
89+
/// Prints performance diagnostics for \p function.
90+
bool PerformanceDiagnostics::visitFunction(SILFunction *function,
91+
PerformanceConstraints perfConstr,
92+
LocWithParent *parentLoc) {
93+
ReachingReturnBlocks rrBlocks(function);
94+
NonErrorHandlingBlocks neBlocks(function);
95+
96+
for (SILBasicBlock &block : *function) {
97+
if (!rrBlocks.reachesReturn(&block) || !neBlocks.isNonErrorHandling(&block))
98+
continue;
99+
for (SILInstruction &inst : block) {
100+
if (visitInst(&inst, perfConstr, parentLoc))
101+
return true;
102+
103+
if (auto as = FullApplySite::isa(&inst)) {
104+
if (isEffectFreeArraySemanticCall(&inst))
105+
continue;
106+
107+
// Recursively walk into the callees.
108+
if (visitCallee(as, perfConstr, parentLoc))
109+
return true;
110+
}
111+
}
112+
}
113+
return false;
114+
}
115+
116+
bool PerformanceDiagnostics::visitCallee(FullApplySite as,
117+
PerformanceConstraints perfConstr,
118+
LocWithParent *parentLoc) {
119+
CalleeList callees = bca->getCalleeList(as);
120+
LocWithParent asLoc(as.getLoc().getSourceLoc(), parentLoc);
121+
LocWithParent *loc = &asLoc;
122+
if (parentLoc && asLoc.loc == as.getFunction()->getLocation().getSourceLoc())
123+
loc = parentLoc;
124+
125+
if (callees.isIncomplete()) {
126+
diagnose(*loc, diag::performance_unknown_callees);
127+
return true;
128+
}
129+
for (SILFunction *callee : callees) {
130+
if (callee->hasSemanticsAttr(semantics::NO_PERFORMANCE_ANALYSIS))
131+
continue;
132+
133+
// If the callee has a defined performance constraint which is at least as
134+
// strong as the required constraint, we are done.
135+
PerformanceConstraints calleeConstr = callee->getPerfConstraints();
136+
if (calleeConstr >= perfConstr)
137+
return false;
138+
139+
if (!callee->isDefinition()) {
140+
diagnose(*loc, diag::performance_callee_unavailable);
141+
return true;
142+
}
143+
144+
// Check if we already visited the callee while checking a constraint which
145+
// is at least as strong as the required constraint.
146+
PerformanceConstraints &computedConstr = visitedFuncs[callee];
147+
if (computedConstr >= perfConstr)
148+
return false;
149+
computedConstr = perfConstr;
150+
151+
if (visitFunction(callee, perfConstr, loc))
152+
return true;
153+
}
154+
return false;
155+
}
156+
157+
bool PerformanceDiagnostics::visitInst(SILInstruction *inst,
158+
PerformanceConstraints perfConstr,
159+
LocWithParent *parentLoc) {
160+
SILType impactType;
161+
RuntimeEffect impact = getRuntimeEffect(inst, impactType);
162+
LocWithParent loc(inst->getLoc().getSourceLoc(), parentLoc);
163+
164+
if (impact & RuntimeEffect::Casting) {
165+
// TODO: be more specific on casting.
166+
// E.g. distinguish locking and allocating dynamic casts, etc.
167+
diagnose(loc, diag::performance_dynamic_casting);
168+
return true;
169+
}
170+
if (impact & RuntimeEffect::MetaData) {
171+
// TODO: be more specific on metadata.
172+
// E.g. distinguish locking and allocating metadata operations, etc.
173+
174+
// Try to give a good error message by looking which type of code it is.
175+
switch (inst->getKind()) {
176+
case SILInstructionKind::KeyPathInst:
177+
diagnose(loc, diag::performance_metadata, "using KeyPath");
178+
break;
179+
case SILInstructionKind::AllocGlobalInst:
180+
case SILInstructionKind::GlobalValueInst:
181+
diagnose(loc, diag::performance_metadata, "global or static variables");
182+
break;
183+
case SILInstructionKind::PartialApplyInst: {
184+
diagnose(loc, diag::performance_metadata,
185+
"generic closures or local functions");
186+
break;
187+
}
188+
case SILInstructionKind::ApplyInst:
189+
case SILInstructionKind::TryApplyInst:
190+
case SILInstructionKind::BeginApplyInst: {
191+
diagnose(loc, diag::performance_metadata, "generic function calls");
192+
break;
193+
}
194+
default:
195+
// We didn't recognize the instruction, so try to give an error message
196+
// based on the involved type.
197+
if (impactType) {
198+
diagnose(loc, diag::performance_metadata_type, impactType.getASTType());
199+
break;
200+
}
201+
// The default error message.
202+
diagnose(loc, diag::performance_metadata, "this code pattern");
203+
break;
204+
}
205+
return true;
206+
}
207+
if (impact & RuntimeEffect::Allocating) {
208+
switch (inst->getKind()) {
209+
case SILInstructionKind::BeginApplyInst:
210+
// Not all begin_applys necessarily allocate. But it's difficult to
211+
// estimate the effect on SIL level.
212+
diagnose(loc, diag::performance_allocating, "co-routine calls");
213+
break;
214+
case SILInstructionKind::PartialApplyInst:
215+
diagnose(loc, diag::performance_allocating, "closure captures");
216+
break;
217+
case SILInstructionKind::AllocRefInst:
218+
case SILInstructionKind::AllocRefDynamicInst:
219+
diagnose(loc, diag::performance_allocating, "class instance construction");
220+
break;
221+
default:
222+
diagnose(loc, diag::performance_allocating, "this code pattern");
223+
break;
224+
}
225+
return true;
226+
}
227+
if (impact & RuntimeEffect::Deallocating) {
228+
if (impactType) {
229+
switch (inst->getKind()) {
230+
case SILInstructionKind::StoreInst:
231+
case SILInstructionKind::CopyAddrInst:
232+
diagnose(loc, diag::performance_deallocating_type,
233+
"storing", impactType.getASTType());
234+
return true;
235+
case SILInstructionKind::DestroyAddrInst:
236+
case SILInstructionKind::DestroyValueInst:
237+
diagnose(loc, diag::performance_deallocating_type,
238+
"ending the lifetime of", impactType.getASTType());
239+
return true;
240+
default:
241+
break;
242+
}
243+
}
244+
diagnose(loc, diag::performance_deallocating, "this code pattern");
245+
return true;
246+
}
247+
if (impact & RuntimeEffect::ObjectiveC) {
248+
diagnose(loc, diag::performance_objectivec);
249+
return true;
250+
}
251+
252+
if (perfConstr == PerformanceConstraints::NoAllocation)
253+
return false;
254+
255+
// Handle locking-only effects.
256+
257+
if (impact & RuntimeEffect::Locking) {
258+
if (inst->getFunction()->isGlobalInit()) {
259+
diagnose(loc, diag::performance_locking,
260+
"global/static variable initialization");
261+
} else {
262+
diagnose(loc, diag::performance_locking, "this code pattern");
263+
}
264+
return true;
265+
}
266+
if (impact & RuntimeEffect::RefCounting) {
267+
diagnose(loc, diag::performance_arc);
268+
return true;
269+
}
270+
return false;
271+
}
272+
273+
void PerformanceDiagnostics::diagnose(LocWithParent loc, Diagnostic &&D) {
274+
// Start with a valid location in the call tree.
275+
LocWithParent *validLoc = &loc;
276+
while (!validLoc->loc.isValid() && validLoc->parent) {
277+
validLoc = validLoc->parent;
278+
}
279+
module.getASTContext().Diags.diagnose(validLoc->loc, D);
280+
281+
// Print the whole back trace. Otherwise it would be difficult for the user
282+
// to know which annotated function caused the error.
283+
LocWithParent *parentLoc = validLoc->parent;
284+
while (parentLoc) {
285+
if (parentLoc->loc.isValid()) {
286+
module.getASTContext().Diags.diagnose(parentLoc->loc,
287+
diag::performance_called_from);
288+
}
289+
parentLoc = parentLoc->parent;
290+
}
291+
}
292+
293+
//===----------------------------------------------------------------------===//
294+
// The function pass
295+
//===----------------------------------------------------------------------===//
296+
297+
class PerformanceDiagnosticsPass : public SILModuleTransform {
298+
public:
299+
PerformanceDiagnosticsPass() {}
300+
301+
private:
302+
void run() override {
303+
SILModule *module = getModule();
304+
305+
PerformanceDiagnostics diagnoser(*module, getAnalysis<BasicCalleeAnalysis>());
306+
307+
for (SILFunction &function : *module) {
308+
// Don't rerun diagnostics on deserialized functions.
309+
if (function.wasDeserializedCanonical())
310+
continue;
311+
312+
if (function.getPerfConstraints() != PerformanceConstraints::None) {
313+
if (!module->getOptions().EnablePerformanceAnnotations) {
314+
module->getASTContext().Diags.diagnose(
315+
function.getLocation().getSourceLoc(),
316+
diag::performance_annotations_not_enabled);
317+
return;
318+
}
319+
320+
diagnoser.visitFunction(&function, function.getPerfConstraints());
321+
}
322+
}
323+
}
324+
};
325+
326+
} // end anonymous namespace
327+
328+
SILTransform *swift::createPerformanceDiagnostics() {
329+
return new PerformanceDiagnosticsPass();
330+
}

lib/SILOptimizer/PassManager/PassPipeline.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P) {
177177
if (P.getOptions().EnableCopyPropagation) {
178178
P.addDiagnoseLifetimeIssues();
179179
}
180+
181+
P.addPerformanceDiagnostics();
182+
180183
// Canonical swift requires all non cond_br critical edges to be split.
181184
P.addSplitNonCondBrCriticalEdges();
182185
}

0 commit comments

Comments
 (0)