1 //===-- CSPreInliner.cpp - Profile guided preinliner -------------- C++ -*-===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #include "CSPreInliner.h" 10 #include "ProfiledBinary.h" 11 #include "llvm/ADT/SCCIterator.h" 12 #include "llvm/ADT/Statistic.h" 13 #include "llvm/DebugInfo/Symbolize/SymbolizableModule.h" 14 #include <cstdint> 15 #include <queue> 16 17 #define DEBUG_TYPE "cs-preinliner" 18 19 using namespace llvm; 20 using namespace sampleprof; 21 22 STATISTIC(PreInlNumCSInlined, 23 "Number of functions inlined with context sensitive profile"); 24 STATISTIC(PreInlNumCSNotInlined, 25 "Number of functions not inlined with context sensitive profile"); 26 STATISTIC(PreInlNumCSInlinedHitMinLimit, 27 "Number of functions with FDO inline stopped due to min size limit"); 28 STATISTIC(PreInlNumCSInlinedHitMaxLimit, 29 "Number of functions with FDO inline stopped due to max size limit"); 30 STATISTIC( 31 PreInlNumCSInlinedHitGrowthLimit, 32 "Number of functions with FDO inline stopped due to growth size limit"); 33 34 // The switches specify inline thresholds used in SampleProfileLoader inlining. 35 // TODO: the actual threshold to be tuned here because the size here is based 36 // on machine code not LLVM IR. 37 extern cl::opt<int> SampleHotCallSiteThreshold; 38 extern cl::opt<int> SampleColdCallSiteThreshold; 39 extern cl::opt<int> ProfileInlineGrowthLimit; 40 extern cl::opt<int> ProfileInlineLimitMin; 41 extern cl::opt<int> ProfileInlineLimitMax; 42 extern cl::opt<bool> SortProfiledSCC; 43 44 cl::opt<bool> EnableCSPreInliner( 45 "csspgo-preinliner", cl::Hidden, cl::init(true), 46 cl::desc("Run a global pre-inliner to merge context profile based on " 47 "estimated global top-down inline decisions")); 48 49 cl::opt<bool> UseContextCostForPreInliner( 50 "use-context-cost-for-preinliner", cl::Hidden, cl::init(true), 51 cl::desc("Use context-sensitive byte size cost for preinliner decisions")); 52 53 static cl::opt<bool> SamplePreInlineReplay( 54 "csspgo-replay-preinline", cl::Hidden, cl::init(false), 55 cl::desc( 56 "Replay previous inlining and adjust context profile accordingly")); 57 58 CSPreInliner::CSPreInliner(SampleProfileMap &Profiles, ProfiledBinary &Binary, 59 ProfileSummary *Summary) 60 : UseContextCost(UseContextCostForPreInliner), 61 // TODO: Pass in a guid-to-name map in order for 62 // ContextTracker.getFuncNameFor to work, if `Profiles` can have md5 codes 63 // as their profile context. 64 ContextTracker(Profiles, nullptr), ProfileMap(Profiles), Binary(Binary), 65 Summary(Summary) { 66 // Set default preinliner hot/cold call site threshold tuned with CSSPGO. 67 // for good performance with reasonable profile size. 68 if (!SampleHotCallSiteThreshold.getNumOccurrences()) 69 SampleHotCallSiteThreshold = 1500; 70 if (!SampleColdCallSiteThreshold.getNumOccurrences()) 71 SampleColdCallSiteThreshold = 0; 72 if (!ProfileInlineLimitMax.getNumOccurrences()) 73 ProfileInlineLimitMax = 3000; 74 } 75 76 std::vector<StringRef> CSPreInliner::buildTopDownOrder() { 77 std::vector<StringRef> Order; 78 ProfiledCallGraph ProfiledCG(ContextTracker); 79 80 // Now that we have a profiled call graph, construct top-down order 81 // by building up SCC and reversing SCC order. 82 scc_iterator<ProfiledCallGraph *> I = scc_begin(&ProfiledCG); 83 while (!I.isAtEnd()) { 84 auto Range = *I; 85 if (SortProfiledSCC) { 86 // Sort nodes in one SCC based on callsite hotness. 87 scc_member_iterator<ProfiledCallGraph *> SI(*I); 88 Range = *SI; 89 } 90 for (auto *Node : Range) { 91 if (Node != ProfiledCG.getEntryNode()) 92 Order.push_back(Node->Name); 93 } 94 ++I; 95 } 96 std::reverse(Order.begin(), Order.end()); 97 98 return Order; 99 } 100 101 bool CSPreInliner::getInlineCandidates(ProfiledCandidateQueue &CQueue, 102 const FunctionSamples *CallerSamples) { 103 assert(CallerSamples && "Expect non-null caller samples"); 104 105 // Ideally we want to consider everything a function calls, but as far as 106 // context profile is concerned, only those frames that are children of 107 // current one in the trie is relavent. So we walk the trie instead of call 108 // targets from function profile. 109 ContextTrieNode *CallerNode = 110 ContextTracker.getContextFor(CallerSamples->getContext()); 111 112 bool HasNewCandidate = false; 113 for (auto &Child : CallerNode->getAllChildContext()) { 114 ContextTrieNode *CalleeNode = &Child.second; 115 FunctionSamples *CalleeSamples = CalleeNode->getFunctionSamples(); 116 if (!CalleeSamples) 117 continue; 118 119 // Call site count is more reliable, so we look up the corresponding call 120 // target profile in caller's context profile to retrieve call site count. 121 uint64_t CalleeEntryCount = CalleeSamples->getEntrySamples(); 122 uint64_t CallsiteCount = 0; 123 LineLocation Callsite = CalleeNode->getCallSiteLoc(); 124 if (auto CallTargets = CallerSamples->findCallTargetMapAt(Callsite)) { 125 SampleRecord::CallTargetMap &TargetCounts = CallTargets.get(); 126 auto It = TargetCounts.find(CalleeSamples->getName()); 127 if (It != TargetCounts.end()) 128 CallsiteCount = It->second; 129 } 130 131 // TODO: call site and callee entry count should be mostly consistent, add 132 // check for that. 133 HasNewCandidate = true; 134 uint32_t CalleeSize = getFuncSize(*CalleeSamples); 135 CQueue.emplace(CalleeSamples, std::max(CallsiteCount, CalleeEntryCount), 136 CalleeSize); 137 } 138 139 return HasNewCandidate; 140 } 141 142 uint32_t CSPreInliner::getFuncSize(const FunctionSamples &FSamples) { 143 if (UseContextCost) { 144 return Binary.getFuncSizeForContext(FSamples.getContext()); 145 } 146 147 return FSamples.getBodySamples().size(); 148 } 149 150 bool CSPreInliner::shouldInline(ProfiledInlineCandidate &Candidate) { 151 // If replay inline is requested, simply follow the inline decision of the 152 // profiled binary. 153 if (SamplePreInlineReplay) 154 return Candidate.CalleeSamples->getContext().hasAttribute( 155 ContextWasInlined); 156 157 unsigned int SampleThreshold = SampleColdCallSiteThreshold; 158 uint64_t ColdCountThreshold = ProfileSummaryBuilder::getColdCountThreshold( 159 (Summary->getDetailedSummary())); 160 161 if (Candidate.CallsiteCount <= ColdCountThreshold) 162 SampleThreshold = SampleColdCallSiteThreshold; 163 else { 164 // Linearly adjust threshold based on normalized hotness, i.e, a value in 165 // [0,1]. Use 10% cutoff instead of the max count as the normalization 166 // upperbound for stability. 167 double NormalizationUpperBound = 168 ProfileSummaryBuilder::getEntryForPercentile( 169 Summary->getDetailedSummary(), 100000 /* 10% */) 170 .MinCount; 171 double NormalizationLowerBound = ColdCountThreshold; 172 double NormalizedHotness = 173 (Candidate.CallsiteCount - NormalizationLowerBound) / 174 (NormalizationUpperBound - NormalizationLowerBound); 175 if (NormalizedHotness > 1.0) 176 NormalizedHotness = 1.0; 177 // Add 1 to to ensure hot callsites get a non-zero threshold, which could 178 // happen when SampleColdCallSiteThreshold is 0. This is when we do not 179 // want any inlining for cold callsites. 180 SampleThreshold = SampleHotCallSiteThreshold * NormalizedHotness * 100 + 181 SampleColdCallSiteThreshold + 1; 182 } 183 184 return (Candidate.SizeCost < SampleThreshold); 185 } 186 187 void CSPreInliner::processFunction(const StringRef Name) { 188 FunctionSamples *FSamples = ContextTracker.getBaseSamplesFor(Name); 189 if (!FSamples) 190 return; 191 192 unsigned FuncSize = getFuncSize(*FSamples); 193 unsigned FuncFinalSize = FuncSize; 194 unsigned SizeLimit = FuncSize * ProfileInlineGrowthLimit; 195 SizeLimit = std::min(SizeLimit, (unsigned)ProfileInlineLimitMax); 196 SizeLimit = std::max(SizeLimit, (unsigned)ProfileInlineLimitMin); 197 198 LLVM_DEBUG(dbgs() << "Process " << Name 199 << " for context-sensitive pre-inlining (pre-inline size: " 200 << FuncSize << ", size limit: " << SizeLimit << ")\n"); 201 202 ProfiledCandidateQueue CQueue; 203 getInlineCandidates(CQueue, FSamples); 204 205 while (!CQueue.empty() && FuncFinalSize < SizeLimit) { 206 ProfiledInlineCandidate Candidate = CQueue.top(); 207 CQueue.pop(); 208 bool ShouldInline = false; 209 if ((ShouldInline = shouldInline(Candidate))) { 210 // We mark context as inlined as the corresponding context profile 211 // won't be merged into that function's base profile. 212 ++PreInlNumCSInlined; 213 ContextTracker.markContextSamplesInlined(Candidate.CalleeSamples); 214 Candidate.CalleeSamples->getContext().setAttribute( 215 ContextShouldBeInlined); 216 FuncFinalSize += Candidate.SizeCost; 217 getInlineCandidates(CQueue, Candidate.CalleeSamples); 218 } else { 219 ++PreInlNumCSNotInlined; 220 } 221 LLVM_DEBUG(dbgs() << (ShouldInline ? " Inlined" : " Outlined") 222 << " context profile for: " 223 << Candidate.CalleeSamples->getContext().toString() 224 << " (callee size: " << Candidate.SizeCost 225 << ", call count:" << Candidate.CallsiteCount << ")\n"); 226 } 227 228 if (!CQueue.empty()) { 229 if (SizeLimit == (unsigned)ProfileInlineLimitMax) 230 ++PreInlNumCSInlinedHitMaxLimit; 231 else if (SizeLimit == (unsigned)ProfileInlineLimitMin) 232 ++PreInlNumCSInlinedHitMinLimit; 233 else 234 ++PreInlNumCSInlinedHitGrowthLimit; 235 } 236 237 LLVM_DEBUG({ 238 if (!CQueue.empty()) 239 dbgs() << " Inline candidates ignored due to size limit (inliner " 240 "original size: " 241 << FuncSize << ", inliner final size: " << FuncFinalSize 242 << ", size limit: " << SizeLimit << ")\n"; 243 244 while (!CQueue.empty()) { 245 ProfiledInlineCandidate Candidate = CQueue.top(); 246 CQueue.pop(); 247 bool WasInlined = 248 Candidate.CalleeSamples->getContext().hasAttribute(ContextWasInlined); 249 dbgs() << " " << Candidate.CalleeSamples->getContext().toString() 250 << " (candidate size:" << Candidate.SizeCost 251 << ", call count: " << Candidate.CallsiteCount << ", previously " 252 << (WasInlined ? "inlined)\n" : "not inlined)\n"); 253 } 254 }); 255 } 256 257 void CSPreInliner::run() { 258 #ifndef NDEBUG 259 auto printProfileNames = [](SampleProfileMap &Profiles, bool IsInput) { 260 dbgs() << (IsInput ? "Input" : "Output") << " context-sensitive profiles (" 261 << Profiles.size() << " total):\n"; 262 for (auto &It : Profiles) { 263 const FunctionSamples &Samples = It.second; 264 dbgs() << " [" << Samples.getContext().toString() << "] " 265 << Samples.getTotalSamples() << ":" << Samples.getHeadSamples() 266 << "\n"; 267 } 268 }; 269 #endif 270 271 LLVM_DEBUG(printProfileNames(ProfileMap, true)); 272 273 // Execute global pre-inliner to estimate a global top-down inline 274 // decision and merge profiles accordingly. This helps with profile 275 // merge for ThinLTO otherwise we won't be able to merge profiles back 276 // to base profile across module/thin-backend boundaries. 277 // It also helps better compress context profile to control profile 278 // size, as we now only need context profile for functions going to 279 // be inlined. 280 for (StringRef FuncName : buildTopDownOrder()) { 281 processFunction(FuncName); 282 } 283 284 // Not inlined context profiles are merged into its base, so we can 285 // trim out such profiles from the output. 286 std::vector<SampleContext> ProfilesToBeRemoved; 287 for (auto &It : ProfileMap) { 288 SampleContext &Context = It.second.getContext(); 289 if (!Context.isBaseContext() && !Context.hasState(InlinedContext)) { 290 assert(Context.hasState(MergedContext) && 291 "Not inlined context profile should be merged already"); 292 ProfilesToBeRemoved.push_back(It.first); 293 } 294 } 295 296 for (auto &ContextName : ProfilesToBeRemoved) { 297 ProfileMap.erase(ContextName); 298 } 299 300 // Make sure ProfileMap's key is consistent with FunctionSamples' name. 301 SampleContextTrimmer(ProfileMap).canonicalizeContextProfiles(); 302 303 FunctionSamples::ProfileIsPreInlined = true; 304 305 LLVM_DEBUG(printProfileNames(ProfileMap, false)); 306 } 307