Skip to content

Commit d985670

Browse files
committed
[LV] Use VPReductionRecipe for partial reductions
Partial reductions can easily be represented by the VPReductionRecipe class by setting their scale factor to something greater than 1. This PR merges the two together and gives VPReductionRecipe a VFScaleFactor so that it can choose to generate the partial reduction intrinsic at execute time. Depends on llvm#144281
1 parent 42359e5 commit d985670

18 files changed

+1152
-1452
lines changed

llvm/include/llvm/Analysis/TargetTransformInfo.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ class TargetTransformInfo {
223223
/// Get the kind of extension that an instruction represents.
224224
LLVM_ABI static PartialReductionExtendKind
225225
getPartialReductionExtendKind(Instruction *I);
226+
LLVM_ABI static PartialReductionExtendKind
227+
getPartialReductionExtendKind(Instruction::CastOps CastOpc);
226228

227229
/// Construct a TTI object using a type implementing the \c Concept
228230
/// API below.

llvm/lib/Analysis/TargetTransformInfo.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,11 +1000,22 @@ InstructionCost TargetTransformInfo::getShuffleCost(
10001000
}
10011001

10021002
TargetTransformInfo::PartialReductionExtendKind
1003-
TargetTransformInfo::getPartialReductionExtendKind(Instruction *I) {
1004-
if (isa<SExtInst>(I))
1005-
return PR_SignExtend;
1006-
if (isa<ZExtInst>(I))
1003+
TargetTransformInfo::getPartialReductionExtendKind(
1004+
Instruction::CastOps CastOpc) {
1005+
switch (CastOpc) {
1006+
case Instruction::CastOps::ZExt:
10071007
return PR_ZeroExtend;
1008+
case Instruction::CastOps::SExt:
1009+
return PR_SignExtend;
1010+
default:
1011+
return PR_None;
1012+
}
1013+
}
1014+
1015+
TargetTransformInfo::PartialReductionExtendKind
1016+
TargetTransformInfo::getPartialReductionExtendKind(Instruction *I) {
1017+
if (auto *Cast = dyn_cast<CastInst>(I))
1018+
return getPartialReductionExtendKind(Cast->getOpcode());
10081019
return PR_None;
10091020
}
10101021

llvm/lib/Transforms/Vectorize/LoopVectorize.cpp

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7050,7 +7050,8 @@ static bool planContainsAdditionalSimplifications(VPlan &Plan,
70507050
}
70517051
// The VPlan-based cost model is more accurate for partial reduction and
70527052
// comparing against the legacy cost isn't desirable.
7053-
if (isa<VPPartialReductionRecipe>(&R))
7053+
if (auto *VPR = dyn_cast<VPReductionRecipe>(&R); VPR &&
7054+
VPR->isPartialReduction())
70547055
return true;
70557056

70567057
/// If a VPlan transform folded a recipe to one producing a single-scalar,
@@ -8280,11 +8281,14 @@ VPRecipeBase *VPRecipeBuilder::tryToCreateWidenRecipe(VPSingleDefRecipe *R,
82808281
Phi->getIncomingValueForBlock(OrigLoop->getLoopPreheader()));
82818282

82828283
// If the PHI is used by a partial reduction, set the scale factor.
8283-
unsigned ScaleFactor =
8284-
getScalingForReduction(RdxDesc.getLoopExitInstr()).value_or(1);
8285-
PhiRecipe = new VPReductionPHIRecipe(
8286-
Phi, RdxDesc, *StartV, CM.isInLoopReduction(Phi),
8287-
CM.useOrderedReductions(RdxDesc), ScaleFactor);
8284+
bool UseInLoopReduction = CM.isInLoopReduction(Phi);
8285+
bool UseOrderedReductions = CM.useOrderedReductions(RdxDesc);
8286+
auto ScaleFactor =
8287+
(UseOrderedReductions || UseInLoopReduction)
8288+
? 0
8289+
: getScalingForReduction(RdxDesc.getLoopExitInstr()).value_or(1);
8290+
PhiRecipe = new VPReductionPHIRecipe(Phi, RdxDesc, *StartV,
8291+
UseOrderedReductions, ScaleFactor);
82888292
} else {
82898293
// TODO: Currently fixed-order recurrences are modeled as chains of
82908294
// first-order recurrences. If there are no users of the intermediate
@@ -8348,7 +8352,8 @@ VPRecipeBuilder::tryToCreatePartialReduction(Instruction *Reduction,
83488352
VPValue *Accumulator = Operands[1];
83498353
VPRecipeBase *BinOpRecipe = BinOp->getDefiningRecipe();
83508354
if (isa<VPReductionPHIRecipe>(BinOpRecipe) ||
8351-
isa<VPPartialReductionRecipe>(BinOpRecipe))
8355+
(isa<VPReductionRecipe>(BinOpRecipe) &&
8356+
cast<VPReductionRecipe>(BinOpRecipe)->isPartialReduction()))
83528357
std::swap(BinOp, Accumulator);
83538358

83548359
unsigned ReductionOpcode = Reduction->getOpcode();
@@ -8369,12 +8374,10 @@ VPRecipeBuilder::tryToCreatePartialReduction(Instruction *Reduction,
83698374
"Expected an ADD or SUB operation for predicated partial "
83708375
"reductions (because the neutral element in the mask is zero)!");
83718376
Cond = getBlockInMask(Builder.getInsertBlock());
8372-
VPValue *Zero =
8373-
Plan.getOrAddLiveIn(ConstantInt::get(Reduction->getType(), 0));
8374-
BinOp = Builder.createSelect(Cond, BinOp, Zero, Reduction->getDebugLoc());
83758377
}
8376-
return new VPPartialReductionRecipe(ReductionOpcode, Accumulator, BinOp, Cond,
8377-
ScaleFactor, Reduction);
8378+
8379+
return new VPReductionRecipe(RecurKind::Add, FastMathFlags(), Reduction,
8380+
Accumulator, BinOp, Cond, false, ScaleFactor);
83788381
}
83798382

83808383
void LoopVectorizationPlanner::buildVPlansWithVPRecipes(ElementCount MinVF,
@@ -9154,9 +9157,11 @@ void LoopVectorizationPlanner::adjustRecipesForReductions(
91549157
FastMathFlags FMFs = isa<FPMathOperator>(CurrentLinkI)
91559158
? RdxDesc.getFastMathFlags()
91569159
: FastMathFlags();
9160+
bool UseOrderedReductions = CM.useOrderedReductions(RdxDesc);
9161+
unsigned VFScaleFactor = !UseOrderedReductions;
91579162
auto *RedRecipe = new VPReductionRecipe(
91589163
Kind, FMFs, CurrentLinkI, PreviousLink, VecOp, CondOp,
9159-
CM.useOrderedReductions(RdxDesc), CurrentLinkI->getDebugLoc());
9164+
UseOrderedReductions, VFScaleFactor, CurrentLinkI->getDebugLoc());
91609165
// Append the recipe to the end of the VPBasicBlock because we need to
91619166
// ensure that it comes after all of it's inputs, including CondOp.
91629167
// Delete CurrentLink as it will be invalid if its operand is replaced
@@ -9190,8 +9195,9 @@ void LoopVectorizationPlanner::adjustRecipesForReductions(
91909195
// Don't output selects for partial reductions because they have an output
91919196
// with fewer lanes than the VF. So the operands of the select would have
91929197
// different numbers of lanes. Partial reductions mask the input instead.
9198+
auto *RR = dyn_cast<VPReductionRecipe>(OrigExitingVPV->getDefiningRecipe());
91939199
if (!PhiR->isInLoop() && CM.foldTailByMasking() &&
9194-
!isa<VPPartialReductionRecipe>(OrigExitingVPV->getDefiningRecipe())) {
9200+
(!RR || !RR->isPartialReduction())) {
91959201
VPValue *Cond = RecipeBuilder.getBlockInMask(PhiR->getParent());
91969202
std::optional<FastMathFlags> FMFs =
91979203
PhiTy->isFloatingPointTy()

llvm/lib/Transforms/Vectorize/VPlan.h

Lines changed: 50 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,6 @@ class VPSingleDefRecipe : public VPRecipeBase, public VPValue {
552552
case VPRecipeBase::VPWidenIntOrFpInductionSC:
553553
case VPRecipeBase::VPWidenPointerInductionSC:
554554
case VPRecipeBase::VPReductionPHISC:
555-
case VPRecipeBase::VPPartialReductionSC:
556555
return true;
557556
case VPRecipeBase::VPBranchOnMaskSC:
558557
case VPRecipeBase::VPInterleaveSC:
@@ -2182,34 +2181,37 @@ class VPReductionPHIRecipe : public VPHeaderPHIRecipe,
21822181
/// Descriptor for the reduction.
21832182
const RecurrenceDescriptor &RdxDesc;
21842183

2185-
/// The phi is part of an in-loop reduction.
2186-
bool IsInLoop;
2187-
21882184
/// The phi is part of an ordered reduction. Requires IsInLoop to be true.
21892185
bool IsOrdered;
21902186

2191-
/// When expanding the reduction PHI, the plan's VF element count is divided
2192-
/// by this factor to form the reduction phi's VF.
2193-
unsigned VFScaleFactor = 1;
2187+
/// The scaling factor, relative to the VF, that this recipe's output is
2188+
/// divided by.
2189+
/// For outer-loop reductions this is equal to 1.
2190+
/// For in-loop reductions this is equal to 0, to specify that this is equal
2191+
/// to the VF (which may not be known yet). For partial-reductions this is
2192+
/// equal to another scalar value.
2193+
unsigned VFScaleFactor;
21942194

21952195
public:
21962196
/// Create a new VPReductionPHIRecipe for the reduction \p Phi described by \p
21972197
/// RdxDesc.
21982198
VPReductionPHIRecipe(PHINode *Phi, const RecurrenceDescriptor &RdxDesc,
2199-
VPValue &Start, bool IsInLoop = false,
2200-
bool IsOrdered = false, unsigned VFScaleFactor = 1)
2199+
VPValue &Start, bool IsOrdered = false,
2200+
unsigned VFScaleFactor = 1)
22012201
: VPHeaderPHIRecipe(VPDef::VPReductionPHISC, Phi, &Start),
2202-
RdxDesc(RdxDesc), IsInLoop(IsInLoop), IsOrdered(IsOrdered),
2203-
VFScaleFactor(VFScaleFactor) {
2204-
assert((!IsOrdered || IsInLoop) && "IsOrdered requires IsInLoop");
2202+
RdxDesc(RdxDesc), IsOrdered(IsOrdered), VFScaleFactor(VFScaleFactor) {
2203+
assert((!IsOrdered || isInLoop()) &&
2204+
"IsOrdered requires the reduction to be in-loop");
2205+
assert(((!isInLoop() && !IsOrdered) || isInLoop()) &&
2206+
"Invalid VFScaleFactor");
22052207
}
22062208

22072209
~VPReductionPHIRecipe() override = default;
22082210

22092211
VPReductionPHIRecipe *clone() override {
2210-
auto *R = new VPReductionPHIRecipe(cast<PHINode>(getUnderlyingInstr()),
2211-
RdxDesc, *getOperand(0), IsInLoop,
2212-
IsOrdered, VFScaleFactor);
2212+
auto *R =
2213+
new VPReductionPHIRecipe(cast<PHINode>(getUnderlyingInstr()), RdxDesc,
2214+
*getOperand(0), IsOrdered, VFScaleFactor);
22132215
R->addOperand(getBackedgeValue());
22142216
return R;
22152217
}
@@ -2235,8 +2237,10 @@ class VPReductionPHIRecipe : public VPHeaderPHIRecipe,
22352237
/// Returns true, if the phi is part of an ordered reduction.
22362238
bool isOrdered() const { return IsOrdered; }
22372239

2238-
/// Returns true, if the phi is part of an in-loop reduction.
2239-
bool isInLoop() const { return IsInLoop; }
2240+
/// Returns true if the phi is part of an in-loop reduction.
2241+
bool isInLoop() const { return VFScaleFactor == 0; }
2242+
2243+
bool isPartialReduction() const { return VFScaleFactor > 1; }
22402244

22412245
/// Returns true if the recipe only uses the first lane of operand \p Op.
22422246
bool onlyFirstLaneUsed(const VPValue *Op) const override {
@@ -2409,23 +2413,32 @@ class VPInterleaveRecipe : public VPRecipeBase {
24092413
Instruction *getInsertPos() const { return IG->getInsertPos(); }
24102414
};
24112415

2412-
/// A recipe to represent inloop reduction operations, performing a reduction on
2413-
/// a vector operand into a scalar value, and adding the result to a chain.
2414-
/// The Operands are {ChainOp, VecOp, [Condition]}.
2416+
/// A recipe to represent inloop, ordered or partial reduction operations. It
2417+
/// performs a reduction on a vector operand into a scalar (vector in the case
2418+
/// of a partial reduction) value, and adds the result to a chain. The Operands
2419+
/// are {ChainOp, VecOp, [Condition]}.
24152420
class VPReductionRecipe : public VPRecipeWithIRFlags {
24162421
/// The recurrence kind for the reduction in question.
24172422
RecurKind RdxKind;
24182423
bool IsOrdered;
24192424
/// Whether the reduction is conditional.
24202425
bool IsConditional = false;
2426+
/// The scaling factor, relative to the VF, that this recipe's output is
2427+
/// divided by.
2428+
/// For outer-loop reductions this is equal to 1.
2429+
/// For in-loop reductions this is equal to 0, to specify that this is equal
2430+
/// to the VF (which may not be known yet).
2431+
/// For partial-reductions this is equal to another scalar value.
2432+
unsigned VFScaleFactor;
24212433

24222434
protected:
24232435
VPReductionRecipe(const unsigned char SC, RecurKind RdxKind,
24242436
FastMathFlags FMFs, Instruction *I,
24252437
ArrayRef<VPValue *> Operands, VPValue *CondOp,
2426-
bool IsOrdered, DebugLoc DL)
2438+
bool IsOrdered, unsigned VFScaleFactor, DebugLoc DL)
24272439
: VPRecipeWithIRFlags(SC, Operands, FMFs, DL), RdxKind(RdxKind),
2428-
IsOrdered(IsOrdered) {
2440+
IsOrdered(IsOrdered), VFScaleFactor(VFScaleFactor) {
2441+
assert((!IsOrdered || VFScaleFactor == 0) && "Invalid scale factor");
24292442
if (CondOp) {
24302443
IsConditional = true;
24312444
addOperand(CondOp);
@@ -2436,24 +2449,24 @@ class VPReductionRecipe : public VPRecipeWithIRFlags {
24362449
public:
24372450
VPReductionRecipe(RecurKind RdxKind, FastMathFlags FMFs, Instruction *I,
24382451
VPValue *ChainOp, VPValue *VecOp, VPValue *CondOp,
2439-
bool IsOrdered, DebugLoc DL = {})
2452+
bool IsOrdered, unsigned VFScaleFactor, DebugLoc DL = {})
24402453
: VPReductionRecipe(VPDef::VPReductionSC, RdxKind, FMFs, I,
24412454
ArrayRef<VPValue *>({ChainOp, VecOp}), CondOp,
2442-
IsOrdered, DL) {}
2455+
IsOrdered, VFScaleFactor, DL) {}
24432456

24442457
VPReductionRecipe(const RecurKind RdxKind, FastMathFlags FMFs,
24452458
VPValue *ChainOp, VPValue *VecOp, VPValue *CondOp,
2446-
bool IsOrdered, DebugLoc DL = {})
2459+
bool IsOrdered, unsigned VFScaleFactor, DebugLoc DL = {})
24472460
: VPReductionRecipe(VPDef::VPReductionSC, RdxKind, FMFs, nullptr,
24482461
ArrayRef<VPValue *>({ChainOp, VecOp}), CondOp,
2449-
IsOrdered, DL) {}
2462+
IsOrdered, VFScaleFactor, DL) {}
24502463

24512464
~VPReductionRecipe() override = default;
24522465

24532466
VPReductionRecipe *clone() override {
2454-
return new VPReductionRecipe(RdxKind, getFastMathFlags(),
2455-
getUnderlyingInstr(), getChainOp(), getVecOp(),
2456-
getCondOp(), IsOrdered, getDebugLoc());
2467+
return new VPReductionRecipe(
2468+
RdxKind, getFastMathFlags(), getUnderlyingInstr(), getChainOp(),
2469+
getVecOp(), getCondOp(), IsOrdered, VFScaleFactor, getDebugLoc());
24572470
}
24582471

24592472
static inline bool classof(const VPRecipeBase *R) {
@@ -2485,6 +2498,8 @@ class VPReductionRecipe : public VPRecipeWithIRFlags {
24852498
bool isOrdered() const { return IsOrdered; };
24862499
/// Return true if the in-loop reduction is conditional.
24872500
bool isConditional() const { return IsConditional; };
2501+
/// Return true if the reduction is a partial reduction.
2502+
bool isPartialReduction() const { return VFScaleFactor > 1; }
24882503
/// The VPValue of the scalar Chain being accumulated.
24892504
VPValue *getChainOp() const { return getOperand(0); }
24902505
/// The VPValue of the vector value to be reduced.
@@ -2493,65 +2508,8 @@ class VPReductionRecipe : public VPRecipeWithIRFlags {
24932508
VPValue *getCondOp() const {
24942509
return isConditional() ? getOperand(getNumOperands() - 1) : nullptr;
24952510
}
2496-
};
2497-
2498-
/// A recipe for forming partial reductions. In the loop, an accumulator and
2499-
/// vector operand are added together and passed to the next iteration as the
2500-
/// next accumulator. After the loop body, the accumulator is reduced to a
2501-
/// scalar value.
2502-
class VPPartialReductionRecipe : public VPReductionRecipe {
2503-
unsigned Opcode;
2504-
2505-
/// The divisor by which the VF of this recipe's output should be divided
2506-
/// during execution.
2507-
unsigned VFScaleFactor;
2508-
2509-
public:
2510-
VPPartialReductionRecipe(Instruction *ReductionInst, VPValue *Op0,
2511-
VPValue *Op1, VPValue *Cond, unsigned VFScaleFactor)
2512-
: VPPartialReductionRecipe(ReductionInst->getOpcode(), Op0, Op1, Cond,
2513-
VFScaleFactor, ReductionInst) {}
2514-
VPPartialReductionRecipe(unsigned Opcode, VPValue *Op0, VPValue *Op1,
2515-
VPValue *Cond, unsigned ScaleFactor,
2516-
Instruction *ReductionInst = nullptr)
2517-
: VPReductionRecipe(VPDef::VPPartialReductionSC, RecurKind::Add,
2518-
FastMathFlags(), ReductionInst,
2519-
ArrayRef<VPValue *>({Op0, Op1}), Cond, false, {}),
2520-
Opcode(Opcode), VFScaleFactor(ScaleFactor) {
2521-
[[maybe_unused]] auto *AccumulatorRecipe =
2522-
getChainOp()->getDefiningRecipe();
2523-
assert((isa<VPReductionPHIRecipe>(AccumulatorRecipe) ||
2524-
isa<VPPartialReductionRecipe>(AccumulatorRecipe)) &&
2525-
"Unexpected operand order for partial reduction recipe");
2526-
}
2527-
~VPPartialReductionRecipe() override = default;
2528-
2529-
VPPartialReductionRecipe *clone() override {
2530-
return new VPPartialReductionRecipe(Opcode, getOperand(0), getOperand(1),
2531-
getCondOp(), VFScaleFactor,
2532-
getUnderlyingInstr());
2533-
}
2534-
2535-
VP_CLASSOF_IMPL(VPDef::VPPartialReductionSC)
2536-
2537-
/// Generate the reduction in the loop.
2538-
void execute(VPTransformState &State) override;
2539-
2540-
/// Return the cost of this VPPartialReductionRecipe.
2541-
InstructionCost computeCost(ElementCount VF,
2542-
VPCostContext &Ctx) const override;
2543-
2544-
/// Get the binary op's opcode.
2545-
unsigned getOpcode() const { return Opcode; }
2546-
25472511
/// Get the factor that the VF of this recipe's output should be scaled by.
25482512
unsigned getVFScaleFactor() const { return VFScaleFactor; }
2549-
2550-
#if !defined(NDEBUG) || defined(LLVM_ENABLE_DUMP)
2551-
/// Print the recipe.
2552-
void print(raw_ostream &O, const Twine &Indent,
2553-
VPSlotTracker &SlotTracker) const override;
2554-
#endif
25552513
};
25562514

25572515
/// A recipe to represent inloop reduction operations with vector-predication
@@ -2567,7 +2525,7 @@ class VPReductionEVLRecipe : public VPReductionRecipe {
25672525
R.getFastMathFlags(),
25682526
cast_or_null<Instruction>(R.getUnderlyingValue()),
25692527
ArrayRef<VPValue *>({R.getChainOp(), R.getVecOp(), &EVL}), CondOp,
2570-
R.isOrdered(), DL) {}
2528+
R.isOrdered(), 0, DL) {}
25712529

25722530
~VPReductionEVLRecipe() override = default;
25732531

@@ -2768,6 +2726,11 @@ class VPSingleDefBundleRecipe : public VPSingleDefRecipe {
27682726
VPWidenRecipe *Mul, VPReductionRecipe *Red)
27692727
: VPSingleDefBundleRecipe(BundleTypes::ExtMulAccumulateReduction,
27702728
{Ext0, Ext1, Mul, Red}) {}
2729+
VPSingleDefBundleRecipe(VPWidenCastRecipe *Ext0, VPWidenCastRecipe *Ext1,
2730+
VPWidenRecipe *Mul, VPWidenRecipe *Sub,
2731+
VPReductionRecipe *Red)
2732+
: VPSingleDefBundleRecipe(BundleTypes::ExtMulAccumulateReduction,
2733+
{Ext0, Ext1, Mul, Sub, Red}) {}
27712734

27722735
~VPSingleDefBundleRecipe() override {
27732736
SmallPtrSet<VPRecipeBase *, 4> Seen;

llvm/lib/Transforms/Vectorize/VPlanAnalysis.cpp

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,10 @@ Type *VPTypeAnalysis::inferScalarType(const VPValue *V) {
283283
[](const auto *R) { return R->getScalarType(); })
284284
.Case<VPReductionRecipe, VPPredInstPHIRecipe, VPWidenPHIRecipe,
285285
VPScalarIVStepsRecipe, VPWidenGEPRecipe, VPVectorPointerRecipe,
286-
VPVectorEndPointerRecipe, VPWidenCanonicalIVRecipe,
287-
VPPartialReductionRecipe>([this](const VPRecipeBase *R) {
288-
return inferScalarType(R->getOperand(0));
289-
})
286+
VPVectorEndPointerRecipe, VPWidenCanonicalIVRecipe>(
287+
[this](const VPRecipeBase *R) {
288+
return inferScalarType(R->getOperand(0));
289+
})
290290
// VPInstructionWithType must be handled before VPInstruction.
291291
.Case<VPInstructionWithType, VPWidenIntrinsicRecipe,
292292
VPWidenCastRecipe>(
@@ -397,7 +397,7 @@ bool VPDominatorTree::properlyDominates(const VPRecipeBase *A,
397397
static unsigned getVFScaleFactor(VPRecipeBase *R) {
398398
if (auto *RR = dyn_cast<VPReductionPHIRecipe>(R))
399399
return RR->getVFScaleFactor();
400-
if (auto *RR = dyn_cast<VPPartialReductionRecipe>(R))
400+
if (auto *RR = dyn_cast<VPReductionRecipe>(R))
401401
return RR->getVFScaleFactor();
402402
assert(
403403
(!isa<VPInstruction>(R) || cast<VPInstruction>(R)->getOpcode() !=
@@ -567,8 +567,9 @@ SmallVector<VPRegisterUsage, 8> llvm::calculateRegisterUsageForPlan(
567567
} else {
568568
// The output from scaled phis and scaled reductions actually has
569569
// fewer lanes than the VF.
570-
unsigned ScaleFactor = getVFScaleFactor(R);
571-
ElementCount VF = VFs[J].divideCoefficientBy(ScaleFactor);
570+
ElementCount VF = VFs[J];
571+
if (unsigned ScaleFactor = getVFScaleFactor(R))
572+
VF = VF.divideCoefficientBy(ScaleFactor);
572573
LLVM_DEBUG(if (VF != VFs[J]) {
573574
dbgs() << "LV(REG): Scaled down VF from " << VFs[J] << " to " << VF
574575
<< " for " << *R << "\n";

0 commit comments

Comments
 (0)