node/deps/v8/test/unittests/compiler/turboshaft/loop-unrolling-analyzer-unittest.cc
Michaël Zasso 9d7cd9b864
deps: update V8 to 12.8.374.13
PR-URL: https://github.com/nodejs/node/pull/54077
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
2024-08-16 16:03:01 +02:00

422 lines
15 KiB
C++

// Copyright 2024 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/compiler/turboshaft/assembler.h"
#include "src/compiler/turboshaft/loop-unrolling-reducer.h"
#include "test/unittests/compiler/turboshaft/reducer-test.h"
namespace v8::internal::compiler::turboshaft {
#include "src/compiler/turboshaft/define-assembler-macros.inc"
class LoopUnrollingAnalyzerTest : public ReducerTest {};
template <typename T>
class LoopUnrollingAnalyzerTestWithParam
: public LoopUnrollingAnalyzerTest,
public ::testing::WithParamInterface<T> {};
size_t CountLoops(const Graph& graph) {
size_t count = 0;
for (const Block& block : graph.blocks()) {
if (block.IsLoop()) count++;
}
return count;
}
const Block& GetFirstLoop(const Graph& graph) {
DCHECK_GE(CountLoops(graph), 1u);
for (const Block& block : graph.blocks()) {
if (block.IsLoop()) return block;
}
UNREACHABLE();
}
#define BUILTIN_CMP_LIST(V) \
V(Uint32LessThan) \
V(Uint32LessThanOrEqual) \
V(Int32LessThan) \
V(Int32LessThanOrEqual) \
V(Word32Equal)
#define CMP_GREATER_THAN_LIST(V) \
V(Uint32GreaterThan) \
V(Uint32GreaterThanOrEqual) \
V(Int32GreaterThan) \
V(Int32GreaterThanOrEqual)
#define CMP_LIST(V) \
BUILTIN_CMP_LIST(V) \
CMP_GREATER_THAN_LIST(V)
enum class Cmp {
#define DEF_CMP_OP(name) k##name,
CMP_LIST(DEF_CMP_OP)
#undef DEF_CMP_OP
};
std::ostream& operator<<(std::ostream& os, const Cmp& cmp) {
switch (cmp) {
case Cmp::kUint32LessThan:
return os << "<ᵘ";
case Cmp::kUint32LessThanOrEqual:
return os << "<=ᵘ";
case Cmp::kInt32LessThan:
return os << "";
case Cmp::kInt32LessThanOrEqual:
return os << "<=ˢ";
case Cmp::kUint32GreaterThan:
return os << ">ᵘ";
case Cmp::kUint32GreaterThanOrEqual:
return os << ">=ᵘ";
case Cmp::kInt32GreaterThan:
return os << "";
case Cmp::kInt32GreaterThanOrEqual:
return os << ">=ᵘ";
case Cmp::kWord32Equal:
return os << "!=";
}
}
bool IsGreaterThan(Cmp cmp) {
switch (cmp) {
#define GREATER_THAN_CASE(name) \
case Cmp::k##name: \
return true;
CMP_GREATER_THAN_LIST(GREATER_THAN_CASE)
default:
return false;
}
}
Cmp GreaterThanToLessThan(Cmp cmp, ConstOrV<Word32>* left,
ConstOrV<Word32>* right) {
if (IsGreaterThan(cmp)) std::swap(*left, *right);
switch (cmp) {
case Cmp::kUint32GreaterThan:
return Cmp::kUint32LessThan;
case Cmp::kUint32GreaterThanOrEqual:
return Cmp::kUint32LessThanOrEqual;
case Cmp::kInt32GreaterThan:
return Cmp::kInt32LessThan;
case Cmp::kInt32GreaterThanOrEqual:
return Cmp::kInt32LessThanOrEqual;
default:
return cmp;
}
}
#define NO_OVERFLOW_BINOP_LIST(V) \
V(Word32Add) \
V(Word32Sub) \
V(Word32Mul) \
V(Int32Div) \
V(Uint32Div)
#define OVERFLOW_CHECKED_BINOP_LIST(V) \
V(Int32AddCheckOverflow) \
V(Int32SubCheckOverflow) \
V(Int32MulCheckOverflow)
#define BINOP_LIST(V) \
NO_OVERFLOW_BINOP_LIST(V) \
OVERFLOW_CHECKED_BINOP_LIST(V)
enum class Binop {
#define DEF_BINOP_OP(name) k##name,
BINOP_LIST(DEF_BINOP_OP)
#undef DEF_BINOP_OP
};
std::ostream& operator<<(std::ostream& os, const Binop& binop) {
switch (binop) {
case Binop::kWord32Add:
return os << "+";
case Binop::kWord32Sub:
return os << "-";
case Binop::kWord32Mul:
return os << "*";
case Binop::kInt32Div:
return os << "";
case Binop::kUint32Div:
return os << "/ᵘ";
case Binop::kInt32AddCheckOverflow:
return os << "+ᵒ";
case Binop::kInt32SubCheckOverflow:
return os << "-ᵒ";
case Binop::kInt32MulCheckOverflow:
return os << "*ᵒ";
}
}
V<Word32> EmitCmp(TestInstance& test_instance, Cmp cmp, ConstOrV<Word32> left,
ConstOrV<Word32> right) {
cmp = GreaterThanToLessThan(cmp, &left, &right);
switch (cmp) {
#define CASE(name) \
case Cmp::k##name: \
return test_instance.Asm().name(left, right);
BUILTIN_CMP_LIST(CASE)
#undef CASE
default:
UNREACHABLE();
}
}
V<Word32> EmitBinop(TestInstance& test_instance, Binop binop,
ConstOrV<Word32> left, ConstOrV<Word32> right) {
switch (binop) {
#define CASE_NO_OVERFLOW(name) \
case Binop::k##name: \
return test_instance.Asm().name(left, right);
NO_OVERFLOW_BINOP_LIST(CASE_NO_OVERFLOW)
#undef CASE_NO_OVERFLOW
#define CASE_OVERFLOW(name) \
case Binop::k##name: \
return test_instance.Asm().Projection<0>( \
test_instance.Asm().name(left, right));
OVERFLOW_CHECKED_BINOP_LIST(CASE_OVERFLOW)
#undef CASE_OVERFLOW
}
}
struct BoundedLoop {
int init;
Cmp cmp;
int max;
Binop binop;
int increment;
uint32_t expected_iter_count;
const char* name;
};
std::ostream& operator<<(std::ostream& os, const BoundedLoop& loop) {
return os << loop.name;
}
static const BoundedLoop kSmallBoundedLoops[] = {
// Increasing positive counter with add increment.
{0, Cmp::kInt32LessThan, 3, Binop::kWord32Add, 1, 3,
"for (int32_t i = 0; i < 3; i += 1)"},
{0, Cmp::kInt32LessThanOrEqual, 3, Binop::kWord32Add, 1, 4,
"for (int32_t i = 0; i <= 3; i += 1)"},
{0, Cmp::kUint32LessThan, 3, Binop::kWord32Add, 1, 3,
"for (uint32_t i = 0; i < 3; i += 1)"},
{0, Cmp::kUint32LessThanOrEqual, 3, Binop::kWord32Add, 1, 4,
"for (uint32_t i = 0; i <= 3; i += 1)"},
// Decreasing counter with add/sub increment.
{1, Cmp::kInt32GreaterThan, -2, Binop::kWord32Sub, 1, 3,
"for (int32_t i = 1; i > -2; i -= 1)"},
{1, Cmp::kInt32GreaterThan, -2, Binop::kWord32Add, -1, 3,
"for (int32_t i = 1; i > -2; i += -1)"},
{1, Cmp::kInt32GreaterThanOrEqual, -2, Binop::kWord32Sub, 1, 4,
"for (int32_t i = 1; i >= -2; i -= 1)"},
{1, Cmp::kInt32GreaterThanOrEqual, -2, Binop::kWord32Add, -1, 4,
"for (int32_t i = 1; i >= -2; i += -1)"},
// Increasing negative counter with add increment.
{-5, Cmp::kInt32LessThan, -2, Binop::kWord32Add, 1, 3,
"for (int32_t i = -5; i < -2; i += 1)"},
{-5, Cmp::kInt32LessThanOrEqual, -2, Binop::kWord32Add, 1, 4,
"for (int32_t i = -5; i <= -2; i += 1)"},
// Increasing positive counter with mul increment.
{3, Cmp::kInt32LessThan, 13, Binop::kWord32Mul, 2, 3,
"for (int32_t i = 3; i < 13; i *= 2)"},
{3, Cmp::kInt32LessThanOrEqual, 13, Binop::kWord32Mul, 2, 3,
"for (int32_t i = 3; i <= 13; i *= 2)"},
};
static const BoundedLoop kLargeBoundedLoops[] = {
// Increasing positive counter with add increment.
{0, Cmp::kInt32LessThan, 4500, Binop::kWord32Add, 1, 4500,
"for (int32_t i = 0; i < 4500; i += 1)"},
{0, Cmp::kInt32LessThan, 1000000, Binop::kWord32Add, 1, 1000000,
"for (int32_t i = 0; i < 1000000; i += 1)"},
{0, Cmp::kUint32LessThan, 4500, Binop::kWord32Add, 1, 4500,
"for (uint32_t i = 0; i < 4500; i += 1)"},
{0, Cmp::kUint32LessThan, 1000000, Binop::kWord32Add, 1, 1000000,
"for (uint32_t i = 0; i < 1000000; i += 1)"},
// Decreasing counter with add increment.
{700, Cmp::kInt32GreaterThan, -1000, Binop::kWord32Add, -2, 850,
"for (int32_t i = 700; i > -1000; i += -1)"},
{700, Cmp::kInt32GreaterThanOrEqual, -1000, Binop::kWord32Add, -2, 851,
"for (int32_t i = 700; i >= -1000; i += -1)"},
};
static const BoundedLoop kUnderOverflowBoundedLoops[] = {
// Increasing positive to negative with add increment and signed overflow.
// Small loop.
{std::numeric_limits<int32_t>::max() - 2, Cmp::kInt32GreaterThan,
std::numeric_limits<int32_t>::min() + 10, Binop::kWord32Add, 1, 3,
"for (int32_i = MAX_INT-2; i > MIN_INT+10; i += 1)"},
{std::numeric_limits<int32_t>::max() - 2, Cmp::kInt32GreaterThanOrEqual,
std::numeric_limits<int32_t>::min() + 10, Binop::kWord32Add, 1, 3,
"for (int32_i = MAX_INT-2; i >= MIN_INT+10; i += 1)"},
// Larger loop.
{std::numeric_limits<int32_t>::max() - 100, Cmp::kInt32GreaterThan,
std::numeric_limits<int32_t>::min() + 100, Binop::kWord32Add, 1, 200,
"for (int32_i = MAX_INT-100; i > MIN_INT+100; i += 1)"},
{std::numeric_limits<int32_t>::max() - 100, Cmp::kInt32GreaterThanOrEqual,
std::numeric_limits<int32_t>::min() + 100, Binop::kWord32Add, 1, 201,
"for (int32_i = MAX_INT-100; i >= MIN_INT+100; i += 1)"},
// Decreasing negative to positive with add/sub increment and signed
// underflow.
// Small loop.
{std::numeric_limits<int32_t>::min() + 2, Cmp::kInt32LessThan,
std::numeric_limits<int32_t>::max() - 10, Binop::kWord32Add, -1, 3,
"for (int32_t i = MIN_INT+2; i < MAX_INT-10; i += -1)"},
{std::numeric_limits<int32_t>::min() + 2, Cmp::kInt32LessThan,
std::numeric_limits<int32_t>::max() - 10, Binop::kWord32Sub, 1, 3,
"for (int32_t i = MIN_INT+2; i < MAX_INT-10; i -= 1)"},
{std::numeric_limits<int32_t>::min() + 2, Cmp::kInt32LessThanOrEqual,
std::numeric_limits<int32_t>::max() - 10, Binop::kWord32Add, -1, 3,
"for (int32_t i = MIN_INT+2; i <= MAX_INT-10; i += -1)"},
{std::numeric_limits<int32_t>::min() + 2, Cmp::kInt32LessThanOrEqual,
std::numeric_limits<int32_t>::max() - 10, Binop::kWord32Sub, 1, 3,
"for (int32_t i = MIN_INT+2; i <= MAX_INT-10; i -= 1)"},
// Large loop.
{std::numeric_limits<int32_t>::min() + 100, Cmp::kInt32LessThan,
std::numeric_limits<int32_t>::max() - 100, Binop::kWord32Add, -1, 200,
"for (int32_t i = MIN_INT+100; i < MAX_INT-100; i -= 1)"},
{std::numeric_limits<int32_t>::min() + 100, Cmp::kInt32LessThanOrEqual,
std::numeric_limits<int32_t>::max() - 100, Binop::kWord32Add, -1, 201,
"for (int32_t i = MIN_INT+100; i <= MAX_INT-100; i -= 1)"},
};
using LoopUnrollingAnalyzerSmallLoopTest =
LoopUnrollingAnalyzerTestWithParam<BoundedLoop>;
// Checking that the LoopUnrollingAnalyzer correctly computes the number of
// iterations of small loops.
TEST_P(LoopUnrollingAnalyzerSmallLoopTest, ExactLoopIterCount) {
BoundedLoop params = GetParam();
auto test = CreateFromGraph(1, [&params](auto& Asm) {
using AssemblerT = std::remove_reference<decltype(Asm)>::type::Assembler;
OpIndex cond = Asm.GetParameter(0);
ScopedVariable<Word32, AssemblerT> index(&Asm, params.init);
WHILE(EmitCmp(Asm, params.cmp, index, params.max)) {
__ JSLoopStackCheck(__ NoContextConstant(), Asm.BuildFrameState());
// Advance the {index}.
index = EmitBinop(Asm, params.binop, index, params.increment);
}
__ Return(index);
});
LoopUnrollingAnalyzer analyzer(test.zone(), &test.graph(), false);
auto stack_checks_to_remove = test.graph().stack_checks_to_remove();
const Block& loop = GetFirstLoop(test.graph());
ASSERT_EQ(1u, stack_checks_to_remove.size());
EXPECT_TRUE(stack_checks_to_remove.contains(loop.index().id()));
IterationCount iter_count = analyzer.GetIterationCount(&loop);
ASSERT_TRUE(iter_count.IsExact());
EXPECT_EQ(params.expected_iter_count, iter_count.exact_count());
}
INSTANTIATE_TEST_SUITE_P(LoopUnrollingAnalyzerTest,
LoopUnrollingAnalyzerSmallLoopTest,
::testing::ValuesIn(kSmallBoundedLoops));
using LoopUnrollingAnalyzerLargeLoopTest =
LoopUnrollingAnalyzerTestWithParam<BoundedLoop>;
// Checking that the LoopUnrollingAnalyzer correctly computes the number of
// iterations of small loops.
TEST_P(LoopUnrollingAnalyzerLargeLoopTest, LargeLoopIterCount) {
BoundedLoop params = GetParam();
auto test = CreateFromGraph(1, [&params](auto& Asm) {
using AssemblerT = std::remove_reference<decltype(Asm)>::type::Assembler;
OpIndex cond = Asm.GetParameter(0);
ScopedVariable<Word32, AssemblerT> index(&Asm, params.init);
WHILE(EmitCmp(Asm, params.cmp, index, params.max)) {
__ JSLoopStackCheck(__ NoContextConstant(), Asm.BuildFrameState());
// Advance the {index}.
index = EmitBinop(Asm, params.binop, index, params.increment);
}
__ Return(index);
});
LoopUnrollingAnalyzer analyzer(test.zone(), &test.graph(), false);
auto stack_checks_to_remove = test.graph().stack_checks_to_remove();
const Block& loop = GetFirstLoop(test.graph());
if (params.expected_iter_count <=
LoopUnrollingAnalyzer::kMaxIterForStackCheckRemoval) {
EXPECT_EQ(1u, stack_checks_to_remove.size());
EXPECT_TRUE(stack_checks_to_remove.contains(loop.index().id()));
IterationCount iter_count = analyzer.GetIterationCount(&loop);
ASSERT_TRUE(iter_count.IsApprox());
EXPECT_TRUE(iter_count.IsSmallerThan(
LoopUnrollingAnalyzer::kMaxIterForStackCheckRemoval));
} else {
EXPECT_EQ(0u, stack_checks_to_remove.size());
EXPECT_FALSE(stack_checks_to_remove.contains(loop.index().id()));
IterationCount iter_count = analyzer.GetIterationCount(&loop);
ASSERT_TRUE(iter_count.IsApprox());
EXPECT_FALSE(iter_count.IsSmallerThan(
LoopUnrollingAnalyzer::kMaxIterForStackCheckRemoval));
}
}
INSTANTIATE_TEST_SUITE_P(LoopUnrollingAnalyzerTest,
LoopUnrollingAnalyzerLargeLoopTest,
::testing::ValuesIn(kLargeBoundedLoops));
using LoopUnrollingAnalyzerOverflowTest =
LoopUnrollingAnalyzerTestWithParam<BoundedLoop>;
// Checking that the LoopUnrollingAnalyzer correctly computes the number of
// iterations of small loops.
TEST_P(LoopUnrollingAnalyzerOverflowTest, LargeLoopIterCount) {
BoundedLoop params = GetParam();
auto test = CreateFromGraph(1, [&params](auto& Asm) {
using AssemblerT = std::remove_reference<decltype(Asm)>::type::Assembler;
OpIndex cond = Asm.GetParameter(0);
ScopedVariable<Word32, AssemblerT> index(&Asm, params.init);
WHILE(EmitCmp(Asm, params.cmp, index, params.max)) {
__ JSLoopStackCheck(__ NoContextConstant(), Asm.BuildFrameState());
// Advance the {index}.
index = EmitBinop(Asm, params.binop, index, params.increment);
}
__ Return(index);
});
LoopUnrollingAnalyzer analyzer(test.zone(), &test.graph(), false);
auto stack_checks_to_remove = test.graph().stack_checks_to_remove();
const Block& loop = GetFirstLoop(test.graph());
EXPECT_EQ(0u, stack_checks_to_remove.size());
EXPECT_FALSE(stack_checks_to_remove.contains(loop.index().id()));
IterationCount iter_count = analyzer.GetIterationCount(&loop);
EXPECT_TRUE(iter_count.IsUnknown());
}
INSTANTIATE_TEST_SUITE_P(LoopUnrollingAnalyzerTest,
LoopUnrollingAnalyzerOverflowTest,
::testing::ValuesIn(kUnderOverflowBoundedLoops));
#include "src/compiler/turboshaft/undef-assembler-macros.inc"
} // namespace v8::internal::compiler::turboshaft