10b57cec5SDimitry Andric //===----------------------- AlignmentFromAssumptions.cpp -----------------===// 20b57cec5SDimitry Andric // Set Load/Store Alignments From Assumptions 30b57cec5SDimitry Andric // 40b57cec5SDimitry Andric // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 50b57cec5SDimitry Andric // See https://llvm.org/LICENSE.txt for license information. 60b57cec5SDimitry Andric // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 70b57cec5SDimitry Andric // 80b57cec5SDimitry Andric //===----------------------------------------------------------------------===// 90b57cec5SDimitry Andric // 100b57cec5SDimitry Andric // This file implements a ScalarEvolution-based transformation to set 110b57cec5SDimitry Andric // the alignments of load, stores and memory intrinsics based on the truth 120b57cec5SDimitry Andric // expressions of assume intrinsics. The primary motivation is to handle 130b57cec5SDimitry Andric // complex alignment assumptions that apply to vector loads and stores that 140b57cec5SDimitry Andric // appear after vectorization and unrolling. 150b57cec5SDimitry Andric // 160b57cec5SDimitry Andric //===----------------------------------------------------------------------===// 170b57cec5SDimitry Andric 180b57cec5SDimitry Andric #include "llvm/Transforms/Scalar/AlignmentFromAssumptions.h" 190b57cec5SDimitry Andric #include "llvm/ADT/SmallPtrSet.h" 200b57cec5SDimitry Andric #include "llvm/ADT/Statistic.h" 210b57cec5SDimitry Andric #include "llvm/Analysis/AliasAnalysis.h" 220b57cec5SDimitry Andric #include "llvm/Analysis/AssumptionCache.h" 230b57cec5SDimitry Andric #include "llvm/Analysis/GlobalsModRef.h" 240b57cec5SDimitry Andric #include "llvm/Analysis/LoopInfo.h" 250b57cec5SDimitry Andric #include "llvm/Analysis/ScalarEvolutionExpressions.h" 260b57cec5SDimitry Andric #include "llvm/Analysis/ValueTracking.h" 270b57cec5SDimitry Andric #include "llvm/IR/Dominators.h" 280b57cec5SDimitry Andric #include "llvm/IR/Instruction.h" 2981ad6265SDimitry Andric #include "llvm/IR/Instructions.h" 305ffd83dbSDimitry Andric #include "llvm/IR/IntrinsicInst.h" 310b57cec5SDimitry Andric #include "llvm/Support/Debug.h" 320b57cec5SDimitry Andric #include "llvm/Support/raw_ostream.h" 33fe6060f1SDimitry Andric 3406c3fb27SDimitry Andric #define DEBUG_TYPE "alignment-from-assumptions" 350b57cec5SDimitry Andric using namespace llvm; 360b57cec5SDimitry Andric 370b57cec5SDimitry Andric STATISTIC(NumLoadAlignChanged, 380b57cec5SDimitry Andric "Number of loads changed by alignment assumptions"); 390b57cec5SDimitry Andric STATISTIC(NumStoreAlignChanged, 400b57cec5SDimitry Andric "Number of stores changed by alignment assumptions"); 410b57cec5SDimitry Andric STATISTIC(NumMemIntAlignChanged, 420b57cec5SDimitry Andric "Number of memory intrinsics changed by alignment assumptions"); 430b57cec5SDimitry Andric 440b57cec5SDimitry Andric // Given an expression for the (constant) alignment, AlignSCEV, and an 450b57cec5SDimitry Andric // expression for the displacement between a pointer and the aligned address, 460b57cec5SDimitry Andric // DiffSCEV, compute the alignment of the displaced pointer if it can be reduced 470b57cec5SDimitry Andric // to a constant. Using SCEV to compute alignment handles the case where 480b57cec5SDimitry Andric // DiffSCEV is a recurrence with constant start such that the aligned offset 490b57cec5SDimitry Andric // is constant. e.g. {16,+,32} % 32 -> 16. 505ffd83dbSDimitry Andric static MaybeAlign getNewAlignmentDiff(const SCEV *DiffSCEV, 510b57cec5SDimitry Andric const SCEV *AlignSCEV, 520b57cec5SDimitry Andric ScalarEvolution *SE) { 530b57cec5SDimitry Andric // DiffUnits = Diff % int64_t(Alignment) 548bcb0991SDimitry Andric const SCEV *DiffUnitsSCEV = SE->getURemExpr(DiffSCEV, AlignSCEV); 550b57cec5SDimitry Andric 560b57cec5SDimitry Andric LLVM_DEBUG(dbgs() << "\talignment relative to " << *AlignSCEV << " is " 570b57cec5SDimitry Andric << *DiffUnitsSCEV << " (diff: " << *DiffSCEV << ")\n"); 580b57cec5SDimitry Andric 590b57cec5SDimitry Andric if (const SCEVConstant *ConstDUSCEV = 600b57cec5SDimitry Andric dyn_cast<SCEVConstant>(DiffUnitsSCEV)) { 610b57cec5SDimitry Andric int64_t DiffUnits = ConstDUSCEV->getValue()->getSExtValue(); 620b57cec5SDimitry Andric 630b57cec5SDimitry Andric // If the displacement is an exact multiple of the alignment, then the 640b57cec5SDimitry Andric // displaced pointer has the same alignment as the aligned pointer, so 650b57cec5SDimitry Andric // return the alignment value. 660b57cec5SDimitry Andric if (!DiffUnits) 675ffd83dbSDimitry Andric return cast<SCEVConstant>(AlignSCEV)->getValue()->getAlignValue(); 680b57cec5SDimitry Andric 690b57cec5SDimitry Andric // If the displacement is not an exact multiple, but the remainder is a 700b57cec5SDimitry Andric // constant, then return this remainder (but only if it is a power of 2). 710b57cec5SDimitry Andric uint64_t DiffUnitsAbs = std::abs(DiffUnits); 720b57cec5SDimitry Andric if (isPowerOf2_64(DiffUnitsAbs)) 735ffd83dbSDimitry Andric return Align(DiffUnitsAbs); 740b57cec5SDimitry Andric } 750b57cec5SDimitry Andric 76bdd1243dSDimitry Andric return std::nullopt; 770b57cec5SDimitry Andric } 780b57cec5SDimitry Andric 790b57cec5SDimitry Andric // There is an address given by an offset OffSCEV from AASCEV which has an 800b57cec5SDimitry Andric // alignment AlignSCEV. Use that information, if possible, to compute a new 810b57cec5SDimitry Andric // alignment for Ptr. 825ffd83dbSDimitry Andric static Align getNewAlignment(const SCEV *AASCEV, const SCEV *AlignSCEV, 830b57cec5SDimitry Andric const SCEV *OffSCEV, Value *Ptr, 840b57cec5SDimitry Andric ScalarEvolution *SE) { 850b57cec5SDimitry Andric const SCEV *PtrSCEV = SE->getSCEV(Ptr); 86*5f757f3fSDimitry Andric 870b57cec5SDimitry Andric const SCEV *DiffSCEV = SE->getMinusSCEV(PtrSCEV, AASCEV); 88fe6060f1SDimitry Andric if (isa<SCEVCouldNotCompute>(DiffSCEV)) 89fe6060f1SDimitry Andric return Align(1); 900b57cec5SDimitry Andric 910b57cec5SDimitry Andric // On 32-bit platforms, DiffSCEV might now have type i32 -- we've always 920b57cec5SDimitry Andric // sign-extended OffSCEV to i64, so make sure they agree again. 930b57cec5SDimitry Andric DiffSCEV = SE->getNoopOrSignExtend(DiffSCEV, OffSCEV->getType()); 940b57cec5SDimitry Andric 950b57cec5SDimitry Andric // What we really want to know is the overall offset to the aligned 960b57cec5SDimitry Andric // address. This address is displaced by the provided offset. 97fe6060f1SDimitry Andric DiffSCEV = SE->getAddExpr(DiffSCEV, OffSCEV); 980b57cec5SDimitry Andric 990b57cec5SDimitry Andric LLVM_DEBUG(dbgs() << "AFI: alignment of " << *Ptr << " relative to " 1000b57cec5SDimitry Andric << *AlignSCEV << " and offset " << *OffSCEV 1010b57cec5SDimitry Andric << " using diff " << *DiffSCEV << "\n"); 1020b57cec5SDimitry Andric 1035ffd83dbSDimitry Andric if (MaybeAlign NewAlignment = getNewAlignmentDiff(DiffSCEV, AlignSCEV, SE)) { 1045ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tnew alignment: " << DebugStr(NewAlignment) << "\n"); 1055ffd83dbSDimitry Andric return *NewAlignment; 1065ffd83dbSDimitry Andric } 1070b57cec5SDimitry Andric 1085ffd83dbSDimitry Andric if (const SCEVAddRecExpr *DiffARSCEV = dyn_cast<SCEVAddRecExpr>(DiffSCEV)) { 1090b57cec5SDimitry Andric // The relative offset to the alignment assumption did not yield a constant, 1100b57cec5SDimitry Andric // but we should try harder: if we assume that a is 32-byte aligned, then in 1110b57cec5SDimitry Andric // for (i = 0; i < 1024; i += 4) r += a[i]; not all of the loads from a are 1120b57cec5SDimitry Andric // 32-byte aligned, but instead alternate between 32 and 16-byte alignment. 1130b57cec5SDimitry Andric // As a result, the new alignment will not be a constant, but can still 1140b57cec5SDimitry Andric // be improved over the default (of 4) to 16. 1150b57cec5SDimitry Andric 1160b57cec5SDimitry Andric const SCEV *DiffStartSCEV = DiffARSCEV->getStart(); 1170b57cec5SDimitry Andric const SCEV *DiffIncSCEV = DiffARSCEV->getStepRecurrence(*SE); 1180b57cec5SDimitry Andric 1190b57cec5SDimitry Andric LLVM_DEBUG(dbgs() << "\ttrying start/inc alignment using start " 1200b57cec5SDimitry Andric << *DiffStartSCEV << " and inc " << *DiffIncSCEV << "\n"); 1210b57cec5SDimitry Andric 1220b57cec5SDimitry Andric // Now compute the new alignment using the displacement to the value in the 1230b57cec5SDimitry Andric // first iteration, and also the alignment using the per-iteration delta. 1240b57cec5SDimitry Andric // If these are the same, then use that answer. Otherwise, use the smaller 1250b57cec5SDimitry Andric // one, but only if it divides the larger one. 1265ffd83dbSDimitry Andric MaybeAlign NewAlignment = getNewAlignmentDiff(DiffStartSCEV, AlignSCEV, SE); 1275ffd83dbSDimitry Andric MaybeAlign NewIncAlignment = 1285ffd83dbSDimitry Andric getNewAlignmentDiff(DiffIncSCEV, AlignSCEV, SE); 1290b57cec5SDimitry Andric 1305ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tnew start alignment: " << DebugStr(NewAlignment) 1315ffd83dbSDimitry Andric << "\n"); 1325ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tnew inc alignment: " << DebugStr(NewIncAlignment) 1335ffd83dbSDimitry Andric << "\n"); 1340b57cec5SDimitry Andric 1355ffd83dbSDimitry Andric if (!NewAlignment || !NewIncAlignment) 1365ffd83dbSDimitry Andric return Align(1); 1375ffd83dbSDimitry Andric 1385ffd83dbSDimitry Andric const Align NewAlign = *NewAlignment; 1395ffd83dbSDimitry Andric const Align NewIncAlign = *NewIncAlignment; 1405ffd83dbSDimitry Andric if (NewAlign > NewIncAlign) { 1415ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tnew start/inc alignment: " 1425ffd83dbSDimitry Andric << DebugStr(NewIncAlign) << "\n"); 1435ffd83dbSDimitry Andric return NewIncAlign; 1440b57cec5SDimitry Andric } 1455ffd83dbSDimitry Andric if (NewIncAlign > NewAlign) { 1465ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tnew start/inc alignment: " << DebugStr(NewAlign) 1470b57cec5SDimitry Andric << "\n"); 1485ffd83dbSDimitry Andric return NewAlign; 1490b57cec5SDimitry Andric } 1505ffd83dbSDimitry Andric assert(NewIncAlign == NewAlign); 1515ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tnew start/inc alignment: " << DebugStr(NewAlign) 1520b57cec5SDimitry Andric << "\n"); 1535ffd83dbSDimitry Andric return NewAlign; 1540b57cec5SDimitry Andric } 1550b57cec5SDimitry Andric 1565ffd83dbSDimitry Andric return Align(1); 1570b57cec5SDimitry Andric } 1580b57cec5SDimitry Andric 1590b57cec5SDimitry Andric bool AlignmentFromAssumptionsPass::extractAlignmentInfo(CallInst *I, 160e8d8bef9SDimitry Andric unsigned Idx, 1610b57cec5SDimitry Andric Value *&AAPtr, 1620b57cec5SDimitry Andric const SCEV *&AlignSCEV, 1630b57cec5SDimitry Andric const SCEV *&OffSCEV) { 164e8d8bef9SDimitry Andric Type *Int64Ty = Type::getInt64Ty(I->getContext()); 165e8d8bef9SDimitry Andric OperandBundleUse AlignOB = I->getOperandBundleAt(Idx); 166e8d8bef9SDimitry Andric if (AlignOB.getTagName() != "align") 1670b57cec5SDimitry Andric return false; 168e8d8bef9SDimitry Andric assert(AlignOB.Inputs.size() >= 2); 169e8d8bef9SDimitry Andric AAPtr = AlignOB.Inputs[0].get(); 170e8d8bef9SDimitry Andric // TODO: Consider accumulating the offset to the base. 171e8d8bef9SDimitry Andric AAPtr = AAPtr->stripPointerCastsSameRepresentation(); 172e8d8bef9SDimitry Andric AlignSCEV = SE->getSCEV(AlignOB.Inputs[1].get()); 173e8d8bef9SDimitry Andric AlignSCEV = SE->getTruncateOrZeroExtend(AlignSCEV, Int64Ty); 17469ade1e0SDimitry Andric if (!isa<SCEVConstant>(AlignSCEV)) 17569ade1e0SDimitry Andric // Added to suppress a crash because consumer doesn't expect non-constant 17669ade1e0SDimitry Andric // alignments in the assume bundle. TODO: Consider generalizing caller. 17769ade1e0SDimitry Andric return false; 178*5f757f3fSDimitry Andric if (!cast<SCEVConstant>(AlignSCEV)->getAPInt().isPowerOf2()) 179*5f757f3fSDimitry Andric // Only power of two alignments are supported. 180*5f757f3fSDimitry Andric return false; 181e8d8bef9SDimitry Andric if (AlignOB.Inputs.size() == 3) 182e8d8bef9SDimitry Andric OffSCEV = SE->getSCEV(AlignOB.Inputs[2].get()); 183e8d8bef9SDimitry Andric else 1840b57cec5SDimitry Andric OffSCEV = SE->getZero(Int64Ty); 185e8d8bef9SDimitry Andric OffSCEV = SE->getTruncateOrZeroExtend(OffSCEV, Int64Ty); 1860b57cec5SDimitry Andric return true; 1870b57cec5SDimitry Andric } 1880b57cec5SDimitry Andric 189e8d8bef9SDimitry Andric bool AlignmentFromAssumptionsPass::processAssumption(CallInst *ACall, 190e8d8bef9SDimitry Andric unsigned Idx) { 1910b57cec5SDimitry Andric Value *AAPtr; 1920b57cec5SDimitry Andric const SCEV *AlignSCEV, *OffSCEV; 193e8d8bef9SDimitry Andric if (!extractAlignmentInfo(ACall, Idx, AAPtr, AlignSCEV, OffSCEV)) 1940b57cec5SDimitry Andric return false; 1950b57cec5SDimitry Andric 1960b57cec5SDimitry Andric // Skip ConstantPointerNull and UndefValue. Assumptions on these shouldn't 1970b57cec5SDimitry Andric // affect other users. 1980b57cec5SDimitry Andric if (isa<ConstantData>(AAPtr)) 1990b57cec5SDimitry Andric return false; 2000b57cec5SDimitry Andric 2010b57cec5SDimitry Andric const SCEV *AASCEV = SE->getSCEV(AAPtr); 2020b57cec5SDimitry Andric 2030b57cec5SDimitry Andric // Apply the assumption to all other users of the specified pointer. 2040b57cec5SDimitry Andric SmallPtrSet<Instruction *, 32> Visited; 2050b57cec5SDimitry Andric SmallVector<Instruction*, 16> WorkList; 2060b57cec5SDimitry Andric for (User *J : AAPtr->users()) { 2070b57cec5SDimitry Andric if (J == ACall) 2080b57cec5SDimitry Andric continue; 2090b57cec5SDimitry Andric 2100b57cec5SDimitry Andric if (Instruction *K = dyn_cast<Instruction>(J)) 2110b57cec5SDimitry Andric WorkList.push_back(K); 2120b57cec5SDimitry Andric } 2130b57cec5SDimitry Andric 2140b57cec5SDimitry Andric while (!WorkList.empty()) { 2150b57cec5SDimitry Andric Instruction *J = WorkList.pop_back_val(); 2160b57cec5SDimitry Andric if (LoadInst *LI = dyn_cast<LoadInst>(J)) { 217e8d8bef9SDimitry Andric if (!isValidAssumeForContext(ACall, J, DT)) 218e8d8bef9SDimitry Andric continue; 2195ffd83dbSDimitry Andric Align NewAlignment = getNewAlignment(AASCEV, AlignSCEV, OffSCEV, 2200b57cec5SDimitry Andric LI->getPointerOperand(), SE); 2215ffd83dbSDimitry Andric if (NewAlignment > LI->getAlign()) { 2225ffd83dbSDimitry Andric LI->setAlignment(NewAlignment); 2230b57cec5SDimitry Andric ++NumLoadAlignChanged; 2240b57cec5SDimitry Andric } 2250b57cec5SDimitry Andric } else if (StoreInst *SI = dyn_cast<StoreInst>(J)) { 226e8d8bef9SDimitry Andric if (!isValidAssumeForContext(ACall, J, DT)) 227e8d8bef9SDimitry Andric continue; 2285ffd83dbSDimitry Andric Align NewAlignment = getNewAlignment(AASCEV, AlignSCEV, OffSCEV, 2290b57cec5SDimitry Andric SI->getPointerOperand(), SE); 2305ffd83dbSDimitry Andric if (NewAlignment > SI->getAlign()) { 2315ffd83dbSDimitry Andric SI->setAlignment(NewAlignment); 2320b57cec5SDimitry Andric ++NumStoreAlignChanged; 2330b57cec5SDimitry Andric } 2340b57cec5SDimitry Andric } else if (MemIntrinsic *MI = dyn_cast<MemIntrinsic>(J)) { 235e8d8bef9SDimitry Andric if (!isValidAssumeForContext(ACall, J, DT)) 236e8d8bef9SDimitry Andric continue; 2375ffd83dbSDimitry Andric Align NewDestAlignment = 2385ffd83dbSDimitry Andric getNewAlignment(AASCEV, AlignSCEV, OffSCEV, MI->getDest(), SE); 2390b57cec5SDimitry Andric 2405ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tmem inst: " << DebugStr(NewDestAlignment) 2415ffd83dbSDimitry Andric << "\n";); 2425ffd83dbSDimitry Andric if (NewDestAlignment > *MI->getDestAlign()) { 2430b57cec5SDimitry Andric MI->setDestAlignment(NewDestAlignment); 2440b57cec5SDimitry Andric ++NumMemIntAlignChanged; 2450b57cec5SDimitry Andric } 2460b57cec5SDimitry Andric 2470b57cec5SDimitry Andric // For memory transfers, there is also a source alignment that 2480b57cec5SDimitry Andric // can be set. 2490b57cec5SDimitry Andric if (MemTransferInst *MTI = dyn_cast<MemTransferInst>(MI)) { 2505ffd83dbSDimitry Andric Align NewSrcAlignment = 2515ffd83dbSDimitry Andric getNewAlignment(AASCEV, AlignSCEV, OffSCEV, MTI->getSource(), SE); 2520b57cec5SDimitry Andric 2535ffd83dbSDimitry Andric LLVM_DEBUG(dbgs() << "\tmem trans: " << DebugStr(NewSrcAlignment) 2545ffd83dbSDimitry Andric << "\n";); 2550b57cec5SDimitry Andric 2565ffd83dbSDimitry Andric if (NewSrcAlignment > *MTI->getSourceAlign()) { 2570b57cec5SDimitry Andric MTI->setSourceAlignment(NewSrcAlignment); 2580b57cec5SDimitry Andric ++NumMemIntAlignChanged; 2590b57cec5SDimitry Andric } 2600b57cec5SDimitry Andric } 2610b57cec5SDimitry Andric } 2620b57cec5SDimitry Andric 2630b57cec5SDimitry Andric // Now that we've updated that use of the pointer, look for other uses of 2640b57cec5SDimitry Andric // the pointer to update. 2650b57cec5SDimitry Andric Visited.insert(J); 266*5f757f3fSDimitry Andric if (isa<GetElementPtrInst>(J) || isa<PHINode>(J)) 267*5f757f3fSDimitry Andric for (auto &U : J->uses()) { 268*5f757f3fSDimitry Andric if (U->getType()->isPointerTy()) { 269*5f757f3fSDimitry Andric Instruction *K = cast<Instruction>(U.getUser()); 270*5f757f3fSDimitry Andric StoreInst *SI = dyn_cast<StoreInst>(K); 271*5f757f3fSDimitry Andric if (SI && SI->getPointerOperandIndex() != U.getOperandNo()) 272*5f757f3fSDimitry Andric continue; 273e8d8bef9SDimitry Andric if (!Visited.count(K)) 2740b57cec5SDimitry Andric WorkList.push_back(K); 2750b57cec5SDimitry Andric } 2760b57cec5SDimitry Andric } 277*5f757f3fSDimitry Andric } 2780b57cec5SDimitry Andric 2790b57cec5SDimitry Andric return true; 2800b57cec5SDimitry Andric } 2810b57cec5SDimitry Andric 2820b57cec5SDimitry Andric bool AlignmentFromAssumptionsPass::runImpl(Function &F, AssumptionCache &AC, 2830b57cec5SDimitry Andric ScalarEvolution *SE_, 2840b57cec5SDimitry Andric DominatorTree *DT_) { 2850b57cec5SDimitry Andric SE = SE_; 2860b57cec5SDimitry Andric DT = DT_; 2870b57cec5SDimitry Andric 2880b57cec5SDimitry Andric bool Changed = false; 2890b57cec5SDimitry Andric for (auto &AssumeVH : AC.assumptions()) 290e8d8bef9SDimitry Andric if (AssumeVH) { 291e8d8bef9SDimitry Andric CallInst *Call = cast<CallInst>(AssumeVH); 292e8d8bef9SDimitry Andric for (unsigned Idx = 0; Idx < Call->getNumOperandBundles(); Idx++) 293e8d8bef9SDimitry Andric Changed |= processAssumption(Call, Idx); 294e8d8bef9SDimitry Andric } 2950b57cec5SDimitry Andric 2960b57cec5SDimitry Andric return Changed; 2970b57cec5SDimitry Andric } 2980b57cec5SDimitry Andric 2990b57cec5SDimitry Andric PreservedAnalyses 3000b57cec5SDimitry Andric AlignmentFromAssumptionsPass::run(Function &F, FunctionAnalysisManager &AM) { 3010b57cec5SDimitry Andric 3020b57cec5SDimitry Andric AssumptionCache &AC = AM.getResult<AssumptionAnalysis>(F); 3030b57cec5SDimitry Andric ScalarEvolution &SE = AM.getResult<ScalarEvolutionAnalysis>(F); 3040b57cec5SDimitry Andric DominatorTree &DT = AM.getResult<DominatorTreeAnalysis>(F); 3050b57cec5SDimitry Andric if (!runImpl(F, AC, &SE, &DT)) 3060b57cec5SDimitry Andric return PreservedAnalyses::all(); 3070b57cec5SDimitry Andric 3080b57cec5SDimitry Andric PreservedAnalyses PA; 3090b57cec5SDimitry Andric PA.preserveSet<CFGAnalyses>(); 3100b57cec5SDimitry Andric PA.preserve<ScalarEvolutionAnalysis>(); 3110b57cec5SDimitry Andric return PA; 3120b57cec5SDimitry Andric } 313