Commit 00741a00 by Andrew Scull

Improve use of CfgLocalAllocator and introduce containers that use it.

This doesn't make a big difference but does reduce the proportion of time spent in malloc and free. BUG= R=stichnot@chromium.org Review URL: https://codereview.chromium.org/1349833005 .
parent 385351ba
...@@ -121,6 +121,7 @@ ifdef ASAN ...@@ -121,6 +121,7 @@ ifdef ASAN
endif endif
ifdef MSAN ifdef MSAN
# TODO(ascull): this has an as yet undiagnosed uninitialized memory access
OBJDIR := $(OBJDIR)+MSan OBJDIR := $(OBJDIR)+MSan
CXX_EXTRA += -fsanitize=memory CXX_EXTRA += -fsanitize=memory
LD_EXTRA += -fsanitize=memory LD_EXTRA += -fsanitize=memory
......
...@@ -314,13 +314,13 @@ void Cfg::advancedPhiLowering() { ...@@ -314,13 +314,13 @@ void Cfg::advancedPhiLowering() {
void Cfg::reorderNodes() { void Cfg::reorderNodes() {
// TODO(ascull): it would be nice if the switch tests were always followed by // TODO(ascull): it would be nice if the switch tests were always followed by
// the default case to allow for fall through. // the default case to allow for fall through.
using PlacedList = std::list<CfgNode *>; using PlacedList = CfgList<CfgNode *>;
PlacedList Placed; // Nodes with relative placement locked down PlacedList Placed; // Nodes with relative placement locked down
PlacedList Unreachable; // Unreachable nodes PlacedList Unreachable; // Unreachable nodes
PlacedList::iterator NoPlace = Placed.end(); PlacedList::iterator NoPlace = Placed.end();
// Keep track of where each node has been tentatively placed so that we can // Keep track of where each node has been tentatively placed so that we can
// manage insertions into the middle. // manage insertions into the middle.
std::vector<PlacedList::iterator> PlaceIndex(Nodes.size(), NoPlace); CfgVector<PlacedList::iterator> PlaceIndex(Nodes.size(), NoPlace);
for (CfgNode *Node : Nodes) { for (CfgNode *Node : Nodes) {
// The "do ... while(0);" construct is to factor out the --PlaceIndex and // The "do ... while(0);" construct is to factor out the --PlaceIndex and
// assert() statements before moving to the next node. // assert() statements before moving to the next node.
......
...@@ -273,7 +273,7 @@ private: ...@@ -273,7 +273,7 @@ private:
std::unique_ptr<Assembler> TargetAssembler; std::unique_ptr<Assembler> TargetAssembler;
/// Globals required by this CFG. Mostly used for the profiler's globals. /// Globals required by this CFG. Mostly used for the profiler's globals.
std::unique_ptr<VariableDeclarationList> GlobalInits; std::unique_ptr<VariableDeclarationList> GlobalInits;
std::vector<InstJumpTable *> JumpTables; CfgVector<InstJumpTable *> JumpTables;
/// CurrentNode is maintained during dumping/emitting just for validating /// CurrentNode is maintained during dumping/emitting just for validating
/// Variable::DefNode. Normally, a traversal over CfgNodes maintains this, but /// Variable::DefNode. Normally, a traversal over CfgNodes maintains this, but
......
...@@ -827,7 +827,7 @@ namespace { ...@@ -827,7 +827,7 @@ namespace {
// Helper functions for emit(). // Helper functions for emit().
void emitRegisterUsage(Ostream &Str, const Cfg *Func, const CfgNode *Node, void emitRegisterUsage(Ostream &Str, const Cfg *Func, const CfgNode *Node,
bool IsLiveIn, std::vector<SizeT> &LiveRegCount) { bool IsLiveIn, CfgVector<SizeT> &LiveRegCount) {
if (!BuildDefs::dump()) if (!BuildDefs::dump())
return; return;
Liveness *Liveness = Func->getLiveness(); Liveness *Liveness = Func->getLiveness();
...@@ -840,7 +840,7 @@ void emitRegisterUsage(Ostream &Str, const Cfg *Func, const CfgNode *Node, ...@@ -840,7 +840,7 @@ void emitRegisterUsage(Ostream &Str, const Cfg *Func, const CfgNode *Node,
Str << "\t\t\t\t# LiveOut="; Str << "\t\t\t\t# LiveOut=";
} }
if (!Live->empty()) { if (!Live->empty()) {
std::vector<Variable *> LiveRegs; CfgVector<Variable *> LiveRegs;
for (SizeT i = 0; i < Live->size(); ++i) { for (SizeT i = 0; i < Live->size(); ++i) {
if ((*Live)[i]) { if ((*Live)[i]) {
Variable *Var = Liveness->getVariable(i, Node); Variable *Var = Liveness->getVariable(i, Node);
...@@ -869,7 +869,7 @@ void emitRegisterUsage(Ostream &Str, const Cfg *Func, const CfgNode *Node, ...@@ -869,7 +869,7 @@ void emitRegisterUsage(Ostream &Str, const Cfg *Func, const CfgNode *Node,
} }
void emitLiveRangesEnded(Ostream &Str, const Cfg *Func, const Inst *Instr, void emitLiveRangesEnded(Ostream &Str, const Cfg *Func, const Inst *Instr,
std::vector<SizeT> &LiveRegCount) { CfgVector<SizeT> &LiveRegCount) {
if (!BuildDefs::dump()) if (!BuildDefs::dump())
return; return;
bool First = true; bool First = true;
...@@ -935,7 +935,7 @@ void CfgNode::emit(Cfg *Func) const { ...@@ -935,7 +935,7 @@ void CfgNode::emit(Cfg *Func) const {
// each register is assigned to. Normally that would be only 0 or 1, but the // each register is assigned to. Normally that would be only 0 or 1, but the
// register allocator's AllowOverlap inference allows it to be greater than 1 // register allocator's AllowOverlap inference allows it to be greater than 1
// for short periods. // for short periods.
std::vector<SizeT> LiveRegCount(Func->getTarget()->getNumRegisters()); CfgVector<SizeT> LiveRegCount(Func->getTarget()->getNumRegisters());
if (DecorateAsm) { if (DecorateAsm) {
constexpr bool IsLiveIn = true; constexpr bool IsLiveIn = true;
emitRegisterUsage(Str, Func, this, IsLiveIn, LiveRegCount); emitRegisterUsage(Str, Func, this, IsLiveIn, LiveRegCount);
......
...@@ -145,10 +145,14 @@ using InstList = llvm::ilist<Inst>; ...@@ -145,10 +145,14 @@ using InstList = llvm::ilist<Inst>;
using PhiList = InstList; using PhiList = InstList;
using AssignList = InstList; using AssignList = InstList;
// Standard library containers with CfgLocalAllocator.
template <typename T> using CfgVector = std::vector<T, CfgLocalAllocator<T>>;
template <typename T> using CfgList = std::list<T, CfgLocalAllocator<T>>;
// Containers that are arena-allocated from the Cfg's allocator. // Containers that are arena-allocated from the Cfg's allocator.
using OperandList = std::vector<Operand *, CfgLocalAllocator<Operand *>>; using OperandList = CfgVector<Operand *>;
using VarList = std::vector<Variable *, CfgLocalAllocator<Variable *>>; using VarList = CfgVector<Variable *>;
using NodeList = std::vector<CfgNode *, CfgLocalAllocator<CfgNode *>>; using NodeList = CfgVector<CfgNode *>;
// Contains that use the default (global) allocator. // Contains that use the default (global) allocator.
using ConstantList = std::vector<Constant *>; using ConstantList = std::vector<Constant *>;
...@@ -168,8 +172,7 @@ using InstNumberT = int32_t; ...@@ -168,8 +172,7 @@ using InstNumberT = int32_t;
/// value, giving the instruction number that begins or ends a variable's live /// value, giving the instruction number that begins or ends a variable's live
/// range. /// range.
using LiveBeginEndMapEntry = std::pair<SizeT, InstNumberT>; using LiveBeginEndMapEntry = std::pair<SizeT, InstNumberT>;
using LiveBeginEndMap = using LiveBeginEndMap = CfgVector<LiveBeginEndMapEntry>;
std::vector<LiveBeginEndMapEntry, CfgLocalAllocator<LiveBeginEndMapEntry>>;
using LivenessBV = llvm::BitVector; using LivenessBV = llvm::BitVector;
using TimerStackIdT = uint32_t; using TimerStackIdT = uint32_t;
......
...@@ -722,10 +722,8 @@ GlobalContext::~GlobalContext() { ...@@ -722,10 +722,8 @@ GlobalContext::~GlobalContext() {
llvm::DeleteContainerPointers(AllThreadContexts); llvm::DeleteContainerPointers(AllThreadContexts);
LockedPtr<DestructorArray> Dtors = getDestructors(); LockedPtr<DestructorArray> Dtors = getDestructors();
// Destructors are invoked in the opposite object construction order. // Destructors are invoked in the opposite object construction order.
for (auto DtorIter = Dtors->crbegin(); DtorIter != Dtors->crend(); for (const auto &Dtor : reverse_range(*Dtors))
++DtorIter) { Dtor();
(*DtorIter)();
}
} }
// TODO(stichnot): Consider adding thread-local caches of constant pool entries // TODO(stichnot): Consider adding thread-local caches of constant pool entries
......
...@@ -161,10 +161,7 @@ public: ...@@ -161,10 +161,7 @@ public:
void dumpDest(const Cfg *Func) const; void dumpDest(const Cfg *Func) const;
virtual bool isRedundantAssign() const { return false; } virtual bool isRedundantAssign() const { return false; }
// TODO(jpp): Insts should not have non-trivial destructors, but they ~Inst() = default;
// currently do. This dtor is marked final as a multi-step refactor that
// will eventually fix this problem.
virtual ~Inst() = default;
protected: protected:
Inst(Cfg *Func, InstKind Kind, SizeT MaxSrcs, Variable *Dest); Inst(Cfg *Func, InstKind Kind, SizeT MaxSrcs, Variable *Dest);
......
...@@ -47,7 +47,7 @@ class Liveness { ...@@ -47,7 +47,7 @@ class Liveness {
// LiveToVarMap maps a liveness bitvector index to a Variable. This is // LiveToVarMap maps a liveness bitvector index to a Variable. This is
// generally just for printing/dumping. The index should be less than // generally just for printing/dumping. The index should be less than
// NumLocals + Liveness::NumGlobals. // NumLocals + Liveness::NumGlobals.
std::vector<Variable *> LiveToVarMap; CfgVector<Variable *> LiveToVarMap;
// LiveIn and LiveOut track the in- and out-liveness of the global // LiveIn and LiveOut track the in- and out-liveness of the global
// variables. The size of each vector is LivenessNode::NumGlobals. // variables. The size of each vector is LivenessNode::NumGlobals.
LivenessBV LiveIn, LiveOut; LivenessBV LiveIn, LiveOut;
...@@ -107,13 +107,13 @@ private: ...@@ -107,13 +107,13 @@ private:
LivenessMode Mode; LivenessMode Mode;
SizeT NumGlobals = 0; SizeT NumGlobals = 0;
/// Size of Nodes is Cfg::Nodes.size(). /// Size of Nodes is Cfg::Nodes.size().
std::vector<LivenessNode> Nodes; CfgVector<LivenessNode> Nodes;
/// VarToLiveMap maps a Variable's Variable::Number to its live index within /// VarToLiveMap maps a Variable's Variable::Number to its live index within
/// its basic block. /// its basic block.
std::vector<SizeT> VarToLiveMap; CfgVector<SizeT> VarToLiveMap;
/// LiveToVarMap is analogous to LivenessNode::LiveToVarMap, but for non-local /// LiveToVarMap is analogous to LivenessNode::LiveToVarMap, but for non-local
/// variables. /// variables.
std::vector<Variable *> LiveToVarMap; CfgVector<Variable *> LiveToVarMap;
/// RangeMask[Variable::Number] indicates whether we want to track that /// RangeMask[Variable::Number] indicates whether we want to track that
/// Variable's live range. /// Variable's live range.
llvm::BitVector RangeMask; llvm::BitVector RangeMask;
......
...@@ -88,9 +88,8 @@ private: ...@@ -88,9 +88,8 @@ private:
bool Deleted = false; bool Deleted = false;
}; };
using LoopNodeList = std::vector<LoopNode, CfgLocalAllocator<LoopNode>>; using LoopNodeList = CfgVector<LoopNode>;
using LoopNodePtrList = using LoopNodePtrList = CfgVector<LoopNode *>;
std::vector<LoopNode *, CfgLocalAllocator<LoopNode *>>;
/// Process the node as part as part of Tarjan's algorithm and return either a /// Process the node as part as part of Tarjan's algorithm and return either a
/// node to recurse into or nullptr when the node has been fully processed. /// node to recurse into or nullptr when the node has been fully processed.
......
...@@ -85,12 +85,13 @@ public: ...@@ -85,12 +85,13 @@ public:
} }
/// @} /// @}
~Operand() = default;
protected: protected:
Operand(OperandKind Kind, Type Ty) : Ty(Ty), Kind(Kind) { Operand(OperandKind Kind, Type Ty) : Ty(Ty), Kind(Kind) {
// It is undefined behavior to have a larger value in the enum // It is undefined behavior to have a larger value in the enum
assert(Kind <= kTarget_Max); assert(Kind <= kTarget_Max);
} }
virtual ~Operand() = default;
const Type Ty; const Type Ty;
const OperandKind Kind; const OperandKind Kind;
...@@ -354,7 +355,7 @@ public: ...@@ -354,7 +355,7 @@ public:
LiveRange() = default; LiveRange() = default;
/// Special constructor for building a kill set. The advantage is that we can /// Special constructor for building a kill set. The advantage is that we can
/// reserve the right amount of space in advance. /// reserve the right amount of space in advance.
explicit LiveRange(const std::vector<InstNumberT> &Kills) { explicit LiveRange(const CfgVector<InstNumberT> &Kills) {
Range.reserve(Kills.size()); Range.reserve(Kills.size());
for (InstNumberT I : Kills) for (InstNumberT I : Kills)
addSegment(I, I); addSegment(I, I);
...@@ -388,8 +389,7 @@ public: ...@@ -388,8 +389,7 @@ public:
private: private:
using RangeElementType = std::pair<InstNumberT, InstNumberT>; using RangeElementType = std::pair<InstNumberT, InstNumberT>;
/// RangeType is arena-allocated from the Cfg's allocator. /// RangeType is arena-allocated from the Cfg's allocator.
using RangeType = using RangeType = CfgVector<RangeElementType>;
std::vector<RangeElementType, CfgLocalAllocator<RangeElementType>>;
RangeType Range; RangeType Range;
/// TrimmedBegin is an optimization for the overlaps() computation. Since the /// TrimmedBegin is an optimization for the overlaps() computation. Since the
/// linear-scan algorithm always calls it as overlaps(Cur) and Cur advances /// linear-scan algorithm always calls it as overlaps(Cur) and Cur advances
...@@ -556,7 +556,7 @@ enum MetadataKind { ...@@ -556,7 +556,7 @@ enum MetadataKind {
VMK_SingleDefs, /// Track uses+defs, but only record single def VMK_SingleDefs, /// Track uses+defs, but only record single def
VMK_All /// Track uses+defs, including full def list VMK_All /// Track uses+defs, including full def list
}; };
using InstDefList = std::vector<const Inst *, CfgLocalAllocator<const Inst *>>; using InstDefList = CfgVector<const Inst *>;
/// VariableTracking tracks the metadata for a single variable. It is /// VariableTracking tracks the metadata for a single variable. It is
/// only meant to be used internally by VariablesMetadata. /// only meant to be used internally by VariablesMetadata.
...@@ -652,7 +652,7 @@ public: ...@@ -652,7 +652,7 @@ public:
private: private:
const Cfg *Func; const Cfg *Func;
MetadataKind Kind; MetadataKind Kind;
std::vector<VariableTracking> Metadata; CfgVector<VariableTracking> Metadata;
const static InstDefList NoDefinitions; const static InstDefList NoDefinitions;
}; };
......
...@@ -166,8 +166,8 @@ void LinearScan::initForInfOnly() { ...@@ -166,8 +166,8 @@ void LinearScan::initForInfOnly() {
// Iterate across all instructions and record the begin and end of the live // Iterate across all instructions and record the begin and end of the live
// range for each variable that is pre-colored or infinite weight. // range for each variable that is pre-colored or infinite weight.
std::vector<InstNumberT> LRBegin(Vars.size(), Inst::NumberSentinel); CfgVector<InstNumberT> LRBegin(Vars.size(), Inst::NumberSentinel);
std::vector<InstNumberT> LREnd(Vars.size(), Inst::NumberSentinel); CfgVector<InstNumberT> LREnd(Vars.size(), Inst::NumberSentinel);
for (CfgNode *Node : Func->getNodes()) { for (CfgNode *Node : Func->getNodes()) {
for (Inst &Inst : Node->getInsts()) { for (Inst &Inst : Node->getInsts()) {
if (Inst.isDeleted()) if (Inst.isDeleted())
......
...@@ -39,8 +39,8 @@ public: ...@@ -39,8 +39,8 @@ public:
static constexpr size_t REGS_SIZE = 32; static constexpr size_t REGS_SIZE = 32;
private: private:
using OrderedRanges = std::vector<Variable *>; using OrderedRanges = CfgVector<Variable *>;
using UnorderedRanges = std::vector<Variable *>; using UnorderedRanges = CfgVector<Variable *>;
class IterationState { class IterationState {
IterationState(const IterationState &) = delete; IterationState(const IterationState &) = delete;
...@@ -103,7 +103,7 @@ private: ...@@ -103,7 +103,7 @@ private:
/// faster processing. /// faster processing.
OrderedRanges UnhandledPrecolored; OrderedRanges UnhandledPrecolored;
UnorderedRanges Active, Inactive, Handled; UnorderedRanges Active, Inactive, Handled;
std::vector<InstNumberT> Kills; CfgVector<InstNumberT> Kills;
RegAllocKind Kind = RAK_Unknown; RegAllocKind Kind = RAK_Unknown;
/// RegUses[I] is the number of live ranges (variables) that register I is /// RegUses[I] is the number of live ranges (variables) that register I is
/// currently assigned to. It can be greater than 1 as a result of /// currently assigned to. It can be greater than 1 as a result of
......
...@@ -20,8 +20,7 @@ namespace Ice { ...@@ -20,8 +20,7 @@ namespace Ice {
class CaseCluster; class CaseCluster;
using CaseClusterArray = using CaseClusterArray = CfgVector<CaseCluster>;
std::vector<CaseCluster, CfgLocalAllocator<CaseCluster>>;
/// A cluster of cases can be tested by a common method during switch lowering. /// A cluster of cases can be tested by a common method during switch lowering.
class CaseCluster { class CaseCluster {
......
...@@ -116,7 +116,6 @@ void TargetX8632::lowerCall(const InstCall *Instr) { ...@@ -116,7 +116,6 @@ void TargetX8632::lowerCall(const InstCall *Instr) {
// the document "OS X ABI Function Call Guide" by Apple. // the document "OS X ABI Function Call Guide" by Apple.
NeedsStackAlignment = true; NeedsStackAlignment = true;
using OperandList = std::vector<Operand *>;
OperandList XmmArgs; OperandList XmmArgs;
OperandList StackArgs, StackArgLocations; OperandList StackArgs, StackArgLocations;
uint32_t ParameterAreaSizeBytes = 0; uint32_t ParameterAreaSizeBytes = 0;
......
...@@ -491,8 +491,8 @@ bool isSameMemAddressOperand(const Operand *A, const Operand *B) { ...@@ -491,8 +491,8 @@ bool isSameMemAddressOperand(const Operand *A, const Operand *B) {
template <class Machine> void TargetX86Base<Machine>::findRMW() { template <class Machine> void TargetX86Base<Machine>::findRMW() {
Func->dump("Before RMW"); Func->dump("Before RMW");
OstreamLocker L(Func->getContext()); if (Func->isVerbose(IceV_RMW))
Ostream &Str = Func->getContext()->getStrDump(); Func->getContext()->lockStr();
for (CfgNode *Node : Func->getNodes()) { for (CfgNode *Node : Func->getNodes()) {
// Walk through the instructions, considering each sequence of 3 // Walk through the instructions, considering each sequence of 3
// instructions, and look for the particular RMW pattern. Note that this // instructions, and look for the particular RMW pattern. Note that this
...@@ -510,9 +510,11 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() { ...@@ -510,9 +510,11 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() {
assert(!I1->isDeleted()); assert(!I1->isDeleted());
assert(!I2->isDeleted()); assert(!I2->isDeleted());
assert(!I3->isDeleted()); assert(!I3->isDeleted());
if (auto *Load = llvm::dyn_cast<InstLoad>(I1)) { auto *Load = llvm::dyn_cast<InstLoad>(I1);
if (auto *Arith = llvm::dyn_cast<InstArithmetic>(I2)) { auto *Arith = llvm::dyn_cast<InstArithmetic>(I2);
if (auto *Store = llvm::dyn_cast<InstStore>(I3)) { auto *Store = llvm::dyn_cast<InstStore>(I3);
if (!Load || !Arith || !Store)
continue;
// Look for: // Look for:
// a = Load addr // a = Load addr
// b = <op> a, other // b = <op> a, other
...@@ -524,25 +526,24 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() { ...@@ -524,25 +526,24 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() {
// RMW <op>, addr, other, x // RMW <op>, addr, other, x
// b = Store b, addr, x // b = Store b, addr, x
// Note that inferTwoAddress() makes sure setDestNonKillable() gets // Note that inferTwoAddress() makes sure setDestNonKillable() gets
// called on the updated Store instruction, to avoid liveness // called on the updated Store instruction, to avoid liveness problems
// problems later. // later.
// //
// With this transformation, the Store instruction acquires a Dest // With this transformation, the Store instruction acquires a Dest
// variable and is now subject to dead code elimination if there // variable and is now subject to dead code elimination if there are no
// are no more uses of "b". Variable "x" is a beacon for // more uses of "b". Variable "x" is a beacon for determining whether
// determining whether the Store instruction gets dead-code // the Store instruction gets dead-code eliminated. If the Store
// eliminated. If the Store instruction is eliminated, then it // instruction is eliminated, then it must be the case that the RMW
// must be the case that the RMW instruction ends x's live range, // instruction ends x's live range, and therefore the RMW instruction
// and therefore the RMW instruction will be retained and later // will be retained and later lowered. On the other hand, if the RMW
// lowered. On the other hand, if the RMW instruction does not end // instruction does not end x's live range, then the Store instruction
// x's live range, then the Store instruction must still be // must still be present, and therefore the RMW instruction is ignored
// present, and therefore the RMW instruction is ignored during // during lowering because it is redundant with the Store instruction.
// lowering because it is redundant with the Store instruction.
// //
// Note that if "a" has further uses, the RMW transformation may // Note that if "a" has further uses, the RMW transformation may still
// still trigger, resulting in two loads and one store, which is // trigger, resulting in two loads and one store, which is worse than the
// worse than the original one load and one store. However, this // original one load and one store. However, this is probably rare, and
// is probably rare, and caching probably keeps it just as fast. // caching probably keeps it just as fast.
if (!isSameMemAddressOperand<Machine>(Load->getSourceAddress(), if (!isSameMemAddressOperand<Machine>(Load->getSourceAddress(),
Store->getAddr())) Store->getAddr()))
continue; continue;
...@@ -558,6 +559,7 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() { ...@@ -558,6 +559,7 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() {
if (!canRMW(Arith)) if (!canRMW(Arith))
continue; continue;
if (Func->isVerbose(IceV_RMW)) { if (Func->isVerbose(IceV_RMW)) {
Ostream &Str = Func->getContext()->getStrDump();
Str << "Found RMW in " << Func->getFunctionName() << ":\n "; Str << "Found RMW in " << Func->getFunctionName() << ":\n ";
Load->dump(Func); Load->dump(Func);
Str << "\n "; Str << "\n ";
...@@ -576,9 +578,8 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() { ...@@ -576,9 +578,8 @@ template <class Machine> void TargetX86Base<Machine>::findRMW() {
Node->getInsts().insert(I3, RMW); Node->getInsts().insert(I3, RMW);
} }
} }
} if (Func->isVerbose(IceV_RMW))
} Func->getContext()->unlockStr();
}
} }
// Converts a ConstantInteger32 operand into its constant value, or // Converts a ConstantInteger32 operand into its constant value, or
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment