Commit 9b0e4e13 authored by Sigurd Schneider's avatar Sigurd Schneider Committed by Commit Bot

[turbofan] Make typed optimization more powerful

This CL moves optimization capabilities from typed lowering to typed
optimization. In particular, this allows retyping of Speculative to
number optimizations depending on their input types. This can save type
checks if we know that inputs are already in SafeIntegerRange and uses
are truncating to 32bit integers.

This change recovers the performance lost to 31bit Smis on
Octane/crypto on x64:
32bit nosmis           avg 30,984.84 stddev 180.52
31bit smis (w/o patch) avg 29,438.52 stddev 120.30  -4.99%
31bit smis             avg 31,274.52 stddev 176.26  +0.93%  +6.24%

Change-Id: I86d6e37305262336f4f7bd46aac0d2cbca11e8c1
Bug: v8:8344
Reviewed-on: https://chromium-review.googlesource.com/c/1323729
Commit-Queue: Sigurd Schneider <sigurds@chromium.org>
Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Reviewed-by: 's avatarJaroslav Sevcik <jarin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#57717}
parent be11d9b8
......@@ -343,31 +343,6 @@ class JSBinopReduction final {
UNREACHABLE();
}
const Operator* NumberOpFromSpeculativeNumberOp() {
switch (node_->opcode()) {
case IrOpcode::kSpeculativeNumberEqual:
return simplified()->NumberEqual();
case IrOpcode::kSpeculativeNumberLessThan:
return simplified()->NumberLessThan();
case IrOpcode::kSpeculativeNumberLessThanOrEqual:
return simplified()->NumberLessThanOrEqual();
case IrOpcode::kSpeculativeNumberAdd:
// Handled by ReduceSpeculativeNumberAdd.
UNREACHABLE();
case IrOpcode::kSpeculativeNumberSubtract:
return simplified()->NumberSubtract();
case IrOpcode::kSpeculativeNumberMultiply:
return simplified()->NumberMultiply();
case IrOpcode::kSpeculativeNumberDivide:
return simplified()->NumberDivide();
case IrOpcode::kSpeculativeNumberModulus:
return simplified()->NumberModulus();
default:
break;
}
UNREACHABLE();
}
bool LeftInputIs(Type t) { return left_type().Is(t); }
bool RightInputIs(Type t) { return right_type().Is(t); }
......@@ -460,21 +435,6 @@ JSTypedLowering::JSTypedLowering(Editor* editor, JSGraph* jsgraph,
graph()->zone())),
type_cache_(TypeCache::Get()) {}
Reduction JSTypedLowering::ReduceSpeculativeNumberAdd(Node* node) {
JSBinopReduction r(this, node);
NumberOperationHint hint = NumberOperationHintOf(node->op());
if ((hint == NumberOperationHint::kNumber ||
hint == NumberOperationHint::kNumberOrOddball) &&
r.BothInputsAre(Type::PlainPrimitive()) &&
r.NeitherInputCanBe(Type::StringOrReceiver())) {
// SpeculativeNumberAdd(x:-string, y:-string) =>
// NumberAdd(ToNumber(x), ToNumber(y))
r.ConvertInputsToNumber();
return r.ChangeToPureOperator(simplified()->NumberAdd(), Type::Number());
}
return NoChange();
}
Reduction JSTypedLowering::ReduceJSBitwiseNot(Node* node) {
Node* input = NodeProperties::GetValueInput(node, 0);
Type input_type = NodeProperties::GetType(input);
......@@ -705,22 +665,6 @@ Reduction JSTypedLowering::ReduceNumberBinop(Node* node) {
return NoChange();
}
Reduction JSTypedLowering::ReduceSpeculativeNumberBinop(Node* node) {
JSBinopReduction r(this, node);
NumberOperationHint hint = NumberOperationHintOf(node->op());
if ((hint == NumberOperationHint::kNumber ||
hint == NumberOperationHint::kNumberOrOddball) &&
r.BothInputsAre(Type::NumberOrUndefinedOrNullOrBoolean())) {
// We intentionally do this only in the Number and NumberOrOddball hint case
// because simplified lowering of these speculative ops may do some clever
// reductions in the other cases.
r.ConvertInputsToNumber();
return r.ChangeToPureOperator(r.NumberOpFromSpeculativeNumberOp(),
Type::Number());
}
return NoChange();
}
Reduction JSTypedLowering::ReduceInt32Binop(Node* node) {
JSBinopReduction r(this, node);
if (r.BothInputsAre(Type::PlainPrimitive())) {
......@@ -743,15 +687,6 @@ Reduction JSTypedLowering::ReduceUI32Shift(Node* node, Signedness signedness) {
return NoChange();
}
Reduction JSTypedLowering::ReduceSpeculativeNumberComparison(Node* node) {
JSBinopReduction r(this, node);
if (r.BothInputsAre(Type::Signed32()) ||
r.BothInputsAre(Type::Unsigned32())) {
return r.ChangeToPureOperator(r.NumberOpFromSpeculativeNumberOp());
}
return NoChange();
}
Reduction JSTypedLowering::ReduceJSComparison(Node* node) {
JSBinopReduction r(this, node);
if (r.BothInputsAre(Type::String())) {
......@@ -2411,19 +2346,6 @@ Reduction JSTypedLowering::Reduce(Node* node) {
return ReduceJSGeneratorRestoreRegister(node);
case IrOpcode::kJSGeneratorRestoreInputOrDebugPos:
return ReduceJSGeneratorRestoreInputOrDebugPos(node);
// TODO(mstarzinger): Simplified operations hiding in JS-level reducer not
// fooling anyone. Consider moving this into a separate reducer.
case IrOpcode::kSpeculativeNumberAdd:
return ReduceSpeculativeNumberAdd(node);
case IrOpcode::kSpeculativeNumberSubtract:
case IrOpcode::kSpeculativeNumberMultiply:
case IrOpcode::kSpeculativeNumberDivide:
case IrOpcode::kSpeculativeNumberModulus:
return ReduceSpeculativeNumberBinop(node);
case IrOpcode::kSpeculativeNumberEqual:
case IrOpcode::kSpeculativeNumberLessThan:
case IrOpcode::kSpeculativeNumberLessThanOrEqual:
return ReduceSpeculativeNumberComparison(node);
case IrOpcode::kJSObjectIsArray:
return ReduceObjectIsArray(node);
case IrOpcode::kJSParseInt:
......
......@@ -81,10 +81,6 @@ class V8_EXPORT_PRIVATE JSTypedLowering final
Reduction ReduceNumberBinop(Node* node);
Reduction ReduceInt32Binop(Node* node);
Reduction ReduceUI32Shift(Node* node, Signedness signedness);
Reduction ReduceSpeculativeNumberAdd(Node* node);
Reduction ReduceSpeculativeNumberMultiply(Node* node);
Reduction ReduceSpeculativeNumberBinop(Node* node);
Reduction ReduceSpeculativeNumberComparison(Node* node);
Reduction ReduceObjectIsArray(Node* node);
Reduction ReduceJSParseInt(Node* node);
Reduction ReduceJSResolvePromise(Node* node);
......
......@@ -1219,8 +1219,8 @@ struct TypedLoweringPhase {
AddReducer(data, &graph_reducer, &dead_code_elimination);
AddReducer(data, &graph_reducer, &create_lowering);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &typed_lowering);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &simple_reducer);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
......@@ -1419,6 +1419,8 @@ struct LoadEliminationPhase {
CommonOperatorReducer common_reducer(&graph_reducer, data->graph(),
data->broker(), data->common(),
data->machine(), temp_zone);
TypedOptimization typed_optimization(&graph_reducer, data->dependencies(),
data->jsgraph(), data->broker());
ConstantFoldingReducer constant_folding_reducer(
&graph_reducer, data->jsgraph(), data->broker());
TypeNarrowingReducer type_narrowing_reducer(&graph_reducer, data->jsgraph(),
......@@ -1429,6 +1431,7 @@ struct LoadEliminationPhase {
AddReducer(data, &graph_reducer, &load_elimination);
AddReducer(data, &graph_reducer, &type_narrowing_reducer);
AddReducer(data, &graph_reducer, &constant_folding_reducer);
AddReducer(data, &graph_reducer, &typed_optimization);
AddReducer(data, &graph_reducer, &checkpoint_elimination);
AddReducer(data, &graph_reducer, &common_reducer);
AddReducer(data, &graph_reducer, &value_numbering);
......
......@@ -84,6 +84,17 @@ Reduction TypedOptimization::Reduce(Node* node) {
return ReduceToBoolean(node);
case IrOpcode::kSpeculativeToNumber:
return ReduceSpeculativeToNumber(node);
case IrOpcode::kSpeculativeNumberAdd:
return ReduceSpeculativeNumberAdd(node);
case IrOpcode::kSpeculativeNumberSubtract:
case IrOpcode::kSpeculativeNumberMultiply:
case IrOpcode::kSpeculativeNumberDivide:
case IrOpcode::kSpeculativeNumberModulus:
return ReduceSpeculativeNumberBinop(node);
case IrOpcode::kSpeculativeNumberEqual:
case IrOpcode::kSpeculativeNumberLessThan:
case IrOpcode::kSpeculativeNumberLessThanOrEqual:
return ReduceSpeculativeNumberComparison(node);
default:
break;
}
......@@ -664,6 +675,148 @@ Reduction TypedOptimization::ReduceToBoolean(Node* node) {
return NoChange();
}
namespace {
bool BothAre(Type t1, Type t2, Type t3) { return t1.Is(t3) && t2.Is(t3); }
bool NeitherCanBe(Type t1, Type t2, Type t3) {
return !t1.Maybe(t3) && !t2.Maybe(t3);
}
const Operator* NumberOpFromSpeculativeNumberOp(
SimplifiedOperatorBuilder* simplified, const Operator* op) {
switch (op->opcode()) {
case IrOpcode::kSpeculativeNumberEqual:
return simplified->NumberEqual();
case IrOpcode::kSpeculativeNumberLessThan:
return simplified->NumberLessThan();
case IrOpcode::kSpeculativeNumberLessThanOrEqual:
return simplified->NumberLessThanOrEqual();
case IrOpcode::kSpeculativeNumberAdd:
// Handled by ReduceSpeculativeNumberAdd.
UNREACHABLE();
case IrOpcode::kSpeculativeNumberSubtract:
return simplified->NumberSubtract();
case IrOpcode::kSpeculativeNumberMultiply:
return simplified->NumberMultiply();
case IrOpcode::kSpeculativeNumberDivide:
return simplified->NumberDivide();
case IrOpcode::kSpeculativeNumberModulus:
return simplified->NumberModulus();
default:
break;
}
UNREACHABLE();
}
} // namespace
Reduction TypedOptimization::ReduceSpeculativeNumberAdd(Node* node) {
Node* const lhs = NodeProperties::GetValueInput(node, 0);
Node* const rhs = NodeProperties::GetValueInput(node, 1);
Type const lhs_type = NodeProperties::GetType(lhs);
Type const rhs_type = NodeProperties::GetType(rhs);
NumberOperationHint hint = NumberOperationHintOf(node->op());
if ((hint == NumberOperationHint::kNumber ||
hint == NumberOperationHint::kNumberOrOddball) &&
BothAre(lhs_type, rhs_type, Type::PlainPrimitive()) &&
NeitherCanBe(lhs_type, rhs_type, Type::StringOrReceiver())) {
// SpeculativeNumberAdd(x:-string, y:-string) =>
// NumberAdd(ToNumber(x), ToNumber(y))
Node* const toNum_lhs =
graph()->NewNode(simplified()->PlainPrimitiveToNumber(), lhs);
Node* const toNum_rhs =
graph()->NewNode(simplified()->PlainPrimitiveToNumber(), rhs);
Node* const value =
graph()->NewNode(simplified()->NumberAdd(), toNum_lhs, toNum_rhs);
ReplaceWithValue(node, value);
return Replace(node);
}
return NoChange();
}
Reduction TypedOptimization::ReduceJSToNumberInput(Node* input) {
// Try constant-folding of JSToNumber with constant inputs.
Type input_type = NodeProperties::GetType(input);
if (input_type.Is(Type::String())) {
HeapObjectMatcher m(input);
if (m.HasValue() && m.Ref(broker()).IsString()) {
StringRef input_value = m.Ref(broker()).AsString();
double number;
ASSIGN_RETURN_NO_CHANGE_IF_DATA_MISSING(number, input_value.ToNumber());
return Replace(jsgraph()->Constant(number));
}
}
if (input_type.IsHeapConstant()) {
HeapObjectRef input_value = input_type.AsHeapConstant()->Ref();
if (input_value.map().oddball_type() != OddballType::kNone) {
return Replace(jsgraph()->Constant(input_value.OddballToNumber()));
}
}
if (input_type.Is(Type::Number())) {
// JSToNumber(x:number) => x
return Changed(input);
}
if (input_type.Is(Type::Undefined())) {
// JSToNumber(undefined) => #NaN
return Replace(jsgraph()->NaNConstant());
}
if (input_type.Is(Type::Null())) {
// JSToNumber(null) => #0
return Replace(jsgraph()->ZeroConstant());
}
return NoChange();
}
Node* TypedOptimization::ConvertPlainPrimitiveToNumber(Node* node) {
DCHECK(NodeProperties::GetType(node).Is(Type::PlainPrimitive()));
// Avoid inserting too many eager ToNumber() operations.
Reduction const reduction = ReduceJSToNumberInput(node);
if (reduction.Changed()) return reduction.replacement();
if (NodeProperties::GetType(node).Is(Type::Number())) {
return node;
}
return graph()->NewNode(simplified()->PlainPrimitiveToNumber(), node);
}
Reduction TypedOptimization::ReduceSpeculativeNumberBinop(Node* node) {
Node* const lhs = NodeProperties::GetValueInput(node, 0);
Node* const rhs = NodeProperties::GetValueInput(node, 1);
Type const lhs_type = NodeProperties::GetType(lhs);
Type const rhs_type = NodeProperties::GetType(rhs);
NumberOperationHint hint = NumberOperationHintOf(node->op());
if ((hint == NumberOperationHint::kNumber ||
hint == NumberOperationHint::kNumberOrOddball) &&
BothAre(lhs_type, rhs_type, Type::NumberOrUndefinedOrNullOrBoolean())) {
// We intentionally do this only in the Number and NumberOrOddball hint case
// because simplified lowering of these speculative ops may do some clever
// reductions in the other cases.
Node* const toNum_lhs = ConvertPlainPrimitiveToNumber(lhs);
Node* const toNum_rhs = ConvertPlainPrimitiveToNumber(rhs);
Node* const value = graph()->NewNode(
NumberOpFromSpeculativeNumberOp(simplified(), node->op()), toNum_lhs,
toNum_rhs);
ReplaceWithValue(node, value);
return Replace(node);
}
return NoChange();
}
Reduction TypedOptimization::ReduceSpeculativeNumberComparison(Node* node) {
Node* const lhs = NodeProperties::GetValueInput(node, 0);
Node* const rhs = NodeProperties::GetValueInput(node, 1);
Type const lhs_type = NodeProperties::GetType(lhs);
Type const rhs_type = NodeProperties::GetType(rhs);
if (BothAre(lhs_type, rhs_type, Type::Signed32()) ||
BothAre(lhs_type, rhs_type, Type::Unsigned32())) {
Node* const value = graph()->NewNode(
NumberOpFromSpeculativeNumberOp(simplified(), node->op()), lhs, rhs);
ReplaceWithValue(node, value);
return Replace(node);
}
return NoChange();
}
Factory* TypedOptimization::factory() const {
return jsgraph()->isolate()->factory();
}
......
......@@ -58,6 +58,10 @@ class V8_EXPORT_PRIVATE TypedOptimization final
Reduction ReduceCheckNotTaggedHole(Node* node);
Reduction ReduceTypeOf(Node* node);
Reduction ReduceToBoolean(Node* node);
Reduction ReduceSpeculativeNumberAdd(Node* node);
Reduction ReduceSpeculativeNumberMultiply(Node* node);
Reduction ReduceSpeculativeNumberBinop(Node* node);
Reduction ReduceSpeculativeNumberComparison(Node* node);
Reduction TryReduceStringComparisonOfStringFromSingleCharCode(
Node* comparison, Node* from_char_code, Type constant_type,
......@@ -66,6 +70,9 @@ class V8_EXPORT_PRIVATE TypedOptimization final
Node* comparison, const StringRef& string, bool inverted);
const Operator* NumberComparisonFor(const Operator* op);
Node* ConvertPlainPrimitiveToNumber(Node* node);
Reduction ReduceJSToNumberInput(Node* input);
SimplifiedOperatorBuilder* simplified() const;
Factory* factory() const;
Graph* graph() const;
......
......@@ -4,178 +4,6 @@
// Flags: --allow-natives-syntax --opt --noalways-opt
// Test that NumberModulus with Number feedback works if only in the
// end SimplifiedLowering figures out that the inputs to this operation
// are actually Unsigned32.
(function() {
// We need a separately polluted % with NumberOrOddball feedback.
function bar(x) { return x % 2; }
bar(undefined); // The % feedback is now NumberOrOddball.
// Now just use the gadget above in a way that only after RETYPE
// in SimplifiedLowering we find out that the `x` is actually in
// Unsigned32 range (based on taking the SignedSmall feedback on
// the + operator).
function foo(x) {
x = (x >>> 0) + 1;
return bar(x) | 0;
}
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
assertOptimized(foo);
})();
// Test that NumberModulus with Number feedback works if only in the
// end SimplifiedLowering figures out that the inputs to this operation
// are actually Signed32.
(function() {
// We need a separately polluted % with NumberOrOddball feedback.
function bar(x) { return x % 2; }
bar(undefined); // The % feedback is now NumberOrOddball.
// Now just use the gadget above in a way that only after RETYPE
// in SimplifiedLowering we find out that the `x` is actually in
// Signed32 range (based on taking the SignedSmall feedback on
// the + operator).
function foo(x) {
x = (x | 0) + 1;
return bar(x) | 0;
}
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
assertOptimized(foo);
})();
// Test that SpeculativeNumberModulus with Number feedback works if
// only in the end SimplifiedLowering figures out that the inputs to
// this operation are actually Unsigned32.
(function() {
// We need to use an object literal here to make sure that the
// SpeculativeNumberModulus is not turned into a NumberModulus
// early during JSTypedLowering.
function bar(x) { return {x}.x % 2; }
bar(undefined); // The % feedback is now NumberOrOddball.
// Now just use the gadget above in a way that only after RETYPE
// in SimplifiedLowering we find out that the `x` is actually in
// Unsigned32 range (based on taking the SignedSmall feedback on
// the + operator).
function foo(x) {
x = (x >>> 0) + 1;
return bar(x) | 0;
}
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
assertOptimized(foo);
})();
// Test that SpeculativeNumberModulus with Number feedback works if
// only in the end SimplifiedLowering figures out that the inputs to
// this operation are actually Signed32.
(function() {
// We need to use an object literal here to make sure that the
// SpeculativeNumberModulus is not turned into a NumberModulus
// early during JSTypedLowering.
function bar(x) { return {x}.x % 2; }
bar(undefined); // The % feedback is now NumberOrOddball.
// Now just use the gadget above in a way that only after RETYPE
// in SimplifiedLowering we find out that the `x` is actually in
// Signed32 range (based on taking the SignedSmall feedback on
// the + operator).
function foo(x) {
x = (x | 0) + 1;
return bar(x) | 0;
}
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(1));
assertEquals(1, foo(2));
assertEquals(0, foo(3));
assertEquals(1, foo(4));
assertOptimized(foo);
})();
// Test that NumberModulus works in the case where TurboFan
// can infer that the output is Signed32 \/ MinusZero, and
// there's a truncation on the result that identifies zeros
// (via the SpeculativeNumberEqual).
(function() {
// We need a separately polluted % with NumberOrOddball feedback.
function bar(x) { return x % 2; }
bar(undefined); // The % feedback is now NumberOrOddball.
// Now we just use the gadget above on an `x` that is known
// to be in Signed32 range and compare it to 0, which passes
// a truncation that identifies zeros.
function foo(x) {
if (bar(x | 0) == 0) return 0;
return 1;
}
assertEquals(0, foo(2));
assertEquals(1, foo(1));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(2));
assertEquals(1, foo(1));
assertOptimized(foo);
// Now `foo` should stay optimized even if `x % 2` would
// produce -0, aka when we pass a negative value for `x`.
assertEquals(0, foo(-2));
assertEquals(1, foo(-1));
assertOptimized(foo);
})();
// Test that CheckedInt32Mod handles the slow-path (when
// the left hand side is negative) correctly.
(function() {
// We need a SpeculativeNumberModulus with SignedSmall feedback.
function foo(x, y) {
return x % y;
}
assertEquals(0, foo(2, 1));
assertEquals(0, foo(2, 2));
assertEquals(-1, foo(-3, 2));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(2, 1));
assertEquals(0, foo(2, 2));
assertEquals(-1, foo(-3, 2));
assertOptimized(foo);
// Now `foo` should deoptimize if the result is -0.
assertEquals(-0, foo(-2, 2));
assertUnoptimized(foo);
})();
// Test that NumberModulus passes kIdentifiesZero to the
// left hand side input when the result doesn't care about
......@@ -196,61 +24,3 @@
assertTrue(foo(0));
assertOptimized(foo);
})();
// Test that NumberModulus passes kIdentifiesZero to the
// right hand side input, even when the inputs are outside
// the Signed32 range.
(function() {
function foo(x) {
return (2 ** 32) % (x * -2);
}
assertEquals(0, foo(1));
assertEquals(0, foo(1));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(1));
// Now `foo` should stay optimized even if `x * -2` would
// produce -0, aka when we pass a zero value for `x`.
assertEquals(NaN, foo(0));
assertOptimized(foo);
})();
// Test that SpeculativeNumberModulus passes kIdentifiesZero
// to the right hand side input, even when feedback is consumed.
(function() {
function foo(x, y) {
return (x % (y * -2)) | 0;
}
assertEquals(0, foo(2, 1));
assertEquals(-1, foo(-3, 1));
%OptimizeFunctionOnNextCall(foo);
assertEquals(0, foo(2, 1));
assertEquals(-1, foo(-3, 1));
assertOptimized(foo);
// Now `foo` should stay optimized even if `y * -2` would
// produce -0, aka when we pass a zero value for `y`.
assertEquals(0, foo(2, 0));
assertOptimized(foo);
})();
// Test that SpeculativeNumberModulus passes kIdentifiesZero
// to the left hand side input, even when feedback is consumed.
(function() {
function foo(x, y) {
return ((x * -2) % y) | 0;
}
assertEquals(-2, foo(1, 3));
assertEquals(-2, foo(1, 3));
%OptimizeFunctionOnNextCall(foo);
assertEquals(-2, foo(1, 3));
assertOptimized(foo);
// Now `foo` should stay optimized even if `x * -2` would
// produce -0, aka when we pass a zero value for `x`.
assertEquals(0, foo(0, 2));
assertOptimized(foo);
})();
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