1 | //===- bolt/Passes/RegReAssign.cpp ----------------------------------------===// |
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 | // This file implements the RegReAssign class. |
10 | // |
11 | //===----------------------------------------------------------------------===// |
12 | |
13 | #include "bolt/Passes/RegReAssign.h" |
14 | #include "bolt/Core/MCPlus.h" |
15 | #include "bolt/Passes/BinaryFunctionCallGraph.h" |
16 | #include "bolt/Passes/DataflowAnalysis.h" |
17 | #include "bolt/Passes/DataflowInfoManager.h" |
18 | #include "bolt/Utils/Utils.h" |
19 | #include <numeric> |
20 | |
21 | #define DEBUG_TYPE "regreassign" |
22 | |
23 | using namespace llvm; |
24 | |
25 | namespace opts { |
26 | extern cl::OptionCategory BoltOptCategory; |
27 | extern cl::opt<bool> UpdateDebugSections; |
28 | |
29 | static cl::opt<bool> AggressiveReAssign( |
30 | "use-aggr-reg-reassign" , |
31 | cl::desc("use register liveness analysis to try to find more opportunities " |
32 | "for -reg-reassign optimization" ), |
33 | cl::cat(BoltOptCategory)); |
34 | } |
35 | |
36 | namespace llvm { |
37 | namespace bolt { |
38 | |
39 | void RegReAssign::swap(BinaryFunction &Function, MCPhysReg A, MCPhysReg B) { |
40 | BinaryContext &BC = Function.getBinaryContext(); |
41 | const BitVector &AliasA = BC.MIB->getAliases(Reg: A, OnlySmaller: false); |
42 | const BitVector &AliasB = BC.MIB->getAliases(Reg: B, OnlySmaller: false); |
43 | |
44 | // Regular instructions |
45 | for (BinaryBasicBlock &BB : Function) { |
46 | for (MCInst &Inst : BB) { |
47 | for (MCOperand &Operand : MCPlus::primeOperands(Inst)) { |
48 | if (!Operand.isReg()) |
49 | continue; |
50 | |
51 | unsigned Reg = Operand.getReg(); |
52 | if (AliasA.test(Idx: Reg)) { |
53 | Operand.setReg(BC.MIB->getAliasSized(Reg: B, Size: BC.MIB->getRegSize(Reg))); |
54 | --StaticBytesSaved; |
55 | DynBytesSaved -= BB.getKnownExecutionCount(); |
56 | continue; |
57 | } |
58 | if (!AliasB.test(Idx: Reg)) |
59 | continue; |
60 | Operand.setReg(BC.MIB->getAliasSized(Reg: A, Size: BC.MIB->getRegSize(Reg))); |
61 | ++StaticBytesSaved; |
62 | DynBytesSaved += BB.getKnownExecutionCount(); |
63 | } |
64 | } |
65 | } |
66 | |
67 | // CFI |
68 | DenseSet<const MCCFIInstruction *> Changed; |
69 | for (BinaryBasicBlock &BB : Function) { |
70 | for (MCInst &Inst : BB) { |
71 | if (!BC.MIB->isCFI(Inst)) |
72 | continue; |
73 | const MCCFIInstruction *CFI = Function.getCFIFor(Instr: Inst); |
74 | if (Changed.count(V: CFI)) |
75 | continue; |
76 | Changed.insert(V: CFI); |
77 | |
78 | switch (CFI->getOperation()) { |
79 | case MCCFIInstruction::OpRegister: { |
80 | const unsigned CFIReg2 = CFI->getRegister2(); |
81 | const MCPhysReg Reg2 = *BC.MRI->getLLVMRegNum(RegNum: CFIReg2, /*isEH=*/false); |
82 | if (AliasA.test(Idx: Reg2)) { |
83 | Function.setCFIFor( |
84 | Instr: Inst, CFIInst: MCCFIInstruction::createRegister( |
85 | L: nullptr, Register1: CFI->getRegister(), |
86 | Register2: BC.MRI->getDwarfRegNum( |
87 | RegNum: BC.MIB->getAliasSized(Reg: B, Size: BC.MIB->getRegSize(Reg: Reg2)), |
88 | isEH: false))); |
89 | } else if (AliasB.test(Idx: Reg2)) { |
90 | Function.setCFIFor( |
91 | Instr: Inst, CFIInst: MCCFIInstruction::createRegister( |
92 | L: nullptr, Register1: CFI->getRegister(), |
93 | Register2: BC.MRI->getDwarfRegNum( |
94 | RegNum: BC.MIB->getAliasSized(Reg: A, Size: BC.MIB->getRegSize(Reg: Reg2)), |
95 | isEH: false))); |
96 | } |
97 | } |
98 | [[fallthrough]]; |
99 | case MCCFIInstruction::OpUndefined: |
100 | case MCCFIInstruction::OpDefCfa: |
101 | case MCCFIInstruction::OpOffset: |
102 | case MCCFIInstruction::OpRestore: |
103 | case MCCFIInstruction::OpSameValue: |
104 | case MCCFIInstruction::OpDefCfaRegister: |
105 | case MCCFIInstruction::OpRelOffset: |
106 | case MCCFIInstruction::OpEscape: { |
107 | unsigned CFIReg; |
108 | if (CFI->getOperation() != MCCFIInstruction::OpEscape) { |
109 | CFIReg = CFI->getRegister(); |
110 | } else { |
111 | std::optional<uint8_t> Reg = |
112 | readDWARFExpressionTargetReg(ExprBytes: CFI->getValues()); |
113 | // Handle DW_CFA_def_cfa_expression |
114 | if (!Reg) |
115 | break; |
116 | CFIReg = *Reg; |
117 | } |
118 | const MCPhysReg Reg = *BC.MRI->getLLVMRegNum(RegNum: CFIReg, /*isEH=*/false); |
119 | if (AliasA.test(Idx: Reg)) |
120 | Function.mutateCFIRegisterFor( |
121 | Instr: Inst, |
122 | NewReg: BC.MRI->getDwarfRegNum( |
123 | RegNum: BC.MIB->getAliasSized(Reg: B, Size: BC.MIB->getRegSize(Reg)), isEH: false)); |
124 | else if (AliasB.test(Idx: Reg)) |
125 | Function.mutateCFIRegisterFor( |
126 | Instr: Inst, |
127 | NewReg: BC.MRI->getDwarfRegNum( |
128 | RegNum: BC.MIB->getAliasSized(Reg: A, Size: BC.MIB->getRegSize(Reg)), isEH: false)); |
129 | break; |
130 | } |
131 | default: |
132 | break; |
133 | } |
134 | } |
135 | } |
136 | } |
137 | |
138 | void RegReAssign::rankRegisters(BinaryFunction &Function) { |
139 | BinaryContext &BC = Function.getBinaryContext(); |
140 | std::fill(first: RegScore.begin(), last: RegScore.end(), value: 0); |
141 | std::fill(first: RankedRegs.begin(), last: RankedRegs.end(), value: 0); |
142 | |
143 | auto countRegScore = [&](BinaryBasicBlock &BB) { |
144 | for (MCInst &Inst : BB) { |
145 | const bool CannotUseREX = BC.MIB->cannotUseREX(Inst); |
146 | const MCInstrDesc &Desc = BC.MII->get(Opcode: Inst.getOpcode()); |
147 | |
148 | // Disallow substituitions involving regs in implicit uses lists |
149 | for (MCPhysReg ImplicitUse : Desc.implicit_uses()) { |
150 | const size_t RegEC = |
151 | BC.MIB->getAliases(Reg: ImplicitUse, OnlySmaller: false).find_first(); |
152 | RegScore[RegEC] = |
153 | std::numeric_limits<decltype(RegScore)::value_type>::min(); |
154 | } |
155 | |
156 | // Disallow substituitions involving regs in implicit defs lists |
157 | for (MCPhysReg ImplicitDef : Desc.implicit_defs()) { |
158 | const size_t RegEC = |
159 | BC.MIB->getAliases(Reg: ImplicitDef, OnlySmaller: false).find_first(); |
160 | RegScore[RegEC] = |
161 | std::numeric_limits<decltype(RegScore)::value_type>::min(); |
162 | } |
163 | |
164 | for (int I = 0, E = MCPlus::getNumPrimeOperands(Inst); I != E; ++I) { |
165 | const MCOperand &Operand = Inst.getOperand(i: I); |
166 | if (!Operand.isReg()) |
167 | continue; |
168 | |
169 | if (Desc.getOperandConstraint(OpNum: I, Constraint: MCOI::TIED_TO) != -1) |
170 | continue; |
171 | |
172 | unsigned Reg = Operand.getReg(); |
173 | size_t RegEC = BC.MIB->getAliases(Reg, OnlySmaller: false).find_first(); |
174 | if (RegEC == 0) |
175 | continue; |
176 | |
177 | // Disallow substituitions involving regs in instrs that cannot use REX |
178 | // The relationship of X86 registers is shown in the diagram. BL and BH |
179 | // do not have a direct alias relationship. However, if the BH register |
180 | // cannot be swapped, then the BX/EBX/RBX registers cannot be swapped as |
181 | // well, which means that BL register also cannot be swapped. Therefore, |
182 | // in the presence of BX/EBX/RBX registers, BL and BH have an alias |
183 | // relationship. |
184 | // ┌─────────────────┐ |
185 | // │ RBX │ |
186 | // ├─────┬───────────┤ |
187 | // │ │ EBX │ |
188 | // ├─────┴──┬────────┤ |
189 | // │ │ BX │ |
190 | // ├────────┼───┬────┤ |
191 | // │ │BH │BL │ |
192 | // └────────┴───┴────┘ |
193 | if (CannotUseREX) { |
194 | RegScore[RegEC] = |
195 | std::numeric_limits<decltype(RegScore)::value_type>::min(); |
196 | RegScore[BC.MIB->getAliasSized(Reg, Size: 1)] = RegScore[RegEC]; |
197 | continue; |
198 | } |
199 | |
200 | // Unsupported substitution, cannot swap BH with R* regs, bail |
201 | if (BC.MIB->isUpper8BitReg(Reg) && ClassicCSR.test(Idx: Reg)) { |
202 | RegScore[RegEC] = |
203 | std::numeric_limits<decltype(RegScore)::value_type>::min(); |
204 | RegScore[BC.MIB->getAliasSized(Reg, Size: 1)] = RegScore[RegEC]; |
205 | continue; |
206 | } |
207 | |
208 | RegScore[RegEC] += BB.getKnownExecutionCount(); |
209 | } |
210 | } |
211 | }; |
212 | for (BinaryBasicBlock &BB : Function) |
213 | countRegScore(BB); |
214 | |
215 | for (BinaryFunction *ChildFrag : Function.getFragments()) { |
216 | for (BinaryBasicBlock &BB : *ChildFrag) |
217 | countRegScore(BB); |
218 | } |
219 | |
220 | std::iota(first: RankedRegs.begin(), last: RankedRegs.end(), value: 0); // 0, 1, 2, 3... |
221 | llvm::sort(C&: RankedRegs, |
222 | Comp: [&](size_t A, size_t B) { return RegScore[A] > RegScore[B]; }); |
223 | |
224 | LLVM_DEBUG({ |
225 | for (size_t Reg : RankedRegs) { |
226 | if (RegScore[Reg] == 0) |
227 | continue; |
228 | dbgs() << Reg << " " ; |
229 | if (RegScore[Reg] > 0) |
230 | dbgs() << BC.MRI->getName(Reg) << ": " << RegScore[Reg] << "\n" ; |
231 | else |
232 | dbgs() << BC.MRI->getName(Reg) << ": (blacklisted)\n" ; |
233 | } |
234 | }); |
235 | } |
236 | |
237 | void RegReAssign::aggressivePassOverFunction(BinaryFunction &Function) { |
238 | BinaryContext &BC = Function.getBinaryContext(); |
239 | rankRegisters(Function); |
240 | |
241 | // If there is a situation where function: |
242 | // A() -> A.cold() |
243 | // A.localalias() -> A.cold() |
244 | // simply swapping these two calls can cause issues. |
245 | for (BinaryFunction *ChildFrag : Function.getFragments()) { |
246 | if (ChildFrag->getParentFragments()->size() > 1) |
247 | return; |
248 | if (ChildFrag->empty()) |
249 | return; |
250 | } |
251 | |
252 | // Bail early if our registers are all black listed, before running expensive |
253 | // analysis passes |
254 | bool Bail = true; |
255 | int64_t LowScoreClassic = std::numeric_limits<int64_t>::max(); |
256 | for (int J : ClassicRegs.set_bits()) { |
257 | if (RegScore[J] <= 0) |
258 | continue; |
259 | Bail = false; |
260 | if (RegScore[J] < LowScoreClassic) |
261 | LowScoreClassic = RegScore[J]; |
262 | } |
263 | if (Bail) |
264 | return; |
265 | BitVector Extended = ClassicRegs; |
266 | Extended.flip(); |
267 | Extended &= GPRegs; |
268 | Bail = true; |
269 | int64_t HighScoreExtended = 0; |
270 | for (int J : Extended.set_bits()) { |
271 | if (RegScore[J] <= 0) |
272 | continue; |
273 | Bail = false; |
274 | if (RegScore[J] > HighScoreExtended) |
275 | HighScoreExtended = RegScore[J]; |
276 | } |
277 | // Also bail early if there is no profitable substitution even if we assume |
278 | // all registers can be exchanged |
279 | if (Bail || (LowScoreClassic << 1) >= HighScoreExtended) |
280 | return; |
281 | |
282 | // -- expensive pass -- determine all regs alive during func start |
283 | DataflowInfoManager Info(Function, RA.get(), nullptr); |
284 | BitVector AliveAtStart = *Info.getLivenessAnalysis().getStateAt( |
285 | Point: ProgramPoint::getFirstPointAt(BB&: *Function.begin())); |
286 | for (BinaryBasicBlock &BB : Function) |
287 | if (BB.pred_size() == 0) |
288 | AliveAtStart |= *Info.getLivenessAnalysis().getStateAt( |
289 | Point: ProgramPoint::getFirstPointAt(BB)); |
290 | |
291 | // Mark frame pointer alive because of CFI |
292 | AliveAtStart |= BC.MIB->getAliases(Reg: BC.MIB->getFramePointer(), OnlySmaller: false); |
293 | // Never touch return registers |
294 | BC.MIB->getDefaultLiveOut(Regs&: AliveAtStart); |
295 | |
296 | // Try swapping more profitable options first |
297 | auto Begin = RankedRegs.begin(); |
298 | auto End = std::prev(x: RankedRegs.end()); |
299 | while (Begin != End) { |
300 | MCPhysReg ClassicReg = *End; |
301 | if (!ClassicRegs[ClassicReg] || RegScore[ClassicReg] <= 0) { |
302 | --End; |
303 | continue; |
304 | } |
305 | |
306 | MCPhysReg ExtReg = *Begin; |
307 | if (!Extended[ExtReg] || RegScore[ExtReg] <= 0) { |
308 | ++Begin; |
309 | continue; |
310 | } |
311 | |
312 | if (RegScore[ClassicReg] << 1 >= RegScore[ExtReg]) { |
313 | LLVM_DEBUG(dbgs() << " Ending at " << BC.MRI->getName(ClassicReg) |
314 | << " with " << BC.MRI->getName(ExtReg) |
315 | << " because exchange is not profitable\n" ); |
316 | break; |
317 | } |
318 | |
319 | BitVector AnyAliasAlive = AliveAtStart; |
320 | AnyAliasAlive &= BC.MIB->getAliases(Reg: ClassicReg); |
321 | if (AnyAliasAlive.any()) { |
322 | LLVM_DEBUG(dbgs() << " Bailed on " << BC.MRI->getName(ClassicReg) |
323 | << " with " << BC.MRI->getName(ExtReg) |
324 | << " because classic reg is alive\n" ); |
325 | --End; |
326 | continue; |
327 | } |
328 | AnyAliasAlive = AliveAtStart; |
329 | AnyAliasAlive &= BC.MIB->getAliases(Reg: ExtReg); |
330 | if (AnyAliasAlive.any()) { |
331 | LLVM_DEBUG(dbgs() << " Bailed on " << BC.MRI->getName(ClassicReg) |
332 | << " with " << BC.MRI->getName(ExtReg) |
333 | << " because extended reg is alive\n" ); |
334 | ++Begin; |
335 | continue; |
336 | } |
337 | |
338 | // Opportunity detected. Swap. |
339 | LLVM_DEBUG(dbgs() << "\n ** Swapping " << BC.MRI->getName(ClassicReg) |
340 | << " with " << BC.MRI->getName(ExtReg) << "\n\n" ); |
341 | swap(Function, A: ClassicReg, B: ExtReg); |
342 | FuncsChanged.insert(V: &Function); |
343 | for (BinaryFunction *ChildFrag : Function.getFragments()) { |
344 | swap(Function&: *ChildFrag, A: ClassicReg, B: ExtReg); |
345 | FuncsChanged.insert(V: ChildFrag); |
346 | } |
347 | ++Begin; |
348 | if (Begin == End) |
349 | break; |
350 | --End; |
351 | } |
352 | } |
353 | |
354 | bool RegReAssign::conservativePassOverFunction(BinaryFunction &Function) { |
355 | BinaryContext &BC = Function.getBinaryContext(); |
356 | rankRegisters(Function); |
357 | |
358 | for (BinaryFunction *ChildFrag : Function.getFragments()) { |
359 | if (ChildFrag->getParentFragments()->size() > 1) |
360 | return false; |
361 | if (ChildFrag->empty()) |
362 | return false; |
363 | } |
364 | |
365 | // Try swapping R12, R13, R14 or R15 with RBX (we work with all callee-saved |
366 | // regs except RBP) |
367 | MCPhysReg Candidate = 0; |
368 | for (int J : ExtendedCSR.set_bits()) |
369 | if (RegScore[J] > RegScore[Candidate]) |
370 | Candidate = J; |
371 | |
372 | if (!Candidate || RegScore[Candidate] < 0) |
373 | return false; |
374 | |
375 | // Check if our classic callee-saved reg (RBX is the only one) has lower |
376 | // score / utilization rate |
377 | MCPhysReg RBX = 0; |
378 | for (int I : ClassicCSR.set_bits()) { |
379 | int64_t ScoreRBX = RegScore[I]; |
380 | if (ScoreRBX <= 0) |
381 | continue; |
382 | |
383 | if (RegScore[Candidate] > (ScoreRBX + 10)) |
384 | RBX = I; |
385 | } |
386 | |
387 | if (!RBX) |
388 | return false; |
389 | |
390 | // The high 8 bits of the register will never be swapped. To prevent the high |
391 | // 8 bits from being swapped incorrectly, we should switched to swapping the |
392 | // low 8 bits of the register instead. |
393 | if (BC.MIB->isUpper8BitReg(Reg: RBX)) { |
394 | RBX = BC.MIB->getAliasSized(Reg: RBX, Size: 1); |
395 | if (RegScore[RBX] < 0 || RegScore[RBX] > RegScore[Candidate]) |
396 | return false; |
397 | } |
398 | |
399 | LLVM_DEBUG(dbgs() << "\n ** Swapping " << BC.MRI->getName(RBX) << " with " |
400 | << BC.MRI->getName(Candidate) << "\n\n" ); |
401 | (void)BC; |
402 | swap(Function, A: RBX, B: Candidate); |
403 | FuncsChanged.insert(V: &Function); |
404 | for (BinaryFunction *ChildFrag : Function.getFragments()) { |
405 | swap(Function&: *ChildFrag, A: RBX, B: Candidate); |
406 | FuncsChanged.insert(V: ChildFrag); |
407 | } |
408 | return true; |
409 | } |
410 | |
411 | void RegReAssign::setupAggressivePass(BinaryContext &BC, |
412 | std::map<uint64_t, BinaryFunction> &BFs) { |
413 | setupConservativePass(BC, BFs); |
414 | CG.reset(p: new BinaryFunctionCallGraph(buildCallGraph(BC))); |
415 | RA.reset(p: new RegAnalysis(BC, &BFs, &*CG)); |
416 | |
417 | GPRegs = BitVector(BC.MRI->getNumRegs(), false); |
418 | BC.MIB->getGPRegs(Regs&: GPRegs); |
419 | } |
420 | |
421 | void RegReAssign::setupConservativePass( |
422 | BinaryContext &BC, std::map<uint64_t, BinaryFunction> &BFs) { |
423 | // Set up constant bitvectors used throughout this analysis |
424 | ClassicRegs = BitVector(BC.MRI->getNumRegs(), false); |
425 | CalleeSaved = BitVector(BC.MRI->getNumRegs(), false); |
426 | ClassicCSR = BitVector(BC.MRI->getNumRegs(), false); |
427 | ExtendedCSR = BitVector(BC.MRI->getNumRegs(), false); |
428 | // Never consider the frame pointer |
429 | BC.MIB->getClassicGPRegs(Regs&: ClassicRegs); |
430 | ClassicRegs.flip(); |
431 | ClassicRegs |= BC.MIB->getAliases(Reg: BC.MIB->getFramePointer(), OnlySmaller: false); |
432 | ClassicRegs.flip(); |
433 | BC.MIB->getCalleeSavedRegs(Regs&: CalleeSaved); |
434 | ClassicCSR |= ClassicRegs; |
435 | ClassicCSR &= CalleeSaved; |
436 | BC.MIB->getClassicGPRegs(Regs&: ClassicRegs); |
437 | ExtendedCSR |= ClassicRegs; |
438 | ExtendedCSR.flip(); |
439 | ExtendedCSR &= CalleeSaved; |
440 | |
441 | LLVM_DEBUG({ |
442 | RegStatePrinter P(BC); |
443 | dbgs() << "Starting register reassignment\nClassicRegs: " ; |
444 | P.print(dbgs(), ClassicRegs); |
445 | dbgs() << "\nCalleeSaved: " ; |
446 | P.print(dbgs(), CalleeSaved); |
447 | dbgs() << "\nClassicCSR: " ; |
448 | P.print(dbgs(), ClassicCSR); |
449 | dbgs() << "\nExtendedCSR: " ; |
450 | P.print(dbgs(), ExtendedCSR); |
451 | dbgs() << "\n" ; |
452 | }); |
453 | } |
454 | |
455 | Error RegReAssign::runOnFunctions(BinaryContext &BC) { |
456 | RegScore = std::vector<int64_t>(BC.MRI->getNumRegs(), 0); |
457 | RankedRegs = std::vector<size_t>(BC.MRI->getNumRegs(), 0); |
458 | |
459 | if (opts::AggressiveReAssign) |
460 | setupAggressivePass(BC, BFs&: BC.getBinaryFunctions()); |
461 | else |
462 | setupConservativePass(BC, BFs&: BC.getBinaryFunctions()); |
463 | |
464 | for (auto &I : BC.getBinaryFunctions()) { |
465 | BinaryFunction &Function = I.second; |
466 | |
467 | if (!Function.isSimple() || Function.isIgnored() || Function.isFragment()) |
468 | continue; |
469 | |
470 | LLVM_DEBUG(dbgs() << "====================================\n" ); |
471 | LLVM_DEBUG(dbgs() << " - " << Function.getPrintName() << "\n" ); |
472 | if (!conservativePassOverFunction(Function) && opts::AggressiveReAssign) { |
473 | aggressivePassOverFunction(Function); |
474 | LLVM_DEBUG({ |
475 | if (FuncsChanged.count(&Function)) |
476 | dbgs() << "Aggressive pass successful on " << Function.getPrintName() |
477 | << "\n" ; |
478 | }); |
479 | } |
480 | } |
481 | |
482 | if (FuncsChanged.empty()) { |
483 | BC.outs() << "BOLT-INFO: Reg Reassignment Pass: no changes were made.\n" ; |
484 | return Error::success(); |
485 | } |
486 | if (opts::UpdateDebugSections) |
487 | BC.outs() |
488 | << "BOLT-WARNING: You used -reg-reassign and -update-debug-sections." |
489 | << " Some registers were changed but associated AT_LOCATION for " |
490 | << "impacted variables were NOT updated! This operation is " |
491 | << "currently unsupported by BOLT.\n" ; |
492 | BC.outs() << "BOLT-INFO: Reg Reassignment Pass Stats:\n" ; |
493 | BC.outs() << "\t " << FuncsChanged.size() << " functions affected.\n" ; |
494 | BC.outs() << "\t " << StaticBytesSaved << " static bytes saved.\n" ; |
495 | BC.outs() << "\t " << DynBytesSaved << " dynamic bytes saved.\n" ; |
496 | return Error::success(); |
497 | } |
498 | |
499 | } // namespace bolt |
500 | } // namespace llvm |
501 | |