Skip to content

Commit 33813d3

Browse files
committed
[VPlan] Add VPSymbolicValueSC for typed VPValues w/o underlying IR value
This introduces a new VPSymbolicValueSC to be used for typed live-in VPValues without underlying IR. VPValue is updated to store either the type or an underlying value in an union. This allows keeping the size of VPValue unchanged. The main motivation for adding the type is to not require passing the canonical IV type to VPTypeAnalysis. While with this patch, we still need to pass the LLVMContext, this can also be removed in a future patch (see 9493c38)
1 parent 01f0425 commit 33813d3

File tree

11 files changed

+77
-55
lines changed

11 files changed

+77
-55
lines changed

llvm/lib/Transforms/Vectorize/LoopVectorize.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4478,7 +4478,7 @@ void LoopVectorizationPlanner::emitInvalidCostRemarks(
44784478
static bool willGenerateVectors(VPlan &Plan, ElementCount VF,
44794479
const TargetTransformInfo &TTI) {
44804480
assert(VF.isVector() && "Checking a scalar VF?");
4481-
VPTypeAnalysis TypeInfo(Plan.getCanonicalIV()->getScalarType());
4481+
VPTypeAnalysis TypeInfo(Plan.getCanonicalIV()->getScalarType()->getContext());
44824482
DenseSet<VPRecipeBase *> EphemeralRecipes;
44834483
collectEphemeralRecipesForVPlan(Plan, EphemeralRecipes);
44844484
// Set of already visited types.
@@ -9083,7 +9083,7 @@ static VPInstruction *addResumePhiRecipeForInduction(
90839083
/// \p IVEndValues.
90849084
static void addScalarResumePhis(VPRecipeBuilder &Builder, VPlan &Plan,
90859085
DenseMap<VPValue *, VPValue *> &IVEndValues) {
9086-
VPTypeAnalysis TypeInfo(Plan.getCanonicalIV()->getScalarType());
9086+
VPTypeAnalysis TypeInfo(Plan.getCanonicalIV()->getScalarType()->getContext());
90879087
auto *ScalarPH = Plan.getScalarPreheader();
90889088
auto *MiddleVPBB = cast<VPBasicBlock>(ScalarPH->getSinglePredecessor());
90899089
VPRegionBlock *VectorRegion = Plan.getVectorLoopRegion();
@@ -9326,7 +9326,8 @@ LoopVectorizationPlanner::tryToBuildVPlanWithVPRecipes(VFRange &Range) {
93269326
return !CM.requiresScalarEpilogue(VF.isVector());
93279327
},
93289328
Range);
9329-
auto Plan = std::make_unique<VPlan>(OrigLoop);
9329+
auto Plan =
9330+
std::make_unique<VPlan>(OrigLoop, Legal->getWidestInductionType());
93309331
// Build hierarchical CFG.
93319332
// Convert to VPlan-transform and consoliate all transforms for VPlan
93329333
// creation.
@@ -9632,7 +9633,8 @@ VPlanPtr LoopVectorizationPlanner::tryToBuildVPlan(VFRange &Range) {
96329633
assert(EnableVPlanNativePath && "VPlan-native path is not enabled.");
96339634

96349635
// Create new empty VPlan
9635-
auto Plan = std::make_unique<VPlan>(OrigLoop);
9636+
auto Plan =
9637+
std::make_unique<VPlan>(OrigLoop, Legal->getWidestInductionType());
96369638
// Build hierarchical CFG
96379639
VPlanHCFGBuilder HCFGBuilder(OrigLoop, LI, *Plan);
96389640
HCFGBuilder.buildHierarchicalCFG();

llvm/lib/Transforms/Vectorize/VPlan.cpp

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ VPTransformState::VPTransformState(const TargetTransformInfo *TTI,
222222
Loop *CurrentParentLoop, Type *CanonicalIVTy)
223223
: TTI(TTI), VF(VF), CFG(DT), LI(LI), Builder(Builder), ILV(ILV), Plan(Plan),
224224
CurrentParentLoop(CurrentParentLoop), LVer(nullptr),
225-
TypeAnalysis(CanonicalIVTy), VPDT(*Plan) {}
225+
TypeAnalysis(CanonicalIVTy->getContext()), VPDT(*Plan) {}
226226

227227
Value *VPTransformState::get(const VPValue *Def, const VPLane &Lane) {
228228
if (Def->isLiveIn())
@@ -850,7 +850,8 @@ void VPRegionBlock::print(raw_ostream &O, const Twine &Indent,
850850
}
851851
#endif
852852

853-
VPlan::VPlan(Loop *L) {
853+
VPlan::VPlan(Loop *L, Type *InductionTy)
854+
: VectorTripCount(InductionTy), VF(InductionTy), VFxUF(InductionTy) {
854855
setEntry(createVPIRBasicBlock(L->getLoopPreheader()));
855856
ScalarHeader = createVPIRBasicBlock(L->getHeader());
856857

@@ -861,7 +862,7 @@ VPlan::VPlan(Loop *L) {
861862
}
862863

863864
VPlan::~VPlan() {
864-
VPValue DummyValue;
865+
VPValue DummyValue((Type *)nullptr);
865866

866867
for (auto *VPB : CreatedBlocks) {
867868
if (auto *VPBB = dyn_cast<VPBasicBlock>(VPB)) {
@@ -891,10 +892,10 @@ void VPlan::prepareToExecute(Value *TripCountV, Value *VectorTripCountV,
891892
IRBuilder<> Builder(State.CFG.PrevBB->getTerminator());
892893
auto *TCMO = Builder.CreateSub(TripCountV, ConstantInt::get(TCTy, 1),
893894
"trip.count.minus.1");
894-
BackedgeTakenCount->setUnderlyingValue(TCMO);
895+
BackedgeTakenCount->replaceAllUsesWith(getOrAddLiveIn(TCMO));
895896
}
896897

897-
VectorTripCount.setUnderlyingValue(VectorTripCountV);
898+
VectorTripCount.replaceAllUsesWith(getOrAddLiveIn(VectorTripCountV));
898899

899900
IRBuilder<> Builder(State.CFG.PrevBB->getTerminator());
900901
// FIXME: Model VF * UF computation completely in VPlan.
@@ -903,12 +904,13 @@ void VPlan::prepareToExecute(Value *TripCountV, Value *VectorTripCountV,
903904
unsigned UF = getUF();
904905
if (VF.getNumUsers()) {
905906
Value *RuntimeVF = getRuntimeVF(Builder, TCTy, State.VF);
906-
VF.setUnderlyingValue(RuntimeVF);
907-
VFxUF.setUnderlyingValue(
907+
VF.replaceAllUsesWith(getOrAddLiveIn(RuntimeVF));
908+
VFxUF.replaceAllUsesWith(getOrAddLiveIn(
908909
UF > 1 ? Builder.CreateMul(RuntimeVF, ConstantInt::get(TCTy, UF))
909-
: RuntimeVF);
910+
: RuntimeVF));
910911
} else {
911-
VFxUF.setUnderlyingValue(createStepForVF(Builder, TCTy, State.VF, UF));
912+
VFxUF.replaceAllUsesWith(
913+
getOrAddLiveIn(createStepForVF(Builder, TCTy, State.VF, UF)));
912914
}
913915
}
914916

@@ -1175,7 +1177,8 @@ VPlan *VPlan::duplicate() {
11751177
return VPIRBB && VPIRBB->getIRBasicBlock() == ScalarHeaderIRBB;
11761178
}));
11771179
// Create VPlan, clone live-ins and remap operands in the cloned blocks.
1178-
auto *NewPlan = new VPlan(cast<VPBasicBlock>(NewEntry), NewScalarHeader);
1180+
auto *NewPlan = new VPlan(cast<VPBasicBlock>(NewEntry), NewScalarHeader,
1181+
getCanonicalIV()->getScalarType());
11791182
DenseMap<VPValue *, VPValue *> Old2NewVPValues;
11801183
for (VPValue *OldLiveIn : getLiveIns()) {
11811184
Old2NewVPValues[OldLiveIn] =
@@ -1185,7 +1188,7 @@ VPlan *VPlan::duplicate() {
11851188
Old2NewVPValues[&VF] = &NewPlan->VF;
11861189
Old2NewVPValues[&VFxUF] = &NewPlan->VFxUF;
11871190
if (BackedgeTakenCount) {
1188-
NewPlan->BackedgeTakenCount = new VPValue();
1191+
NewPlan->BackedgeTakenCount = new VPValue((Type *)nullptr);
11891192
Old2NewVPValues[BackedgeTakenCount] = NewPlan->BackedgeTakenCount;
11901193
}
11911194
assert(TripCount && "trip count must be set");
@@ -1371,6 +1374,11 @@ static bool isDefinedInsideLoopRegions(const VPValue *VPV) {
13711374
DefR->getParent()->getEnclosingLoopRegion());
13721375
}
13731376

1377+
Type *VPValue::getType() const {
1378+
assert(isLiveIn());
1379+
return SubclassID == VPSymbolicValueSC ? Ty : getUnderlyingValue()->getType();
1380+
}
1381+
13741382
bool VPValue::isDefinedOutsideLoopRegions() const {
13751383
return !isDefinedInsideLoopRegions(this);
13761384
}

llvm/lib/Transforms/Vectorize/VPlan.h

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3439,8 +3439,9 @@ class VPlan {
34393439

34403440
/// Construct a VPlan with \p Entry to the plan and with \p ScalarHeader
34413441
/// wrapping the original header of the scalar loop.
3442-
VPlan(VPBasicBlock *Entry, VPIRBasicBlock *ScalarHeader)
3443-
: Entry(Entry), ScalarHeader(ScalarHeader) {
3442+
VPlan(VPBasicBlock *Entry, VPIRBasicBlock *ScalarHeader, Type *InductionTy)
3443+
: Entry(Entry), ScalarHeader(ScalarHeader), VectorTripCount(InductionTy),
3444+
VF(InductionTy), VFxUF(InductionTy) {
34443445
Entry->setPlan(this);
34453446
assert(ScalarHeader->getNumSuccessors() == 0 &&
34463447
"scalar header must be a leaf node");
@@ -3450,11 +3451,12 @@ class VPlan {
34503451
/// Construct a VPlan for \p L. This will create VPIRBasicBlocks wrapping the
34513452
/// original preheader and scalar header of \p L, to be used as entry and
34523453
/// scalar header blocks of the new VPlan.
3453-
VPlan(Loop *L);
3454+
VPlan(Loop *L, Type *InductionTy);
34543455

34553456
/// Construct a VPlan with a new VPBasicBlock as entry, a VPIRBasicBlock
34563457
/// wrapping \p ScalarHeaderBB and a trip count of \p TC.
3457-
VPlan(BasicBlock *ScalarHeaderBB, VPValue *TC) {
3458+
VPlan(BasicBlock *ScalarHeaderBB, VPValue *TC, Type *InductionTy)
3459+
: VectorTripCount(InductionTy), VF(InductionTy), VFxUF(InductionTy) {
34583460
setEntry(createVPBasicBlock("preheader"));
34593461
ScalarHeader = createVPIRBasicBlock(ScalarHeaderBB);
34603462
TripCount = TC;
@@ -3546,7 +3548,7 @@ class VPlan {
35463548
/// The backedge taken count of the original loop.
35473549
VPValue *getOrCreateBackedgeTakenCount() {
35483550
if (!BackedgeTakenCount)
3549-
BackedgeTakenCount = new VPValue();
3551+
BackedgeTakenCount = new VPValue(getCanonicalIV()->getScalarType());
35503552
return BackedgeTakenCount;
35513553
}
35523554

llvm/lib/Transforms/Vectorize/VPlanAnalysis.cpp

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,8 @@ Type *VPTypeAnalysis::inferScalarType(const VPValue *V) {
231231
if (Type *CachedTy = CachedTypes.lookup(V))
232232
return CachedTy;
233233

234-
if (V->isLiveIn()) {
235-
if (auto *IRValue = V->getLiveInIRValue())
236-
return IRValue->getType();
237-
// All VPValues without any underlying IR value (like the vector trip count
238-
// or the backedge-taken count) have the same type as the canonical IV.
239-
return CanonicalIVTy;
240-
}
234+
if (V->isLiveIn())
235+
return V->getType();
241236

242237
Type *ResultTy =
243238
TypeSwitch<const VPRecipeBase *, Type *>(V->getDefiningRecipe())

llvm/lib/Transforms/Vectorize/VPlanAnalysis.h

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,6 @@ class Type;
3939
/// of the previously inferred types.
4040
class VPTypeAnalysis {
4141
DenseMap<const VPValue *, Type *> CachedTypes;
42-
/// Type of the canonical induction variable. Used for all VPValues without
43-
/// any underlying IR value (like the vector trip count or the backedge-taken
44-
/// count).
45-
Type *CanonicalIVTy;
4642
LLVMContext &Ctx;
4743

4844
Type *inferScalarTypeForRecipe(const VPBlendRecipe *R);
@@ -55,8 +51,7 @@ class VPTypeAnalysis {
5551
Type *inferScalarTypeForRecipe(const VPReplicateRecipe *R);
5652

5753
public:
58-
VPTypeAnalysis(Type *CanonicalIVTy)
59-
: CanonicalIVTy(CanonicalIVTy), Ctx(CanonicalIVTy->getContext()) {}
54+
VPTypeAnalysis(LLVMContext &Ctx) : Ctx(Ctx) {}
6055

6156
/// Infer the type of \p V. Returns the scalar type of \p V.
6257
Type *inferScalarType(const VPValue *V);

llvm/lib/Transforms/Vectorize/VPlanHelpers.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ struct VPCostContext {
379379
VPCostContext(const TargetTransformInfo &TTI, const TargetLibraryInfo &TLI,
380380
Type *CanIVTy, LoopVectorizationCostModel &CM,
381381
TargetTransformInfo::TargetCostKind CostKind)
382-
: TTI(TTI), TLI(TLI), Types(CanIVTy), LLVMCtx(CanIVTy->getContext()),
383-
CM(CM), CostKind(CostKind) {}
382+
: TTI(TTI), TLI(TLI), Types(CanIVTy->getContext()),
383+
LLVMCtx(CanIVTy->getContext()), CM(CM), CostKind(CostKind) {}
384384

385385
/// Return the cost for \p UI with \p VF using the legacy cost model as
386386
/// fallback until computing the cost of all recipes migrates to VPlan.

llvm/lib/Transforms/Vectorize/VPlanTransforms.cpp

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ createScalarIVSteps(VPlan &Plan, InductionDescriptor::InductionKind Kind,
549549

550550
// Truncate base induction if needed.
551551
Type *CanonicalIVType = CanonicalIV->getScalarType();
552-
VPTypeAnalysis TypeInfo(CanonicalIVType);
552+
VPTypeAnalysis TypeInfo(CanonicalIVType->getContext());
553553
Type *ResultTy = TypeInfo.inferScalarType(BaseIV);
554554
if (TruncI) {
555555
Type *TruncTy = TruncI->getType();
@@ -795,13 +795,14 @@ optimizeLatchExitInductionUser(VPlan &Plan, VPTypeAnalysis &TypeInfo,
795795
void VPlanTransforms::optimizeInductionExitUsers(
796796
VPlan &Plan, DenseMap<VPValue *, VPValue *> &EndValues) {
797797
VPBlockBase *MiddleVPBB = Plan.getMiddleBlock();
798-
VPTypeAnalysis TypeInfo(Plan.getCanonicalIV()->getScalarType());
798+
VPTypeAnalysis TypeInfo(Plan.getCanonicalIV()->getScalarType()->getContext());
799799
for (VPIRBasicBlock *ExitVPBB : Plan.getExitBlocks()) {
800800
for (VPRecipeBase &R : *ExitVPBB) {
801801
auto *ExitIRI = cast<VPIRInstruction>(&R);
802802
if (!isa<PHINode>(ExitIRI->getInstruction()))
803803
break;
804804

805+
805806
for (auto [Idx, PredVPBB] : enumerate(ExitVPBB->getPredecessors())) {
806807
if (PredVPBB == MiddleVPBB)
807808
if (VPValue *Escape = optimizeLatchExitInductionUser(
@@ -957,8 +958,11 @@ static void simplifyRecipe(VPRecipeBase &R, VPTypeAnalysis &TypeInfo) {
957958
#ifndef NDEBUG
958959
// Verify that the cached type info is for both A and its users is still
959960
// accurate by comparing it to freshly computed types.
960-
VPTypeAnalysis TypeInfo2(
961-
R.getParent()->getPlan()->getCanonicalIV()->getScalarType());
961+
VPTypeAnalysis TypeInfo2(R.getParent()
962+
->getPlan()
963+
->getCanonicalIV()
964+
->getScalarType()
965+
->getContext());
962966
assert(TypeInfo.inferScalarType(A) == TypeInfo2.inferScalarType(A));
963967
for (VPUser *U : A->users()) {
964968
auto *R = cast<VPRecipeBase>(U);
@@ -1001,7 +1005,7 @@ static void simplifyRecipe(VPRecipeBase &R, VPTypeAnalysis &TypeInfo) {
10011005
void VPlanTransforms::simplifyRecipes(VPlan &Plan, Type &CanonicalIVTy) {
10021006
ReversePostOrderTraversal<VPBlockDeepTraversalWrapper<VPBlockBase *>> RPOT(
10031007
Plan.getEntry());
1004-
VPTypeAnalysis TypeInfo(&CanonicalIVTy);
1008+
VPTypeAnalysis TypeInfo(CanonicalIVTy.getContext());
10051009
for (VPBasicBlock *VPBB : VPBlockUtils::blocksOnly<VPBasicBlock>(RPOT)) {
10061010
for (VPRecipeBase &R : make_early_inc_range(*VPBB)) {
10071011
simplifyRecipe(R, TypeInfo);
@@ -1351,7 +1355,7 @@ void VPlanTransforms::truncateToMinimalBitwidths(
13511355
// typed.
13521356
DenseMap<VPValue *, VPWidenCastRecipe *> ProcessedTruncs;
13531357
Type *CanonicalIVType = Plan.getCanonicalIV()->getScalarType();
1354-
VPTypeAnalysis TypeInfo(CanonicalIVType);
1358+
VPTypeAnalysis TypeInfo(CanonicalIVType->getContext());
13551359
VPBasicBlock *PH = Plan.getVectorPreheader();
13561360
for (VPBasicBlock *VPBB : VPBlockUtils::blocksOnly<VPBasicBlock>(
13571361
vp_depth_first_deep(Plan.getVectorLoopRegion()))) {
@@ -1743,8 +1747,8 @@ static VPRecipeBase *createEVLRecipe(VPValue *HeaderMask,
17431747
/// Replace recipes with their EVL variants.
17441748
static void transformRecipestoEVLRecipes(VPlan &Plan, VPValue &EVL) {
17451749
Type *CanonicalIVType = Plan.getCanonicalIV()->getScalarType();
1746-
VPTypeAnalysis TypeInfo(CanonicalIVType);
17471750
LLVMContext &Ctx = CanonicalIVType->getContext();
1751+
VPTypeAnalysis TypeInfo(Ctx);
17481752
VPValue *AllOneMask = Plan.getOrAddLiveIn(ConstantInt::getTrue(Ctx));
17491753
VPRegionBlock *LoopRegion = Plan.getVectorLoopRegion();
17501754
VPBasicBlock *Header = LoopRegion->getEntryBasicBlock();

llvm/lib/Transforms/Vectorize/VPlanUnroll.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class UnrollState {
7373

7474
public:
7575
UnrollState(VPlan &Plan, unsigned UF, LLVMContext &Ctx)
76-
: Plan(Plan), UF(UF), TypeInfo(Plan.getCanonicalIV()->getScalarType()) {}
76+
: Plan(Plan), UF(UF), TypeInfo(Ctx) {}
7777

7878
void unrollBlock(VPBlockBase *VPB);
7979

llvm/lib/Transforms/Vectorize/VPlanValue.h

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,22 @@ class VPValue {
6161
SmallVector<VPUser *, 1> Users;
6262

6363
protected:
64-
// Hold the underlying Value, if any, attached to this VPValue.
65-
Value *UnderlyingVal;
64+
union {
65+
// Hold the underlying Value, if any, attached to this non-symbolic VPValue.
66+
Value *UnderlyingVal;
67+
// Hold the type of this VPValue, if it is symbolic.
68+
Type *Ty;
69+
};
6670

6771
/// Pointer to the VPDef that defines this VPValue. If it is nullptr, the
6872
/// VPValue is not defined by any recipe modeled in VPlan.
6973
VPDef *Def;
7074

7175
VPValue(const unsigned char SC, Value *UV = nullptr, VPDef *Def = nullptr);
7276

73-
/// Create a live-in VPValue.
74-
VPValue(Value *UV = nullptr) : VPValue(VPValueSC, UV, nullptr) {}
77+
/// Create a live-in IR VPValue.
78+
VPValue(Value *UV) : VPValue(VPValueSC, UV, nullptr) {}
79+
VPValue(Type *Ty) : SubclassID(VPSymbolicValueSC), Ty(Ty), Def(nullptr) {}
7580
/// Create a VPValue for a \p Def which is a subclass of VPValue.
7681
VPValue(VPDef *Def, Value *UV = nullptr) : VPValue(VPVRecipeSC, UV, Def) {}
7782
/// Create a VPValue for a \p Def which defines multiple values.
@@ -86,14 +91,18 @@ class VPValue {
8691

8792
public:
8893
/// Return the underlying Value attached to this VPValue.
89-
Value *getUnderlyingValue() const { return UnderlyingVal; }
94+
Value *getUnderlyingValue() const {
95+
return SubclassID == VPSymbolicValueSC ? nullptr : UnderlyingVal;
96+
}
9097

9198
/// An enumeration for keeping track of the concrete subclass of VPValue that
9299
/// are actually instantiated.
93100
enum {
94-
VPValueSC, /// A generic VPValue, like live-in values or defined by a recipe
95-
/// that defines multiple values.
96-
VPVRecipeSC /// A VPValue sub-class that is a VPRecipeBase.
101+
VPValueSC, /// A generic non-symbolic VPValue, like live-in IR values or
102+
/// defined by a recipe that defines multiple values.
103+
VPSymbolicValueSC, /// A generic VPValue, like live-in values or defined by
104+
/// a recipe that defines multiple values.
105+
VPVRecipeSC /// A VPValue sub-class that is a VPRecipeBase.
97106
};
98107

99108
VPValue(const VPValue &) = delete;
@@ -172,6 +181,10 @@ class VPValue {
172181
/// Returns true if this VPValue is a live-in, i.e. defined outside the VPlan.
173182
bool isLiveIn() const { return !hasDefiningRecipe(); }
174183

184+
bool isSymbolic() const { return SubclassID == VPSymbolicValueSC; }
185+
186+
Type *getType() const;
187+
175188
/// Returns the underlying IR value, if this VPValue is defined outside the
176189
/// scope of VPlan. Returns nullptr if the VPValue is defined by a VPDef
177190
/// inside a VPlan.

llvm/lib/Transforms/Vectorize/VPlanVerifier.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,10 @@ bool VPlanVerifier::verify(const VPlan &Plan) {
420420
bool llvm::verifyVPlanIsValid(const VPlan &Plan) {
421421
VPDominatorTree VPDT;
422422
VPDT.recalculate(const_cast<VPlan &>(Plan));
423-
VPTypeAnalysis TypeInfo(
424-
const_cast<VPlan &>(Plan).getCanonicalIV()->getScalarType());
423+
VPTypeAnalysis TypeInfo(const_cast<VPlan &>(Plan)
424+
.getCanonicalIV()
425+
->getScalarType()
426+
->getContext());
425427
VPlanVerifier Verifier(VPDT, TypeInfo);
426428
return Verifier.verify(Plan);
427429
}

0 commit comments

Comments
 (0)