Files
sdk/runtime/vm/compiler/backend/il_test_helper.h
T
Vyacheslav Egorov 4e027fae97 [vm/compiler] Force unboxing of int phis on 64-bit platforms
More specifically on 64-bit platforms, when other heuristic fail:

- in AOT mode unbox all int typed phis.
- in JIT mode unbox all non-Smi int typed phis i.e. mixed
  Mint/Smi phis.

This brings treatment of int phis closer to how we treat other
boxed phis.

This considerably improves AOT code quality for loops which
our current conservative heuristic does not currently hit,
for example:

  void foo(int start, int end) {
    for (var i = start; i < end; i++) {
    }
  }

Code like this would use a boxed Phi previously which does not make
sense.

In JIT mode to really benefit from aggressive unboxing we
might need to shift from over-reliance on Smi operations, e.g.
when phi is unboxed we want to cascade that through and unbox
operations which are performed on it transitively. This requires
much more sophisticated algorithm and is outside of scope for this
change.

In AOT we in general treat all arithmetic operations as operations
over 64-bit int values so unboxing phis makes more sense.

As a result of this change we see small improvement in code size:

  flutter_gallery_app_so_gzip_size -0.2392%

TEST=ci


Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-linux-release-x64-try
Change-Id: Iae9a16f353e53f649da4e665d2812ad47099a674
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/187860
Commit-Queue: Vyacheslav Egorov <vegorov@google.com>
Reviewed-by: Alexander Markov <alexmarkov@google.com>
2021-03-11 22:20:51 +00:00

387 lines
12 KiB
C++

// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#ifndef RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_
#define RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_
#include <type_traits>
#include <utility>
#include <vector>
#include "include/dart_api.h"
#include "platform/allocation.h"
#include "vm/compiler/backend/flow_graph.h"
#include "vm/compiler/backend/il.h"
#include "vm/compiler/compiler_pass.h"
#include "vm/compiler/compiler_state.h"
#include "vm/compiler/jit/compiler.h"
#include "vm/unit_test.h"
// The helpers in this file make it easier to write C++ unit tests which assert
// that Dart code gets turned into certain IR.
//
// Here is an example on how to use it:
//
// ISOLATE_UNIT_TEST_CASE(MyIRTest) {
// const char* script = R"(
// void foo() { ... }
// void main() { foo(); }
// )";
//
// // Load the script and exercise the code once.
// const auto& lib = Library::Handle(LoadTestScript(script);
//
// // Cause the code to be exercised once (to populate ICData).
// Invoke(lib, "main");
//
// // Look up the function.
// const auto& function = Function::Handle(GetFunction(lib, "foo"));
//
// // Run the JIT compilation pipeline with two passes.
// TestPipeline pipeline(function);
// FlowGraph* graph = pipeline.RunJITPasses("ComputeSSA,TypePropagation");
//
// ...
// }
//
namespace dart {
class FlowGraph;
class Function;
class Library;
LibraryPtr LoadTestScript(const char* script,
Dart_NativeEntryResolver resolver = nullptr,
const char* lib_uri = RESOLVED_USER_TEST_URI);
FunctionPtr GetFunction(const Library& lib, const char* name);
ClassPtr GetClass(const Library& lib, const char* name);
TypeParameterPtr GetClassTypeParameter(const Class& klass, const char* name);
TypeParameterPtr GetFunctionTypeParameter(const Function& fun,
const char* name);
ObjectPtr Invoke(const Library& lib, const char* name);
class TestPipeline : public ValueObject {
public:
explicit TestPipeline(const Function& function,
CompilerPass::PipelineMode mode,
bool is_optimizing = true)
: function_(function),
thread_(Thread::Current()),
compiler_state_(thread_,
mode == CompilerPass::PipelineMode::kAOT,
is_optimizing,
CompilerState::ShouldTrace(function)),
mode_(mode) {}
~TestPipeline() { delete pass_state_; }
// As a side-effect this will populate
// - [ic_data_array_]
// - [parsed_function_]
// - [pass_state_]
// - [flow_graph_]
FlowGraph* RunPasses(std::initializer_list<CompilerPass::Id> passes);
void CompileGraphAndAttachFunction();
private:
const Function& function_;
Thread* thread_;
CompilerState compiler_state_;
CompilerPass::PipelineMode mode_;
ZoneGrowableArray<const ICData*>* ic_data_array_ = nullptr;
ParsedFunction* parsed_function_ = nullptr;
CompilerPassState* pass_state_ = nullptr;
FlowGraph* flow_graph_ = nullptr;
};
// Match opcodes used for [ILMatcher], see below.
enum MatchOpCode {
// Emit a match and match-and-move code for every instruction.
#define DEFINE_MATCH_OPCODES(Instruction, _) \
kMatch##Instruction, kMatchAndMove##Instruction, \
kMatchAndMoveOptional##Instruction,
FOR_EACH_INSTRUCTION(DEFINE_MATCH_OPCODES)
#undef DEFINE_MATCH_OPCODES
// Matches a branch and moves left.
kMatchAndMoveBranchTrue,
// Matches a branch and moves right.
kMatchAndMoveBranchFalse,
// Is ignored.
kNop,
// Moves forward across any instruction.
kMoveAny,
// Moves over all parallel moves.
kMoveParallelMoves,
// Moves forward until the next match code matches.
kMoveGlob,
// Moves over any DebugStepChecks.
kMoveDebugStepChecks,
// Invalid match opcode used as default [insert_before] argument to TryMatch
// to signal that no insertions should occur.
kInvalidMatchOpCode,
};
// Match codes used for [ILMatcher], see below.
class MatchCode {
public:
MatchCode(MatchOpCode opcode) // NOLINT
: opcode_(opcode), capture_(nullptr) {}
MatchCode(MatchOpCode opcode, Instruction** capture)
: opcode_(opcode), capture_(capture) {}
#define DEFINE_TYPED_CONSTRUCTOR(Type, ignored) \
MatchCode(MatchOpCode opcode, Type##Instr** capture) \
: opcode_(opcode), capture_(reinterpret_cast<Instruction**>(capture)) { \
RELEASE_ASSERT(opcode == kMatch##Type || opcode == kMatchAndMove##Type); \
}
FOR_EACH_INSTRUCTION(DEFINE_TYPED_CONSTRUCTOR)
#undef DEFINE_TYPED_CONSTRUCTOR
MatchOpCode opcode() { return opcode_; }
private:
friend class ILMatcher;
MatchOpCode opcode_;
Instruction** capture_;
};
enum class ParallelMovesHandling {
// Matcher doesn't do anything special with ParallelMove instructions.
kDefault,
// All ParallelMove instructions are skipped.
// This mode is useful when matching a flow graph after the whole
// compiler pipeline, as it may have ParallelMove instructions
// at arbitrary architecture-dependent places.
kSkip,
};
// Used for matching a sequence of IL instructions including capturing support.
//
// Example:
//
// TargetEntryInstr* entry = ....;
// BranchInstr* branch = nullptr;
//
// ILMatcher matcher(flow_graph, entry);
// if (matcher.TryMatch({ kMoveGlob, {kMatchBranch, &branch}, })) {
// EXPECT(branch->operation_cid() == kMintCid);
// ...
// }
//
// This match will start at [entry], follow any number instructions (including
// [GotoInstr]s until a [BranchInstr] is found).
//
// If the match was successful, this returns `true` and updates the current
// value for the cursor.
class ILMatcher : public ValueObject {
public:
ILMatcher(FlowGraph* flow_graph,
Instruction* cursor,
bool trace = true,
ParallelMovesHandling parallel_moves_handling =
ParallelMovesHandling::kDefault)
: flow_graph_(flow_graph),
cursor_(cursor),
parallel_moves_handling_(parallel_moves_handling),
// clang-format off
#if !defined(PRODUCT)
trace_(trace) {}
#else
trace_(false) {}
#endif
// clang-format on
Instruction* value() { return cursor_; }
// From the current [value] according to match_codes.
//
// Returns `true` if the match was successful and cursor has been updated,
// otherwise returns `false`.
//
// If [insert_before] is a valid match opcode, then it will be inserted
// before each MatchCode in [match_codes] prior to matching.
bool TryMatch(std::initializer_list<MatchCode> match_codes,
MatchOpCode insert_before = kInvalidMatchOpCode);
private:
Instruction* MatchInternal(std::vector<MatchCode> match_codes,
size_t i,
Instruction* cursor);
const char* MatchOpCodeToCString(MatchOpCode code);
FlowGraph* flow_graph_;
Instruction* cursor_;
ParallelMovesHandling parallel_moves_handling_;
bool trace_;
};
#if !defined(PRODUCT)
#define ENTITY_TOCSTRING(v) ((v)->ToCString())
#else
#define ENTITY_TOCSTRING(v) "<?>"
#endif
// Helper to check various IL and object properties and informative error
// messages if check fails. [entity] should be a pointer to a value.
// [property] should be an expression which can refer to [entity] using
// variable named [it].
// [entity] is expected to have a ToCString() method in non-PRODUCT builds.
#define EXPECT_PROPERTY(entity, property) \
do { \
auto& it = *entity; \
if (!(property)) { \
dart::Expect(__FILE__, __LINE__) \
.Fail("expected " #property " for " #entity " which is %s.\n", \
ENTITY_TOCSTRING(entity)); \
} \
} while (0)
class FlowGraphBuilderHelper {
public:
FlowGraphBuilderHelper()
: state_(CompilerState::Current()),
flow_graph_(MakeDummyGraph(Thread::Current())) {
flow_graph_.CreateCommonConstants();
}
TargetEntryInstr* TargetEntry(intptr_t try_index = kInvalidTryIndex) const {
return new TargetEntryInstr(flow_graph_.allocate_block_id(), try_index,
state_.GetNextDeoptId());
}
JoinEntryInstr* JoinEntry(intptr_t try_index = kInvalidTryIndex) const {
return new JoinEntryInstr(flow_graph_.allocate_block_id(), try_index,
state_.GetNextDeoptId());
}
ConstantInstr* IntConstant(int64_t value) const {
return flow_graph_.GetConstant(
Integer::Handle(Integer::NewCanonical(value)));
}
ConstantInstr* DoubleConstant(double value) {
return flow_graph_.GetConstant(Double::Handle(Double::NewCanonical(value)));
}
enum class IncomingDefKind {
kImmediate,
kDelayed,
};
class IncomingDef {
public:
IncomingDef(BlockEntryInstr* from, Definition* defn)
: kind_(IncomingDefKind::kImmediate), from_(from), defn_(defn) {}
template <typename T,
typename = typename std::enable_if<
std::is_base_of<Definition, T>::value>::type>
IncomingDef(BlockEntryInstr* from, T** defn_source)
: kind_(IncomingDefKind::kDelayed),
from_(from),
defn_source_(reinterpret_cast<Definition**>(defn_source)) {}
BlockEntryInstr* from() const { return from_; }
Definition* defn() const {
return kind_ == IncomingDefKind::kImmediate ? defn_ : *defn_source_;
}
private:
IncomingDefKind kind_;
BlockEntryInstr* from_;
union {
Definition* defn_;
Definition** defn_source_;
};
};
PhiInstr* Phi(JoinEntryInstr* join,
std::initializer_list<IncomingDef> incoming) {
auto phi = new PhiInstr(join, incoming.size());
for (size_t i = 0; i < incoming.size(); i++) {
auto input = new Value(flow_graph_.constant_dead());
phi->SetInputAt(i, input);
input->definition()->AddInputUse(input);
}
for (auto def : incoming) {
pending_phis_.Add({phi, def});
}
return phi;
}
void FinishGraph() {
flow_graph_.DiscoverBlocks();
GrowableArray<BitVector*> dominance_frontier;
flow_graph_.ComputeDominators(&dominance_frontier);
for (auto& pending : pending_phis_) {
auto join = pending.phi->block();
EXPECT(pending.phi->InputCount() == join->PredecessorCount());
auto pred_index = join->IndexOfPredecessor(pending.incoming.from());
EXPECT(pred_index != -1);
pending.phi->InputAt(pred_index)->BindTo(pending.incoming.defn());
}
}
FlowGraph* flow_graph() { return &flow_graph_; }
private:
static FlowGraph& MakeDummyGraph(Thread* thread) {
const FunctionType& signature =
FunctionType::ZoneHandle(FunctionType::New());
const Function& func = Function::ZoneHandle(Function::New(
signature, String::Handle(Symbols::New(thread, "dummy")),
UntaggedFunction::kRegularFunction,
/*is_static=*/true,
/*is_const=*/false,
/*is_abstract=*/false,
/*is_external=*/false,
/*is_native=*/true,
Class::Handle(thread->isolate_group()->object_store()->object_class()),
TokenPosition::kNoSource));
Zone* zone = thread->zone();
ParsedFunction* parsed_function = new (zone) ParsedFunction(thread, func);
parsed_function->set_scope(new LocalScope(nullptr, 0, 0));
auto graph_entry =
new GraphEntryInstr(*parsed_function, Compiler::kNoOSRDeoptId);
const intptr_t block_id = 1; // 0 is GraphEntry.
graph_entry->set_normal_entry(
new FunctionEntryInstr(graph_entry, block_id, kInvalidTryIndex,
CompilerState::Current().GetNextDeoptId()));
return *new FlowGraph(*parsed_function, graph_entry, block_id,
PrologueInfo{-1, -1});
}
CompilerState& state_;
FlowGraph& flow_graph_;
struct PendingPhiInput {
PhiInstr* phi;
IncomingDef incoming;
};
GrowableArray<PendingPhiInput> pending_phis_;
};
} // namespace dart
#endif // RUNTIME_VM_COMPILER_BACKEND_IL_TEST_HELPER_H_