Reland "[wasm] Lazy compilation after deserialization"

The original CL introduced a test that does not work when it is executed
concurrently on multiple isolates. This CL skips this test
configuration.

Original change's description:
> [wasm] Lazy compilation after deserialization
>
> The serialization format contains one boolean flag per function which
> specifies whether the function code exists in the serialized module or
> not. With this CL, this boolean flag is extended to a three-value flag
> which indicates whether the function exists, and if not, whether the
> function was executed before serialization. This information can then be
> used upon deserialization to compile only those functions that were
> executed before serialization.
>
> Design doc: https://docs.google.com/document/d/1U3uqq4njqLqFhr1G2sU_bmpQxY-3bvfG55udSb-DvA4/edit?usp=sharing
>
> Bug: v8:12281

Change-Id: I36ce90b37736172aa01c47ab04e154ec8ea2d8aa
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3380590
Reviewed-by: Clemens Backes <clemensb@chromium.org>
Commit-Queue: Andreas Haas <ahaas@chromium.org>
Cr-Commit-Position: refs/heads/main@{#78564}
This commit is contained in:
Andreas Haas 2022-01-11 11:52:49 +01:00 committed by V8 LUCI CQ
parent e7f92432db
commit eb129a5cf1
20 changed files with 185 additions and 62 deletions

View File

@ -987,6 +987,7 @@ DEFINE_BOOL(wasm_tier_up, true,
"have an effect)")
DEFINE_BOOL(wasm_dynamic_tiering, false,
"enable dynamic tier up to the optimizing compiler")
DEFINE_NEG_NEG_IMPLICATION(liftoff, wasm_dynamic_tiering)
DEFINE_WEAK_IMPLICATION(future, wasm_dynamic_tiering)
DEFINE_INT(wasm_tiering_budget, 1800000,
"budget for dynamic tiering (rough approximation of bytes executed")

View File

@ -319,15 +319,12 @@ RUNTIME_FUNCTION(Runtime_GetWasmExceptionValues) {
return *isolate->factory()->NewJSArrayWithElements(values);
}
// Wait until the given module is fully tiered up, then serialize it into an
// array buffer.
RUNTIME_FUNCTION(Runtime_SerializeWasmModule) {
HandleScope scope(isolate);
DCHECK_EQ(1, args.length());
CONVERT_ARG_HANDLE_CHECKED(WasmModuleObject, module_obj, 0);
wasm::NativeModule* native_module = module_obj->native_module();
native_module->compilation_state()->WaitForTopTierFinished();
DCHECK(!native_module->compilation_state()->failed());
wasm::WasmSerializer wasm_serializer(native_module);
@ -470,6 +467,21 @@ RUNTIME_FUNCTION(Runtime_IsLiftoffFunction) {
return isolate->heap()->ToBoolean(code && code->is_liftoff());
}
RUNTIME_FUNCTION(Runtime_IsTurboFanFunction) {
HandleScope scope(isolate);
DCHECK_EQ(1, args.length());
CONVERT_ARG_HANDLE_CHECKED(JSFunction, function, 0);
CHECK(WasmExportedFunction::IsWasmExportedFunction(*function));
Handle<WasmExportedFunction> exp_fun =
Handle<WasmExportedFunction>::cast(function);
wasm::NativeModule* native_module =
exp_fun->instance().module_object().native_module();
uint32_t func_index = exp_fun->function_index();
wasm::WasmCodeRefScope code_ref_scope;
wasm::WasmCode* code = native_module->GetCode(func_index);
return isolate->heap()->ToBoolean(code && code->is_turbofan());
}
RUNTIME_FUNCTION(Runtime_FreezeWasmLazyCompilation) {
DCHECK_EQ(1, args.length());
DisallowGarbageCollection no_gc;

View File

@ -608,6 +608,7 @@ namespace internal {
F(GetWasmRecoveredTrapCount, 0, 1) \
F(IsAsmWasmCode, 1, 1) \
F(IsLiftoffFunction, 1, 1) \
F(IsTurboFanFunction, 1, 1) \
F(IsThreadInWasm, 0, 1) \
F(IsWasmCode, 1, 1) \
F(IsWasmTrapHandlerEnabled, 0, 1) \

View File

@ -158,7 +158,8 @@ class V8_EXPORT_PRIVATE CompilationState {
void AddCallback(std::unique_ptr<CompilationEventCallback> callback);
void InitializeAfterDeserialization(
base::Vector<const int> missing_functions);
base::Vector<const int> lazy_functions,
base::Vector<const int> liftoff_functions);
// Wait until top tier compilation finished, or compilation failed.
void WaitForTopTierFinished();

View File

@ -556,7 +556,8 @@ class CompilationStateImpl {
// Initialize the compilation progress after deserialization. This is needed
// for recompilation (e.g. for tier down) to work later.
void InitializeCompilationProgressAfterDeserialization(
base::Vector<const int> missing_functions);
base::Vector<const int> lazy_functions,
base::Vector<const int> liftoff_functions);
// Initializes compilation units based on the information encoded in the
// {compilation_progress_}.
@ -666,7 +667,7 @@ class CompilationStateImpl {
private:
uint8_t SetupCompilationProgressForFunction(
bool lazy_module, NativeModule* module,
bool lazy_function, NativeModule* module,
const WasmFeatures& enabled_features, int func_index);
// Returns the potentially-updated {function_progress}.
@ -849,9 +850,10 @@ void CompilationState::WaitForTopTierFinished() {
void CompilationState::SetHighPriority() { Impl(this)->SetHighPriority(); }
void CompilationState::InitializeAfterDeserialization(
base::Vector<const int> missing_functions) {
base::Vector<const int> lazy_functions,
base::Vector<const int> liftoff_functions) {
Impl(this)->InitializeCompilationProgressAfterDeserialization(
missing_functions);
lazy_functions, liftoff_functions);
}
bool CompilationState::failed() const { return Impl(this)->failed(); }
@ -3031,12 +3033,12 @@ bool CompilationStateImpl::cancelled() const {
}
uint8_t CompilationStateImpl::SetupCompilationProgressForFunction(
bool lazy_module, NativeModule* native_module,
bool lazy_function, NativeModule* native_module,
const WasmFeatures& enabled_features, int func_index) {
ExecutionTierPair requested_tiers =
GetRequestedExecutionTiers(native_module, enabled_features, func_index);
CompileStrategy strategy = GetCompileStrategy(
native_module->module(), enabled_features, func_index, lazy_module);
native_module->module(), enabled_features, func_index, lazy_function);
bool required_for_baseline = strategy == CompileStrategy::kEager;
bool required_for_top_tier = strategy != CompileStrategy::kLazy;
@ -3200,9 +3202,11 @@ void CompilationStateImpl::AddCompilationUnit(CompilationUnitBuilder* builder,
}
void CompilationStateImpl::InitializeCompilationProgressAfterDeserialization(
base::Vector<const int> missing_functions) {
TRACE_EVENT1("v8.wasm", "wasm.CompilationAfterDeserialization",
"num_missing_functions", missing_functions.size());
base::Vector<const int> lazy_functions,
base::Vector<const int> liftoff_functions) {
TRACE_EVENT2("v8.wasm", "wasm.CompilationAfterDeserialization",
"num_lazy_functions", lazy_functions.size(),
"num_liftoff_functions", liftoff_functions.size());
TimedHistogramScope lazy_compile_time_scope(
counters()->wasm_compile_after_deserialize());
@ -3212,21 +3216,35 @@ void CompilationStateImpl::InitializeCompilationProgressAfterDeserialization(
{
base::MutexGuard guard(&callbacks_mutex_);
DCHECK(compilation_progress_.empty());
constexpr uint8_t kProgressAfterDeserialization =
constexpr uint8_t kProgressAfterTurbofanDeserialization =
RequiredBaselineTierField::encode(ExecutionTier::kTurbofan) |
RequiredTopTierField::encode(ExecutionTier::kTurbofan) |
ReachedTierField::encode(ExecutionTier::kTurbofan);
finished_events_.Add(CompilationEvent::kFinishedExportWrappers);
if (missing_functions.empty() || FLAG_wasm_lazy_compilation) {
if (liftoff_functions.empty() || lazy_module) {
// We have to trigger the compilation events to finish compilation.
// Typically the events get triggered when a CompilationUnit finishes, but
// with lazy compilation there are no compilation units.
finished_events_.Add(CompilationEvent::kFinishedBaselineCompilation);
finished_events_.Add(CompilationEvent::kFinishedTopTierCompilation);
}
compilation_progress_.assign(module->num_declared_functions,
kProgressAfterDeserialization);
for (auto func_index : missing_functions) {
if (FLAG_wasm_lazy_compilation) {
kProgressAfterTurbofanDeserialization);
for (auto func_index : lazy_functions) {
native_module_->UseLazyStub(func_index);
compilation_progress_[declared_function_index(module, func_index)] =
SetupCompilationProgressForFunction(/*lazy_function =*/true,
native_module_, enabled_features,
func_index);
}
for (auto func_index : liftoff_functions) {
if (lazy_module) {
native_module_->UseLazyStub(func_index);
}
// Check that {func_index} is not contained in {lazy_functions}.
DCHECK_EQ(
compilation_progress_[declared_function_index(module, func_index)],
kProgressAfterTurbofanDeserialization);
compilation_progress_[declared_function_index(module, func_index)] =
SetupCompilationProgressForFunction(lazy_module, native_module_,
enabled_features, func_index);

View File

@ -292,7 +292,11 @@ class V8_EXPORT_PRIVATE WasmCode final {
uint32_t raw_tagged_parameter_slots_for_serialization() const {
return tagged_parameter_slots_;
}
bool is_liftoff() const { return tier() == ExecutionTier::kLiftoff; }
bool is_turbofan() const { return tier() == ExecutionTier::kTurbofan; }
bool contains(Address pc) const {
return reinterpret_cast<Address>(instructions_) <= pc &&
pc < reinterpret_cast<Address>(instructions_ + instructions_size_);

View File

@ -30,6 +30,9 @@ namespace internal {
namespace wasm {
namespace {
constexpr uint8_t kLazyFunction = 2;
constexpr uint8_t kLiftoffFunction = 3;
constexpr uint8_t kTurboFanFunction = 4;
// TODO(bbudge) Try to unify the various implementations of readers and writers
// in Wasm, e.g. StreamProcessor and ZoneBuffer, with these.
@ -189,7 +192,7 @@ uint32_t GetWasmCalleeTag(RelocInfo* rinfo) {
constexpr size_t kHeaderSize = sizeof(size_t); // total code size
constexpr size_t kCodeHeaderSize = sizeof(bool) + // whether code is present
constexpr size_t kCodeHeaderSize = sizeof(uint8_t) + // code kind
sizeof(int) + // offset of constant pool
sizeof(int) + // offset of safepoint table
sizeof(int) + // offset of handler table
@ -303,10 +306,10 @@ NativeModuleSerializer::NativeModuleSerializer(
}
size_t NativeModuleSerializer::MeasureCode(const WasmCode* code) const {
if (code == nullptr) return sizeof(bool);
if (code == nullptr) return sizeof(uint8_t);
DCHECK_EQ(WasmCode::kWasmFunction, code->kind());
if (code->tier() != ExecutionTier::kTurbofan) {
return sizeof(bool);
return sizeof(uint8_t);
}
return kCodeHeaderSize + code->instructions().size() +
code->reloc_info().size() + code->source_positions().size() +
@ -330,20 +333,30 @@ void NativeModuleSerializer::WriteHeader(Writer* writer,
}
bool NativeModuleSerializer::WriteCode(const WasmCode* code, Writer* writer) {
DCHECK_IMPLIES(!FLAG_wasm_lazy_compilation, code != nullptr);
if (code == nullptr) {
writer->Write(false);
writer->Write(kLazyFunction);
return true;
}
DCHECK_EQ(WasmCode::kWasmFunction, code->kind());
// Only serialize TurboFan code, as Liftoff code can contain breakpoints or
// non-relocatable constants.
if (code->tier() != ExecutionTier::kTurbofan) {
writer->Write(false);
// We check if the function has been executed already. If so, we serialize
// it as {kLiftoffFunction} so that upon deserialization the function will
// get compiled with Liftoff eagerly. If the function has not been executed
// yet, we serialize it as {kLazyFunction}, and the function will not get
// compiled upon deserialization.
NativeModule* native_module = code->native_module();
uint32_t budget =
native_module->tiering_budget_array()[declared_function_index(
native_module->module(), code->index())];
writer->Write(budget == static_cast<uint32_t>(FLAG_wasm_tiering_budget)
? kLazyFunction
: kLiftoffFunction);
return true;
}
++num_turbofan_functions_;
writer->Write(true);
writer->Write(kTurboFanFunction);
// Write the size of the entire code section, followed by the code header.
writer->Write(code->constant_pool_offset());
writer->Write(code->safepoint_table_offset());
@ -537,8 +550,12 @@ class V8_EXPORT_PRIVATE NativeModuleDeserializer {
bool Read(Reader* reader);
base::Vector<const int> missing_functions() {
return base::VectorOf(missing_functions_);
base::Vector<const int> lazy_functions() {
return base::VectorOf(lazy_functions_);
}
base::Vector<const int> liftoff_functions() {
return base::VectorOf(liftoff_functions_);
}
private:
@ -559,7 +576,8 @@ class V8_EXPORT_PRIVATE NativeModuleDeserializer {
size_t remaining_code_size_ = 0;
base::Vector<byte> current_code_space_;
NativeModule::JumpTablesRef current_jump_tables_;
std::vector<int> missing_functions_;
std::vector<int> lazy_functions_;
std::vector<int> liftoff_functions_;
};
class CopyAndRelocTask : public JobTask {
@ -692,11 +710,16 @@ void NativeModuleDeserializer::ReadHeader(Reader* reader) {
DeserializationUnit NativeModuleDeserializer::ReadCode(int fn_index,
Reader* reader) {
bool has_code = reader->Read<bool>();
if (!has_code) {
missing_functions_.push_back(fn_index);
uint8_t code_kind = reader->Read<uint8_t>();
if (code_kind == kLazyFunction) {
lazy_functions_.push_back(fn_index);
return {};
}
if (code_kind == kLiftoffFunction) {
liftoff_functions_.push_back(fn_index);
return {};
}
int constant_pool_offset = reader->Read<int>();
int safepoint_table_offset = reader->Read<int>();
int handler_table_offset = reader->Read<int>();
@ -873,7 +896,7 @@ MaybeHandle<WasmModuleObject> DeserializeNativeModule(
return {};
}
shared_native_module->compilation_state()->InitializeAfterDeserialization(
deserializer.missing_functions());
deserializer.lazy_functions(), deserializer.liftoff_functions());
wasm_engine->UpdateNativeModuleCache(error, &shared_native_module, isolate);
}

View File

@ -1084,6 +1084,11 @@
# in the module, that can be modified by all instances.
'wasm/wasm-dynamic-tiering': [SKIP],
# The test relies on precise switching of code kinds of wasm functions. With
# multiple isolates that share the wasm functions, the precise switching is
# not possible.
'wasm/serialization-with-compilation-hints': [SKIP],
# waitAsync tests modify the global state (across Isolates)
'harmony/atomics-waitasync': [SKIP],
'harmony/atomics-waitasync-1thread-2timeout': [SKIP],

View File

@ -5,7 +5,6 @@
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --expose-wasm --allow-natives-syntax --expose-gc --no-liftoff
// Flags: --no-wasm-dynamic-tiering
d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");

View File

@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The test needs --wasm-tier-up because we can't serialize and deserialize
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --allow-natives-syntax --wasm-tier-up --no-wasm-dynamic-tiering
// Flags: --allow-natives-syntax --no-liftoff
d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");

View File

@ -2,9 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The test needs --wasm-tier-up because we can't serialize and deserialize
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --allow-natives-syntax --wasm-tier-up --no-wasm-dynamic-tiering
// Flags: --allow-natives-syntax --no-liftoff
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');

View File

@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The test needs --wasm-tier-up because we can't serialize and deserialize
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --allow-natives-syntax --throws --wasm-tier-up
// Flags: --no-wasm-dynamic-tiering
// Flags: --allow-natives-syntax --throws --no-liftoff
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
let kTableSize = 3;

View File

@ -4,7 +4,7 @@
// Flags: --experimental-wasm-eh --allow-natives-syntax
// Disable Liftoff so we can serialize the module.
// Flags: --no-liftoff --no-wasm-dynamic-tiering
// Flags: --no-liftoff
d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");

View File

@ -4,8 +4,7 @@
// The test needs --wasm-tier-up because we can't serialize and deserialize
// Liftoff code.
// Flags: --expose-wasm --allow-natives-syntax --expose-gc --wasm-tier-up
// Flags: --no-wasm-dynamic-tiering
// Flags: --expose-wasm --allow-natives-syntax --expose-gc --no-liftoff
d8.file.execute("test/mjsunit/wasm/wasm-module-builder.js");

View File

@ -2,10 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The test needs --wasm-tier-up because we can't serialize and deserialize
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --allow-natives-syntax --print-wasm-code --wasm-tier-up
// Flags: --no-wasm-dynamic-tiering
// Flags: --allow-natives-syntax --print-wasm-code --no-liftoff
// Just test that printing the code of the following wasm modules does not
// crash.

View File

@ -0,0 +1,61 @@
// Copyright 2022 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.
// Flags: --allow-natives-syntax --expose-gc --wasm-dynamic-tiering --liftoff
// Make the test faster:
// Flags: --wasm-tiering-budget=1000
// This test busy-waits for tier-up to be complete, hence it does not work in
// predictable mode where we only have a single thread.
// Flags: --no-predictable
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
const num_functions = 3;
function create_builder() {
const builder = new WasmModuleBuilder();
builder.addImport("foo", "bar", kSig_i_v);
for (let i = 0; i < num_functions; ++i) {
builder.addFunction('f' + i, kSig_i_v)
.addBody(wasmI32Const(i))
.exportFunc();
}
return builder;
}
const wire_bytes = create_builder().toBuffer();
function serializeModule() {
const module = new WebAssembly.Module(wire_bytes);
let instance = new WebAssembly.Instance(module, {foo: {bar: () => 1}});
// Execute {f1} until it gets tiered up.
while (%IsLiftoffFunction(instance.exports.f1)) {
instance.exports.f1();
}
// Execute {f2} once, so that the module knows that this is a used function.
instance.exports.f2();
const buff = %SerializeWasmModule(module);
return buff;
};
const serialized_module = serializeModule();
// Do some GCs to make sure the first module got collected and removed from the
// module cache.
gc();
gc();
gc();
(function testSerializedModule() {
print(arguments.callee.name);
const module = %DeserializeWasmModule(serialized_module, wire_bytes);
const instance = new WebAssembly.Instance(module, {foo: {bar: () => 1}});
assertTrue(%IsTurboFanFunction(instance.exports.f1));
assertTrue(%IsLiftoffFunction(instance.exports.f2));
assertTrue(
!%IsLiftoffFunction(instance.exports.f0) &&
!%IsTurboFanFunction(instance.exports.f0));
})();

View File

@ -5,7 +5,7 @@
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --wasm-lazy-compilation --allow-natives-syntax --expose-gc
// Flags: --no-liftoff --no-wasm-dynamic-tiering
// Flags: --no-liftoff
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');

View File

@ -23,9 +23,9 @@ function create_builder() {
function check(instance) {
for (let i = 0; i < num_functions; ++i) {
const expect_liftoff = i != 0 && i != 2;
const expect_turbofan = i == 0 || i == 2;
assertEquals(
expect_liftoff, %IsLiftoffFunction(instance.exports['f' + i]),
expect_turbofan, %IsTurboFanFunction(instance.exports['f' + i]),
'function ' + i);
}
}

View File

@ -5,7 +5,7 @@
// The test needs --no-liftoff because we can't serialize and deserialize
// Liftoff code.
// Flags: --allow-natives-syntax --wasm-lazy-compilation --expose-gc
// Flags: --no-liftoff --no-wasm-dynamic-tiering
// Flags: --no-liftoff
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');

View File

@ -72,7 +72,8 @@ INCOMPATIBLE_FLAGS_PER_VARIANT = {
"stress": ["--always-opt", "--no-always-opt",
"--max-inlined-bytecode-size=*",
"--max-inlined-bytecode-size-cumulative=*", "--stress-inline",
"--liftoff-only", "--wasm-speculative-inlining"],
"--liftoff-only", "--wasm-speculative-inlining",
"--wasm-dynamic-tiering"],
"sparkplug": ["--jitless"],
"always_sparkplug": ["--jitless"],
"code_serializer": ["--cache=after-execute", "--cache=full-code-cache",