In previous post, we get the raw sil of our hello world example. In this post, we go on to get the canonical sil from that raw sil with SIL Guaranteed Optimization and Diagnostic Passes.
The same trick we used in previous post will guide us over the process of SIL Guaranteed Optimization.
Firstly, we set a break point at the entry of function CompilerInstance::performSILProcessing . Then the first statement if (performMandatorySILPasses(Invocation, silModule) && !Invocation.getFrontendOptions().AllowModuleWithCompilerErrors)
is the one to perform Guaranteed Optimization that turns raw sil in canonical sil. As showed in following.
(lldb) b CompilerInstance::performSILProcessing
Breakpoint 21: where = swift-frontend`swift::CompilerInstance::performSILProcessing(swift::SILModule*) + 28 at Frontend.cpp:1158:45, address = 0x0000000100369484
(lldb) r
Process 68621 launched: '/Users/k/wtsc/hello/bin/swift-frontend' (arm64)
Process 68621 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 21.1
frame #0: 0x0000000100369484 swift-frontend`swift::CompilerInstance::performSILProcessing(this=0x0000000117812200, silModule=0x0000000116d44950) at Frontend.cpp:1158:45
1155 }
1156
1157 bool CompilerInstance::performSILProcessing(SILModule *silModule) {
-> 1158 if (performMandatorySILPasses(Invocation, silModule) &&
1159 !Invocation.getFrontendOptions().AllowModuleWithCompilerErrors)
1160 return true;
1161
Target 0: (swift-frontend) stopped.
(lldb) expr silModule->dump(true);
sil_stage raw
...
(lldb) n
Process 68621 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step over
frame #0: 0x00000001003694bc swift-frontend`swift::CompilerInstance::performSILProcessing(this=0x0000000117812200, silModule=0x0000000116d44950) at Frontend.cpp:1163:32
1160 return true;
1161
1162 {
-> 1163 FrontendStatsTracer tracer(silModule->getASTContext().Stats,
1164 "SIL verification, pre-optimization");
1165 silModule->verify();
1166 }
Target 0: (swift-frontend) stopped.
(lldb) expr silModule->dump(true);
sil_stage canonical
...
After we pinpoints the function, performMandatorySILPasses(Invocation, silModule)
, let us dive into it to see what happen inside.
In this function, static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P)
, we can see what are the passes would be run.
static void addMandatoryDiagnosticOptPipeline(SILPassPipelinePlan &P) {
P.startPipeline("Mandatory Diagnostic Passes + Enabling Optimization Passes");
P.addSILGenCleanup();
P.addDiagnoseInvalidEscapingCaptures();
P.addDiagnoseStaticExclusivity();
P.addNestedSemanticFunctionCheck();
P.addCapturePromotion();
// Select access kind after capture promotion and before stack promotion.
// This guarantees that stack-promotable boxes have [static] enforcement.
P.addAccessEnforcementSelection();
P.addAllocBoxToStack();
P.addNoReturnFolding();
addDefiniteInitialization(P);
// Automatic differentiation: canonicalize all differentiability witnesses
// and `differentiable_function` instructions.
P.addDifferentiation();
// Only run semantic arc opts if we are optimizing and if mandatory semantic
// arc opts is explicitly enabled.
//
// NOTE: Eventually this pass will be split into a mandatory/more aggressive
// pass. This will happen when OSSA is no longer eliminated before the
// optimizer pipeline is run implying we can put a pass that requires OSSA
// there.
const auto &Options = P.getOptions();
P.addClosureLifetimeFixup();
#ifndef NDEBUG
// Add a verification pass to check our work when skipping
// function bodies.
if (Options.SkipFunctionBodies != FunctionBodySkipping::None)
P.addSILSkippingChecker();
#endif
if (Options.shouldOptimize()) {
P.addDestroyHoisting();
}
P.addMandatoryInlining();
P.addMandatorySILLinker();
// Promote loads as necessary to ensure we have enough SSA formation to emit
// SSA based diagnostics.
P.addPredictableMemoryAccessOptimizations();
// This phase performs optimizations necessary for correct interoperation of
// Swift os log APIs with C os_log ABIs.
// Pass dependencies: this pass depends on MandatoryInlining and Mandatory
// Linking happening before this pass and ConstantPropagation happening after
// this pass.
P.addOSLogOptimization();
// Diagnostic ConstantPropagation must be rerun on deserialized functions
// because it is sensitive to the assert configuration.
// Consequently, certain optimization passes beyond this point will also rerun.
P.addDiagnosticConstantPropagation();
// Now that we have emitted constant propagation diagnostics, try to eliminate
// dead allocations.
P.addPredictableDeadAllocationElimination();
P.addOptimizeHopToExecutor();
P.addDiagnoseUnreachable();
P.addDiagnoseInfiniteRecursion();
P.addYieldOnceCheck();
P.addEmitDFDiagnostics();
// Canonical swift requires all non cond_br critical edges to be split.
P.addSplitNonCondBrCriticalEdges();
}
After we got the pipeline plan, it will execute that pipeline plan to do the actual work.
evaluator::SideEffect ExecuteSILPipelineRequest::evaluate(
Evaluator &evaluator, SILPipelineExecutionDescriptor desc) const {
SILPassManager PM(desc.SM, desc.IsMandatory, desc.IRMod);
PM.executePassPipelinePlan(desc.Plan);
return std::make_tuple<>();
}
The SILPassManager just iterate over the passes and the contents of SIL Module, e.g. functions, basic blocks and so on, to perform transformations of passes on those contents.
void SILPassManager::execute() {
const SILOptions &Options = getOptions();
LLVM_DEBUG(llvm::dbgs() << "*** Optimizing the module (" << StageName
<< ") *** \n");
if (SILPrintAll) {
llvm::dbgs() << "*** SIL module before " << StageName << " ***\n";
printModule(Mod, Options.EmitVerboseSIL);
}
// Run the transforms by alternating between function transforms and
// module transforms. We'll queue up all the function transforms
// that we see in a row and then run the entire group of transforms
// on each function in turn. Then we move on to running the next set
// of consecutive module transforms.
unsigned Idx = 0, NumTransforms = Transformations.size();
while (Idx < NumTransforms && continueTransforming()) {
SILTransform *Tr = Transformations[Idx];
assert((isa<SILFunctionTransform>(Tr) || isa<SILModuleTransform>(Tr)) &&
"Unexpected pass kind!");
(void)Tr;
unsigned FirstFuncTrans = Idx;
while (Idx < NumTransforms && isa<SILFunctionTransform>(Transformations[Idx]))
++Idx;
runFunctionPasses(FirstFuncTrans, Idx);
while (Idx < NumTransforms && isa<SILModuleTransform>(Transformations[Idx])
&& continueTransforming()) {
runModulePass(Idx);
++Idx;
++NumPassesRun;
}
}
}
We can stop at a run of a pass to what happen in a transformation.
(lldb) b PassManager.cpp:463
Breakpoint 22: where = swift-frontend`swift::SILPassManager::runPassOnFunction(unsigned int, swift::SILFunction*) + 1168 at PassManager.cpp:463:3, address = 0x000000010122986c
(lldb) c
Process 68621 resuming
Process 68621 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 22.1
frame #0: 0x000000010122986c swift-frontend`swift::SILPassManager::runPassOnFunction(this=0x000000016fdf97a8, TransIdx=0, F=0x000000011788eaf8) at PassManager.cpp:463:3
460 SILForceVerifyAroundPass.end(), MatchFun)) {
461 forcePrecomputeAnalyses(F);
462 }
-> 463 SFT->run();
464 if (SILForceVerifyAll ||
465 SILForceVerifyAroundPass.end() !=
466 std::find_if(SILForceVerifyAroundPass.begin(),
Target 0: (swift-frontend) stopped.
(lldb) s
Process 68621 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = step in
frame #0: 0x00000001010d160c swift-frontend`(anonymous namespace)::ForEachLoopUnroller::run(this=0x0000000116d56150) at ForEachLoopUnroll.cpp:614:25
611 ~ForEachLoopUnroller() override {}
612
613 void run() override {
-> 614 SILFunction &fun = *getFunction();
615 bool changed = false;
616
617 if (!fun.hasOwnership())
Target 0: (swift-frontend) stopped.
It shows that we get into the ForEachLoopUnroller
tranformation. In side its run() method, we can know what this transformation does.
void run() override {
SILFunction &fun = *getFunction();
bool changed = false;
if (!fun.hasOwnership())
return;
InstructionDeleter deleter;
for (SILBasicBlock &bb : fun) {
for (auto instIter = bb.begin(); instIter != bb.end();) {
SILInstruction *inst = &*instIter;
ApplyInst *apply = dyn_cast<ApplyInst>(inst);
if (!apply) {
++instIter;
continue;
}
// Note that the following operation may delete a forEach call but
// would not delete this apply instruction, which is an array
// initializer. Therefore, the iterator should be valid here.
changed |= tryUnrollForEachCallsOverArrayLiteral(apply, deleter);
++instIter;
}
}
if (changed) {
deleter.cleanUpDeadInstructions();
PM->invalidateAnalysis(&fun,
SILAnalysis::InvalidationKind::FunctionBody);
}
}
That is the sketch of the process of SIL Guaranteed Optimizations. And Other SIL Optimizations use similar process of this one. Therefore, let the readers go over other optimizations. I will jump over the SIL Optimizations stage directly to the IR Gen stage, which lower the optimized SIL to LLVM IR.