Commit bdf70aa0 authored by Ross McIlroy's avatar Ross McIlroy Committed by Commit Bot

[Interpreter] Improve handling of a === true / false.

Add support for direct jumping on True/False for strict equals of boolean
literals. This improves the score for such comparisons by around 75% on
baseline code, and by around 40x on optimized code for the added performance
test.

Bug=v8:6403

Change-Id: I81ea16a057e081eb6d159cd64c8e8615f65f9abb
Reviewed-on: https://chromium-review.googlesource.com/509570
Commit-Queue: Ross McIlroy <rmcilroy@chromium.org>
Reviewed-by: 's avatarMythri Alle <mythria@chromium.org>
Reviewed-by: 's avatarBenedikt Meurer <bmeurer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#45450}
parent 457a4a6d
...@@ -102,6 +102,11 @@ bool Expression::IsStringLiteral() const { ...@@ -102,6 +102,11 @@ bool Expression::IsStringLiteral() const {
return IsLiteral() && AsLiteral()->raw_value()->IsString(); return IsLiteral() && AsLiteral()->raw_value()->IsString();
} }
bool Expression::IsBooleanLiteral() const {
return IsLiteral() && (AsLiteral()->raw_value()->IsTrue() ||
AsLiteral()->raw_value()->IsFalse());
}
bool Expression::IsPropertyName() const { bool Expression::IsPropertyName() const {
return IsLiteral() && AsLiteral()->IsPropertyName(); return IsLiteral() && AsLiteral()->IsPropertyName();
} }
...@@ -995,7 +1000,6 @@ bool CompareOperation::IsLiteralCompareUndefined(Expression** expr) { ...@@ -995,7 +1000,6 @@ bool CompareOperation::IsLiteralCompareUndefined(Expression** expr) {
MatchLiteralCompareUndefined(right_, op(), left_, expr); MatchLiteralCompareUndefined(right_, op(), left_, expr);
} }
// Check for the pattern: null equals <expression> // Check for the pattern: null equals <expression>
static bool MatchLiteralCompareNull(Expression* left, static bool MatchLiteralCompareNull(Expression* left,
Token::Value op, Token::Value op,
...@@ -1008,12 +1012,28 @@ static bool MatchLiteralCompareNull(Expression* left, ...@@ -1008,12 +1012,28 @@ static bool MatchLiteralCompareNull(Expression* left,
return false; return false;
} }
bool CompareOperation::IsLiteralCompareNull(Expression** expr) { bool CompareOperation::IsLiteralCompareNull(Expression** expr) {
return MatchLiteralCompareNull(left_, op(), right_, expr) || return MatchLiteralCompareNull(left_, op(), right_, expr) ||
MatchLiteralCompareNull(right_, op(), left_, expr); MatchLiteralCompareNull(right_, op(), left_, expr);
} }
// Check for the pattern: true/false equals <expression>
static bool MatchLiteralStrictEqualBoolean(Expression* left, Token::Value op,
Expression* right, Expression** expr,
Literal** literal) {
if (left->IsBooleanLiteral() && op == Token::EQ_STRICT) {
*expr = right;
*literal = left->AsLiteral();
return true;
}
return false;
}
bool CompareOperation::IsLiteralStrictEqualBoolean(Expression** expr,
Literal** literal) {
return MatchLiteralStrictEqualBoolean(right_, op(), left_, expr, literal) ||
MatchLiteralStrictEqualBoolean(left_, op(), right_, expr, literal);
}
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// Recording of type feedback // Recording of type feedback
......
...@@ -315,6 +315,9 @@ class Expression : public AstNode { ...@@ -315,6 +315,9 @@ class Expression : public AstNode {
// True iff the expression is a string literal. // True iff the expression is a string literal.
bool IsStringLiteral() const; bool IsStringLiteral() const;
// True iff the expression is a boolean literal.
bool IsBooleanLiteral() const;
// True iff the expression is the null literal. // True iff the expression is the null literal.
bool IsNullLiteral() const; bool IsNullLiteral() const;
...@@ -1184,6 +1187,11 @@ class Literal final : public Expression { ...@@ -1184,6 +1187,11 @@ class Literal final : public Expression {
return raw_value()->AsSmi(); return raw_value()->AsSmi();
} }
bool AsBooleanLiteral() {
DCHECK(IsBooleanLiteral());
return raw_value()->IsTrue();
}
bool ToBooleanIsTrue() const { return raw_value()->BooleanValue(); } bool ToBooleanIsTrue() const { return raw_value()->BooleanValue(); }
bool ToBooleanIsFalse() const { return !raw_value()->BooleanValue(); } bool ToBooleanIsFalse() const { return !raw_value()->BooleanValue(); }
...@@ -2294,6 +2302,7 @@ class CompareOperation final : public Expression { ...@@ -2294,6 +2302,7 @@ class CompareOperation final : public Expression {
// Match special cases. // Match special cases.
bool IsLiteralCompareTypeof(Expression** expr, Literal** literal); bool IsLiteralCompareTypeof(Expression** expr, Literal** literal);
bool IsLiteralStrictEqualBoolean(Expression** expr, Literal** literal);
bool IsLiteralCompareUndefined(Expression** expr); bool IsLiteralCompareUndefined(Expression** expr);
bool IsLiteralCompareNull(Expression** expr); bool IsLiteralCompareNull(Expression** expr);
......
...@@ -2461,28 +2461,18 @@ void BytecodeGraphBuilder::BuildJumpIfNotEqual(Node* comperand) { ...@@ -2461,28 +2461,18 @@ void BytecodeGraphBuilder::BuildJumpIfNotEqual(Node* comperand) {
BuildJumpIfNot(condition); BuildJumpIfNot(condition);
} }
void BytecodeGraphBuilder::BuildJumpIfFalse() { void BytecodeGraphBuilder::BuildJumpIfTrue() {
NewBranch(environment()->LookupAccumulator()); Node* accumulator = environment()->LookupAccumulator();
{ Node* condition = NewNode(simplified()->ReferenceEqual(), accumulator,
SubEnvironment sub_environment(this); jsgraph()->TrueConstant());
NewIfFalse(); BuildJumpIf(condition);
environment()->BindAccumulator(jsgraph()->FalseConstant());
MergeIntoSuccessorEnvironment(bytecode_iterator().GetJumpTargetOffset());
}
NewIfTrue();
environment()->BindAccumulator(jsgraph()->TrueConstant());
} }
void BytecodeGraphBuilder::BuildJumpIfTrue() { void BytecodeGraphBuilder::BuildJumpIfFalse() {
NewBranch(environment()->LookupAccumulator()); Node* accumulator = environment()->LookupAccumulator();
{ Node* condition = NewNode(simplified()->ReferenceEqual(), accumulator,
SubEnvironment sub_environment(this); jsgraph()->FalseConstant());
NewIfTrue(); BuildJumpIf(condition);
environment()->BindAccumulator(jsgraph()->TrueConstant());
MergeIntoSuccessorEnvironment(bytecode_iterator().GetJumpTargetOffset());
}
NewIfFalse();
environment()->BindAccumulator(jsgraph()->FalseConstant());
} }
void BytecodeGraphBuilder::BuildJumpIfToBooleanTrue() { void BytecodeGraphBuilder::BuildJumpIfToBooleanTrue() {
......
...@@ -1064,6 +1064,15 @@ BytecodeArrayBuilder& BytecodeArrayBuilder::JumpIfFalse(ToBooleanMode mode, ...@@ -1064,6 +1064,15 @@ BytecodeArrayBuilder& BytecodeArrayBuilder::JumpIfFalse(ToBooleanMode mode,
return *this; return *this;
} }
BytecodeArrayBuilder& BytecodeArrayBuilder::JumpIfBoolean(
bool boolean_comparison, ToBooleanMode mode, BytecodeLabel* label) {
if (boolean_comparison) {
return JumpIfTrue(mode, label);
} else {
return JumpIfFalse(mode, label);
}
}
BytecodeArrayBuilder& BytecodeArrayBuilder::JumpIfNull(BytecodeLabel* label) { BytecodeArrayBuilder& BytecodeArrayBuilder::JumpIfNull(BytecodeLabel* label) {
DCHECK(!label->is_bound()); DCHECK(!label->is_bound());
OutputJumpIfNull(label, 0); OutputJumpIfNull(label, 0);
......
...@@ -367,6 +367,8 @@ class V8_EXPORT_PRIVATE BytecodeArrayBuilder final ...@@ -367,6 +367,8 @@ class V8_EXPORT_PRIVATE BytecodeArrayBuilder final
BytecodeArrayBuilder& JumpIfTrue(ToBooleanMode mode, BytecodeLabel* label); BytecodeArrayBuilder& JumpIfTrue(ToBooleanMode mode, BytecodeLabel* label);
BytecodeArrayBuilder& JumpIfFalse(ToBooleanMode mode, BytecodeLabel* label); BytecodeArrayBuilder& JumpIfFalse(ToBooleanMode mode, BytecodeLabel* label);
BytecodeArrayBuilder& JumpIfBoolean(bool boolean_comparison,
ToBooleanMode mode, BytecodeLabel* label);
BytecodeArrayBuilder& JumpIfNotHole(BytecodeLabel* label); BytecodeArrayBuilder& JumpIfNotHole(BytecodeLabel* label);
BytecodeArrayBuilder& JumpIfJSReceiver(BytecodeLabel* label); BytecodeArrayBuilder& JumpIfJSReceiver(BytecodeLabel* label);
BytecodeArrayBuilder& JumpIfNull(BytecodeLabel* label); BytecodeArrayBuilder& JumpIfNull(BytecodeLabel* label);
......
...@@ -3182,6 +3182,25 @@ void BytecodeGenerator::BuildLiteralCompareNil(Token::Value op, NilValue nil) { ...@@ -3182,6 +3182,25 @@ void BytecodeGenerator::BuildLiteralCompareNil(Token::Value op, NilValue nil) {
} }
} }
void BytecodeGenerator::BuildLiteralBooleanStrictEquals(Literal* literal) {
TestResultScope* test_result = execution_result()->AsTest();
switch (test_result->fallthrough()) {
case TestFallthrough::kElse:
builder()->JumpIfBoolean(literal->AsBooleanLiteral(),
ToBooleanMode::kAlreadyBoolean,
test_result->NewThenLabel());
break;
case TestFallthrough::kNone:
case TestFallthrough::kThen:
builder()
->JumpIfBoolean(literal->AsBooleanLiteral(),
ToBooleanMode::kAlreadyBoolean,
test_result->NewThenLabel())
.Jump(test_result->NewElseLabel());
}
test_result->SetResultConsumedByTest();
}
void BytecodeGenerator::VisitCompareOperation(CompareOperation* expr) { void BytecodeGenerator::VisitCompareOperation(CompareOperation* expr) {
Expression* sub_expr; Expression* sub_expr;
Literal* literal; Literal* literal;
...@@ -3197,6 +3216,11 @@ void BytecodeGenerator::VisitCompareOperation(CompareOperation* expr) { ...@@ -3197,6 +3216,11 @@ void BytecodeGenerator::VisitCompareOperation(CompareOperation* expr) {
} else { } else {
builder()->CompareTypeOf(literal_flag); builder()->CompareTypeOf(literal_flag);
} }
} else if (execution_result()->IsTest() &&
expr->IsLiteralStrictEqualBoolean(&sub_expr, &literal)) {
VisitForAccumulatorValue(sub_expr);
builder()->SetExpressionPosition(expr);
BuildLiteralBooleanStrictEquals(literal);
} else if (expr->IsLiteralCompareUndefined(&sub_expr)) { } else if (expr->IsLiteralCompareUndefined(&sub_expr)) {
VisitForAccumulatorValue(sub_expr); VisitForAccumulatorValue(sub_expr);
builder()->SetExpressionPosition(expr); builder()->SetExpressionPosition(expr);
......
...@@ -114,6 +114,7 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> { ...@@ -114,6 +114,7 @@ class BytecodeGenerator final : public AstVisitor<BytecodeGenerator> {
FeedbackSlot slot, FeedbackSlot slot,
HoleCheckMode hole_check_mode); HoleCheckMode hole_check_mode);
void BuildLiteralCompareNil(Token::Value compare_op, NilValue nil); void BuildLiteralCompareNil(Token::Value compare_op, NilValue nil);
void BuildLiteralBooleanStrictEquals(Literal* literal);
void BuildReturn(); void BuildReturn();
void BuildAsyncReturn(); void BuildAsyncReturn();
void BuildAsyncGeneratorReturn(); void BuildAsyncGeneratorReturn();
......
...@@ -2442,8 +2442,6 @@ IGNITION_HANDLER(JumpIfTrue, InterpreterAssembler) { ...@@ -2442,8 +2442,6 @@ IGNITION_HANDLER(JumpIfTrue, InterpreterAssembler) {
Node* accumulator = GetAccumulator(); Node* accumulator = GetAccumulator();
Node* relative_jump = BytecodeOperandUImmWord(0); Node* relative_jump = BytecodeOperandUImmWord(0);
Node* true_value = BooleanConstant(true); Node* true_value = BooleanConstant(true);
CSA_ASSERT(this, TaggedIsNotSmi(accumulator));
CSA_ASSERT(this, IsBoolean(accumulator));
JumpIfWordEqual(accumulator, true_value, relative_jump); JumpIfWordEqual(accumulator, true_value, relative_jump);
} }
...@@ -2457,8 +2455,6 @@ IGNITION_HANDLER(JumpIfTrueConstant, InterpreterAssembler) { ...@@ -2457,8 +2455,6 @@ IGNITION_HANDLER(JumpIfTrueConstant, InterpreterAssembler) {
Node* index = BytecodeOperandIdx(0); Node* index = BytecodeOperandIdx(0);
Node* relative_jump = LoadAndUntagConstantPoolEntry(index); Node* relative_jump = LoadAndUntagConstantPoolEntry(index);
Node* true_value = BooleanConstant(true); Node* true_value = BooleanConstant(true);
CSA_ASSERT(this, TaggedIsNotSmi(accumulator));
CSA_ASSERT(this, IsBoolean(accumulator));
JumpIfWordEqual(accumulator, true_value, relative_jump); JumpIfWordEqual(accumulator, true_value, relative_jump);
} }
...@@ -2471,8 +2467,6 @@ IGNITION_HANDLER(JumpIfFalse, InterpreterAssembler) { ...@@ -2471,8 +2467,6 @@ IGNITION_HANDLER(JumpIfFalse, InterpreterAssembler) {
Node* accumulator = GetAccumulator(); Node* accumulator = GetAccumulator();
Node* relative_jump = BytecodeOperandUImmWord(0); Node* relative_jump = BytecodeOperandUImmWord(0);
Node* false_value = BooleanConstant(false); Node* false_value = BooleanConstant(false);
CSA_ASSERT(this, TaggedIsNotSmi(accumulator));
CSA_ASSERT(this, IsBoolean(accumulator));
JumpIfWordEqual(accumulator, false_value, relative_jump); JumpIfWordEqual(accumulator, false_value, relative_jump);
} }
...@@ -2486,8 +2480,6 @@ IGNITION_HANDLER(JumpIfFalseConstant, InterpreterAssembler) { ...@@ -2486,8 +2480,6 @@ IGNITION_HANDLER(JumpIfFalseConstant, InterpreterAssembler) {
Node* index = BytecodeOperandIdx(0); Node* index = BytecodeOperandIdx(0);
Node* relative_jump = LoadAndUntagConstantPoolEntry(index); Node* relative_jump = LoadAndUntagConstantPoolEntry(index);
Node* false_value = BooleanConstant(false); Node* false_value = BooleanConstant(false);
CSA_ASSERT(this, TaggedIsNotSmi(accumulator));
CSA_ASSERT(this, IsBoolean(accumulator));
JumpIfWordEqual(accumulator, false_value, relative_jump); JumpIfWordEqual(accumulator, false_value, relative_jump);
} }
......
#
# Autogenerated by generate-bytecode-expectations.
#
---
wrap: yes
---
snippet: "
return (1 === true) ? 1 : 2;
"
frame size: 0
parameter count: 1
bytecode array length: 14
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 34 S> */ B(LdaSmi), I8(1),
B(JumpIfTrue), U8(4),
B(Jump), U8(6),
B(LdaSmi), I8(1),
B(Jump), U8(4),
B(LdaSmi), I8(2),
/* 63 S> */ B(Return),
]
constant pool: [
]
handlers: [
]
---
snippet: "
return (false === 1) ? 1 : 2;
"
frame size: 0
parameter count: 1
bytecode array length: 14
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 34 S> */ B(LdaSmi), I8(1),
B(JumpIfFalse), U8(4),
B(Jump), U8(6),
B(LdaSmi), I8(1),
B(Jump), U8(4),
B(LdaSmi), I8(2),
/* 64 S> */ B(Return),
]
constant pool: [
]
handlers: [
]
---
snippet: "
return (1 === true || 0 === false) ? 1 : 2;
"
frame size: 0
parameter count: 1
bytecode array length: 17
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 34 S> */ B(LdaSmi), I8(1),
B(JumpIfTrue), U8(7),
B(LdaZero),
B(JumpIfFalse), U8(4),
B(Jump), U8(6),
B(LdaSmi), I8(1),
B(Jump), U8(4),
B(LdaSmi), I8(2),
/* 78 S> */ B(Return),
]
constant pool: [
]
handlers: [
]
---
snippet: "
if (1 === true || 1 === false) return 1;
"
frame size: 0
parameter count: 1
bytecode array length: 16
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 34 S> */ B(LdaSmi), I8(1),
B(JumpIfTrue), U8(8),
B(LdaSmi), I8(1),
B(JumpIfFalse), U8(4),
B(Jump), U8(5),
/* 65 S> */ B(LdaSmi), I8(1),
/* 75 S> */ B(Return),
B(LdaUndefined),
/* 75 S> */ B(Return),
]
constant pool: [
]
handlers: [
]
---
snippet: "
if (!('false' === false)) return 1;
"
frame size: 0
parameter count: 1
bytecode array length: 10
bytecodes: [
/* 30 E> */ B(StackCheck),
/* 34 S> */ B(LdaConstant), U8(0),
B(JumpIfFalse), U8(5),
/* 60 S> */ B(LdaSmi), I8(1),
/* 70 S> */ B(Return),
B(LdaUndefined),
/* 70 S> */ B(Return),
]
constant pool: [
ONE_BYTE_INTERNALIZED_STRING_TYPE ["false"],
]
handlers: [
]
...@@ -1029,6 +1029,26 @@ TEST(Typeof) { ...@@ -1029,6 +1029,26 @@ TEST(Typeof) {
LoadGolden("Typeof.golden"))); LoadGolden("Typeof.golden")));
} }
TEST(CompareBoolean) {
InitializedIgnitionHandleScope scope;
BytecodeExpectationsPrinter printer(CcTest::isolate());
const char* snippets[] = {
"return (1 === true) ? 1 : 2;\n",
"return (false === 1) ? 1 : 2;\n",
"return (1 === true || 0 === false) ? 1 : 2;\n",
"if (1 === true || 1 === false) return 1;\n",
"if (!('false' === false)) return 1;\n",
};
CHECK(CompareTexts(BuildActual(printer, snippets),
LoadGolden("CompareBoolean.golden")));
}
TEST(CompareTypeOf) { TEST(CompareTypeOf) {
InitializedIgnitionHandleScope scope; InitializedIgnitionHandleScope scope;
BytecodeExpectationsPrinter printer(CcTest::isolate()); BytecodeExpectationsPrinter printer(CcTest::isolate());
......
...@@ -16,6 +16,8 @@ addBenchmark('Number-StrictEquals-False', NumberStrictEqualsFalse); ...@@ -16,6 +16,8 @@ addBenchmark('Number-StrictEquals-False', NumberStrictEqualsFalse);
addBenchmark('String-StrictEquals-True', StringStrictEqualsTrue); addBenchmark('String-StrictEquals-True', StringStrictEqualsTrue);
addBenchmark('String-StrictEquals-False', StringStrictEqualsFalse); addBenchmark('String-StrictEquals-False', StringStrictEqualsFalse);
addBenchmark('SmiString-StrictEquals', MixedStrictEquals); addBenchmark('SmiString-StrictEquals', MixedStrictEquals);
addBenchmark('Boolean-StrictEquals-True', BooleanStrictEqualsTrue);
addBenchmark('Boolean-StrictEquals-False', BooleanStrictEqualsTrue);
addBenchmark('Smi-Equals-True', SmiEqualsTrue); addBenchmark('Smi-Equals-True', SmiEqualsTrue);
addBenchmark('Smi-Equals-False', SmiEqualsFalse); addBenchmark('Smi-Equals-False', SmiEqualsFalse);
addBenchmark('Number-Equals-True', NumberEqualsTrue); addBenchmark('Number-Equals-True', NumberEqualsTrue);
...@@ -61,6 +63,61 @@ function equals(a, b) { ...@@ -61,6 +63,61 @@ function equals(a, b) {
} }
} }
function testStrictEqualsBool(a) {
var ret;
for (var i = 0; i < 1000; ++i) {
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
if (a === true || a === false) ret = true;
}
return ret;
}
// Relational comparison handlers are similar, so use one benchmark to measure // Relational comparison handlers are similar, so use one benchmark to measure
// all of them. // all of them.
function relationalCompare(a, b) { function relationalCompare(a, b) {
...@@ -132,6 +189,14 @@ function StringEqualsTrue() { ...@@ -132,6 +189,14 @@ function StringEqualsTrue() {
equals("abc", "abc"); equals("abc", "abc");
} }
function BooleanStrictEqualsTrue() {
testStrictEqualsBool(true);
}
function BooleanStrictEqualsFalse() {
testStrictEqualsBool("10");
}
function MixedEquals() { function MixedEquals() {
equals(10, "10"); equals(10, "10");
} }
......
...@@ -405,6 +405,9 @@ ...@@ -405,6 +405,9 @@
{"name": "String-StrictEquals-True"}, {"name": "String-StrictEquals-True"},
{"name": "String-StrictEquals-False"}, {"name": "String-StrictEquals-False"},
{"name": "SmiString-StrictEquals"}, {"name": "SmiString-StrictEquals"},
{"name": "Boolean-StrictEquals-True"},
{"name": "Boolean-StrictEquals-False"},
{"name": "SmiString-RelationalCompare"}
{"name": "Smi-Equals-True"}, {"name": "Smi-Equals-True"},
{"name": "Smi-Equals-False"}, {"name": "Smi-Equals-False"},
{"name": "Number-Equals-True"}, {"name": "Number-Equals-True"},
......
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