[kernel] Load classes lazily

This CL gives the option to load classes lazily (on by default),
meaning that fields, procedures, constructors and redirecting
factory constructors of classes are not initially read,
but instead read when (or if) needed.

The idea being that many classes aren’t needed, and that
spending time on reading them is thus just a waste of time.

This is especially true in modular compilation where a lot
of modules needs to be given as input as the compilation at
hand might technically rely on it, but in practice only very
few of the classes are actually needed (or very few classes
actually need their members).

The below numbers are comparing a VM without this change and without
the lazy class hierarchy member lists change with
b31566b297 (the base change for those
two changes).

For running a simple hello-world script, on my machine 500 times
and doing statistics on it I can measure the following differences:

With verbose, from kernel_service startup to compile complete:
Difference at 95.0% confidence
        -17.8 +/- 0.555817
        -24.3455% +/- 0.760206%
        (Student's t, pooled s = 4.48379)

With verbose, total runtime measure by `time`
Difference at 95.0% confidence
        -19.346 +/- 0.905217
        -12.0333% +/- 0.563051%
        (Student's t, pooled s = 7.30242)

Without verbose, total runtime measure by `time`
Difference at 95.0% confidence
        -17.862 +/- 0.842905
        -12.0635% +/- 0.569276%
        (Student's t, pooled s = 6.79974)

For running a number of modular compilations I’ve recorded an
improvement in actual runtime in the order of 20-37% depending
on the circumstance. In a real build system, though, the
difference seems to be more in the order of around 5%.

Change-Id: Id329bcf2b01d12c12d7a49f2b8abacd9c2447f05
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/115703
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
This commit is contained in:
Jens Johansen 2019-09-12 08:08:33 +00:00 committed by commit-bot@chromium.org
parent 2bfe714d42
commit ce5556ab36
11 changed files with 361 additions and 59 deletions

View file

@ -717,7 +717,8 @@ class IncrementalCompiler implements IncrementalKernelGenerator {
if (summaryBytes != null) {
ticker.logMs("Read ${c.options.sdkSummary}");
data.component = c.options.target.configureComponent(new Component());
new BinaryBuilder(summaryBytes, disableLazyReading: false)
new BinaryBuilder(summaryBytes,
disableLazyReading: false, disableLazyClassReading: true)
.readComponent(data.component);
ticker.logMs("Deserialized ${c.options.sdkSummary}");
bytesLength += summaryBytes.length;

View file

@ -0,0 +1,59 @@
// 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.
import 'package:front_end/src/fasta/kernel/utils.dart' show serializeComponent;
import 'package:kernel/ast.dart' show Component;
import 'package:kernel/binary/ast_from_binary.dart' show BinaryBuilder;
import 'package:kernel/target/targets.dart' show NoneTarget, TargetFlags;
import 'incremental_load_from_dill_test.dart'
show checkIsEqual, getOptions, normalCompilePlain;
main() async {
final Uri dart2jsUrl = Uri.base.resolve("pkg/compiler/bin/dart2js.dart");
Stopwatch stopwatch = new Stopwatch()..start();
Component compiledComponent = await normalCompilePlain(dart2jsUrl,
options: getOptions()
..target = new NoneTarget(new TargetFlags())
..omitPlatform = false);
print("Compiled dart2js in ${stopwatch.elapsedMilliseconds} ms");
stopwatch.reset();
List<int> bytes = serializeComponent(compiledComponent);
print("Serialized dart2js in ${stopwatch.elapsedMilliseconds} ms");
print("Output is ${bytes.length} bytes long.");
print("");
stopwatch.reset();
print("Round-tripping with lazy disabled");
roundTrip(
new BinaryBuilder(bytes,
disableLazyReading: true, disableLazyClassReading: true),
bytes);
print("Round-tripping with lazy enabled");
roundTrip(
new BinaryBuilder(bytes,
disableLazyReading: false, disableLazyClassReading: false),
bytes);
print("OK");
}
void roundTrip(BinaryBuilder binaryBuilder, List<int> bytes) {
Stopwatch stopwatch = new Stopwatch()..start();
Component c = new Component();
binaryBuilder.readComponent(c);
List<int> bytesRoundTripped = serializeComponent(c);
print("Loaded and serialized in ${stopwatch.elapsedMilliseconds} ms");
stopwatch.reset();
checkIsEqual(bytes, bytesRoundTripped);
print("Checked equal in ${stopwatch.elapsedMilliseconds} ms");
stopwatch.reset();
print("");
}

View file

@ -84,7 +84,7 @@ Future<void> testDart2jsCompile() async {
bool initializeResult = await initializedCompile(
dart2jsUrl, fullDillFromInitialized, initializeWith, [invalidateUri],
options: getOptions()..target = new NoneTarget(new TargetFlags()));
Expect.equals(initializeResult, initializeExpect);
Expect.equals(initializeExpect, initializeResult);
print("Initialized compile(s) from ${initializeWith.pathSegments.last} "
"took ${stopwatch.elapsedMilliseconds} ms");

View file

@ -839,14 +839,20 @@ Future<List<int>> normalCompileToBytes(Uri input,
Future<Component> normalCompileToComponent(Uri input,
{CompilerOptions options, IncrementalCompiler compiler}) async {
options ??= getOptions();
compiler ??= new TestIncrementalCompiler(options, input);
Component component = await compiler.computeDelta();
Component component =
await normalCompilePlain(input, options: options, compiler: compiler);
util.throwOnEmptyMixinBodies(component);
util.throwOnInsufficientUriToSource(component);
return component;
}
Future<Component> normalCompilePlain(Uri input,
{CompilerOptions options, IncrementalCompiler compiler}) async {
options ??= getOptions();
compiler ??= new TestIncrementalCompiler(options, input);
return await compiler.computeDelta();
}
Future<bool> initializedCompile(
Uri input, Uri output, Uri initializeWith, List<Uri> invalidateUris,
{CompilerOptions options}) async {

View file

@ -229,6 +229,8 @@ digests
dijkstra's
directed
directions
dirtify
dirtifying
dirty
disallow
disambiguator
@ -638,6 +640,8 @@ realign
reassigned
rebind
rebuild
recalculating
recalculation
recall
received
recompiled

View file

@ -210,6 +210,7 @@ la
lc
ld
le
lengths
lightly
likewise
linebreaks
@ -312,6 +313,7 @@ segment
shipped
shot
signalled
somehow
spans
spell
spellcheck
@ -350,6 +352,8 @@ triangle
trigger
triggers
trimming
tripped
tripping
trivially
truncated
tt

View file

@ -35,8 +35,6 @@ import 'package:front_end/src/fasta/util/relativize.dart' show relativizeUri;
import 'package:kernel/ast.dart' show Component, Library;
import 'package:kernel/binary/ast_from_binary.dart' show BinaryBuilder;
import 'package:kernel/binary/ast_to_binary.dart' show BinaryPrinter;
import 'package:kernel/error_formatter.dart' show ErrorFormatter;
@ -333,11 +331,7 @@ class WriteDill extends Step<Component, Uri, ChainContext> {
File generated = new File.fromUri(uri);
IOSink sink = generated.openWrite();
try {
try {
new BinaryPrinter(sink).writeComponentFile(component);
} finally {
component.unbindCanonicalNames();
}
new BinaryPrinter(sink).writeComponentFile(component);
} catch (e, s) {
return fail(uri, e, s);
} finally {
@ -363,21 +357,6 @@ class ReadDill extends Step<Uri, Uri, ChainContext> {
}
}
class Copy extends Step<Component, Component, ChainContext> {
const Copy();
String get name => "copy component";
Future<Result<Component>> run(Component component, _) async {
BytesCollector sink = new BytesCollector();
new BinaryPrinter(sink).writeComponentFile(component);
component.unbindCanonicalNames();
Uint8List bytes = sink.collect();
new BinaryBuilder(bytes).readComponent(component);
return pass(component);
}
}
class BytesCollector implements Sink<List<int>> {
final List<List<int>> lists = <List<int>>[];

View file

@ -0,0 +1,46 @@
import 'dart:io' show File;
import '../test/binary_md_dill_reader.dart' show DillComparer;
import '../test/utils/io_utils.dart' show computeRepoDir;
main(List<String> args) {
if (args.length != 2) {
throw "Expects two arguments: The two files to compare";
}
File fileA = new File(args[0]);
File fileB = new File(args[1]);
List<int> a = fileA.readAsBytesSync();
List<int> b = fileB.readAsBytesSync();
bool shouldCompare = false;
if (a.length != b.length) {
print("Input lengths are different.");
shouldCompare = true;
} else {
for (int i = 0; i < a.length; ++i) {
if (a[i] != b[i]) {
print("Data differs at byte ${i + 1}.");
shouldCompare = true;
}
}
}
if (shouldCompare) {
StringBuffer message = new StringBuffer();
final String repoDir = computeRepoDir();
File binaryMd = new File("$repoDir/pkg/kernel/binary.md");
String binaryMdContent = binaryMd.readAsStringSync();
DillComparer dillComparer = new DillComparer();
if (dillComparer.compare(a, b, binaryMdContent, message)) {
message.writeln(
"Somehow the two different byte-lists compared to the same.");
}
print(message);
} else {
print("Inputs byte-equal!");
}
}

View file

@ -64,6 +64,7 @@
///
library kernel.ast;
import 'dart:collection' show ListBase;
import 'dart:convert' show utf8;
import 'visitor.dart';
@ -200,7 +201,38 @@ abstract class Annotatable {
class Reference {
CanonicalName canonicalName;
NamedNode node;
NamedNode _node;
NamedNode get node {
if (_node == null) {
// Either this is an unbound reference or it belongs to a lazy-loaded
// (and not yet loaded) class. If it belongs to a lazy-loaded class,
// load the class.
CanonicalName canonicalNameParent = canonicalName?.parent;
while (canonicalNameParent != null) {
if (canonicalNameParent.name.startsWith("@")) {
break;
}
canonicalNameParent = canonicalNameParent.parent;
}
if (canonicalNameParent != null) {
NamedNode parentNamedNode =
canonicalNameParent?.parent?.reference?._node;
if (parentNamedNode is Class) {
Class parentClass = parentNamedNode;
if (parentClass.lazyBuilder != null) {
parentClass.ensureLoaded();
}
}
}
}
return _node;
}
void set node(NamedNode node) {
_node = node;
}
String toString() {
if (canonicalName != null) {
@ -745,6 +777,41 @@ enum ClassLevel {
Body,
}
/// List-wrapper that marks the parent-class as dirty if the list is modified.
///
/// The idea being, that for non-dirty classes (classes just loaded from dill)
/// the canonical names has already been calculated, and recalculating them is
/// not needed. If, however, we change anything, recalculation of the canonical
/// names can be needed.
class DirtifyingList<E> extends ListBase<E> {
final Class dirtifyClass;
final List<E> wrapped;
DirtifyingList(this.dirtifyClass, this.wrapped);
@override
int get length {
return wrapped.length;
}
@override
void set length(int length) {
dirtifyClass.dirty = true;
wrapped.length = length;
}
@override
E operator [](int index) {
return wrapped[index];
}
@override
void operator []=(int index, E value) {
dirtifyClass.dirty = true;
wrapped[index] = value;
}
}
/// Declaration of a regular class or a mixin application.
///
/// Mixin applications may not contain fields or procedures, as they implicitly
@ -885,23 +952,92 @@ class Class extends NamedNode implements Annotatable, FileUriNode {
/// The types from the `implements` clause.
final List<Supertype> implementedTypes;
/// Internal. Should *ONLY* be used from within kernel.
///
/// If non-null, the function that will have to be called to fill-out the
/// content of this class. Note that this should not be called directly though.
void Function() lazyBuilder;
/// Makes sure the class is loaded, i.e. the fields, procedures etc have been
/// loaded from the dill. Generally, one should not need to call this as it is
/// done automatically when accessing the lists.
void ensureLoaded() {
if (lazyBuilder != null) {
var lazyBuilderLocal = lazyBuilder;
lazyBuilder = null;
lazyBuilderLocal();
}
}
/// Internal. Should *ONLY* be used from within kernel.
///
/// Used for adding fields when reading the dill file.
final List<Field> fieldsInternal;
DirtifyingList<Field> _fieldsView;
/// Fields declared in the class.
///
/// For mixin applications this should be empty.
final List<Field> fields;
List<Field> get fields {
ensureLoaded();
// If already dirty the caller just might as well add stuff directly too.
if (dirty) return fieldsInternal;
_fieldsView ??= new DirtifyingList(this, fieldsInternal);
return _fieldsView;
}
/// Internal. Should *ONLY* be used from within kernel.
///
/// Used for adding constructors when reading the dill file.
final List<Constructor> constructorsInternal;
DirtifyingList<Constructor> _constructorsView;
/// Constructors declared in the class.
final List<Constructor> constructors;
List<Constructor> get constructors {
ensureLoaded();
// If already dirty the caller just might as well add stuff directly too.
if (dirty) return constructorsInternal;
_constructorsView ??= new DirtifyingList(this, constructorsInternal);
return _constructorsView;
}
/// Internal. Should *ONLY* be used from within kernel.
///
/// Used for adding procedures when reading the dill file.
final List<Procedure> proceduresInternal;
DirtifyingList<Procedure> _proceduresView;
/// Procedures declared in the class.
///
/// For mixin applications this should only contain forwarding stubs.
final List<Procedure> procedures;
List<Procedure> get procedures {
ensureLoaded();
// If already dirty the caller just might as well add stuff directly too.
if (dirty) return proceduresInternal;
_proceduresView ??= new DirtifyingList(this, proceduresInternal);
return _proceduresView;
}
/// Internal. Should *ONLY* be used from within kernel.
///
/// Used for adding redirecting factory constructor when reading the dill
/// file.
final List<RedirectingFactoryConstructor>
redirectingFactoryConstructorsInternal;
DirtifyingList<RedirectingFactoryConstructor>
_redirectingFactoryConstructorsView;
/// Redirecting factory constructors declared in the class.
///
/// For mixin applications this should be empty.
final List<RedirectingFactoryConstructor> redirectingFactoryConstructors;
List<RedirectingFactoryConstructor> get redirectingFactoryConstructors {
ensureLoaded();
// If already dirty the caller just might as well add stuff directly too.
if (dirty) return redirectingFactoryConstructorsInternal;
_redirectingFactoryConstructorsView ??=
new DirtifyingList(this, redirectingFactoryConstructorsInternal);
return _redirectingFactoryConstructorsView;
}
Class(
{this.name,
@ -919,23 +1055,24 @@ class Class extends NamedNode implements Annotatable, FileUriNode {
Reference reference})
: this.typeParameters = typeParameters ?? <TypeParameter>[],
this.implementedTypes = implementedTypes ?? <Supertype>[],
this.fields = fields ?? <Field>[],
this.constructors = constructors ?? <Constructor>[],
this.procedures = procedures ?? <Procedure>[],
this.redirectingFactoryConstructors =
this.fieldsInternal = fields ?? <Field>[],
this.constructorsInternal = constructors ?? <Constructor>[],
this.proceduresInternal = procedures ?? <Procedure>[],
this.redirectingFactoryConstructorsInternal =
redirectingFactoryConstructors ?? <RedirectingFactoryConstructor>[],
super(reference) {
setParents(this.typeParameters, this);
setParents(this.constructors, this);
setParents(this.procedures, this);
setParents(this.fields, this);
setParents(this.redirectingFactoryConstructors, this);
setParents(this.constructorsInternal, this);
setParents(this.proceduresInternal, this);
setParents(this.fieldsInternal, this);
setParents(this.redirectingFactoryConstructorsInternal, this);
this.isAbstract = isAbstract;
this.isAnonymousMixin = isAnonymousMixin;
}
void computeCanonicalNames() {
assert(canonicalName != null);
if (!dirty) return;
for (int i = 0; i < fields.length; ++i) {
Field member = fields[i];
canonicalName.getChildFromMember(member).bindTo(member.reference);
@ -1008,20 +1145,27 @@ class Class extends NamedNode implements Annotatable, FileUriNode {
/// The library containing this class.
Library get enclosingLibrary => parent;
/// Internal. Should *ONLY* be used from within kernel.
///
/// If true we have to compute canonical names for all children of this class.
/// if false we can skip it.
bool dirty = true;
/// Adds a member to this class.
///
/// Throws an error if attempting to add a field or procedure to a mixin
/// application.
void addMember(Member member) {
dirty = true;
member.parent = this;
if (member is Constructor) {
constructors.add(member);
constructorsInternal.add(member);
} else if (member is Procedure) {
procedures.add(member);
proceduresInternal.add(member);
} else if (member is Field) {
fields.add(member);
fieldsInternal.add(member);
} else if (member is RedirectingFactoryConstructor) {
redirectingFactoryConstructors.add(member);
redirectingFactoryConstructorsInternal.add(member);
} else {
throw new ArgumentError(member);
}
@ -6326,6 +6470,14 @@ class Component extends TreeNode {
}
void unbindCanonicalNames() {
// TODO(jensj): Get rid of this.
for (int i = 0; i < libraries.length; i++) {
Library lib = libraries[i];
for (int j = 0; j < lib.classes.length; j++) {
Class c = lib.classes[j];
c.dirty = true;
}
}
root.unbindAll();
}

View file

@ -87,11 +87,21 @@ class BinaryBuilder {
/// will not be resolved correctly.
bool _disableLazyReading = false;
/// If binary contains metadata section with payloads referencing other nodes
/// such Kernel binary can't be read lazily because metadata cross references
/// will not be resolved correctly.
bool _disableLazyClassReading = false;
/// Note that [disableLazyClassReading] is incompatible
/// with checkCanonicalNames on readComponent.
BinaryBuilder(this._bytes,
{this.filename,
bool disableLazyReading = false,
bool disableLazyClassReading = false,
bool alwaysCreateNewNamedNodes})
: _disableLazyReading = disableLazyReading,
_disableLazyClassReading =
disableLazyReading || disableLazyClassReading,
this.alwaysCreateNewNamedNodes = alwaysCreateNewNamedNodes ?? false;
fail(String message) {
@ -687,6 +697,8 @@ class BinaryBuilder {
component.mainMethodName ??= mainMethod;
_byteOffset = _componentStartOffset + componentFileSize;
assert(typeParameterStack.isEmpty);
}
/// Read a list of strings. If the list is empty, [null] is returned.
@ -1016,7 +1028,9 @@ class BinaryBuilder {
}
bool shouldWriteData = node == null || _isReadingLibraryImplementation;
if (node == null) {
node = new Class(reference: reference)..level = ClassLevel.Temporary;
node = new Class(reference: reference)
..level = ClassLevel.Temporary
..dirty = false;
}
var fileUri = readUriReference();
@ -1036,6 +1050,9 @@ class BinaryBuilder {
debugPath.add(node.name ?? 'normal-class');
return true;
}());
assert(typeParameterStack.length == 0);
readAndPushTypeParameterList(node.typeParameters, node);
var supertype = readSupertypeOption();
var mixedInType = readSupertypeOption();
@ -1044,16 +1061,12 @@ class BinaryBuilder {
} else {
_skipNodeList(readSupertype);
}
_mergeNamedNodeList(node.fields, (index) => readField(), node);
_mergeNamedNodeList(node.constructors, (index) => readConstructor(), node);
if (_disableLazyClassReading) {
readClassPartialContent(node, procedureOffsets);
} else {
_setLazyLoadClass(node, procedureOffsets);
}
_mergeNamedNodeList(node.procedures, (index) {
_byteOffset = procedureOffsets[index];
return readProcedure(procedureOffsets[index + 1]);
}, node);
_byteOffset = procedureOffsets.last;
_mergeNamedNodeList(node.redirectingFactoryConstructors,
(index) => readRedirectingFactoryConstructor(), node);
typeParameterStack.length = 0;
assert(debugPath.removeLast() != null);
if (shouldWriteData) {
@ -1120,6 +1133,39 @@ class BinaryBuilder {
return node;
}
/// Reads the partial content of a class, namely fields, procedures,
/// constructors and redirecting factory constructors.
void readClassPartialContent(Class node, List<int> procedureOffsets) {
_mergeNamedNodeList(node.fieldsInternal, (index) => readField(), node);
_mergeNamedNodeList(
node.constructorsInternal, (index) => readConstructor(), node);
_mergeNamedNodeList(node.proceduresInternal, (index) {
_byteOffset = procedureOffsets[index];
return readProcedure(procedureOffsets[index + 1]);
}, node);
_byteOffset = procedureOffsets.last;
_mergeNamedNodeList(node.redirectingFactoryConstructorsInternal,
(index) => readRedirectingFactoryConstructor(), node);
}
/// Set the lazyBuilder on the class so it can be lazy loaded in the future.
void _setLazyLoadClass(Class node, List<int> procedureOffsets) {
final int savedByteOffset = _byteOffset;
final int componentStartOffset = _componentStartOffset;
final Library currentLibrary = _currentLibrary;
node.lazyBuilder = () {
_byteOffset = savedByteOffset;
_currentLibrary = currentLibrary;
assert(typeParameterStack.isEmpty);
_componentStartOffset = componentStartOffset;
typeParameterStack.addAll(node.typeParameters);
readClassPartialContent(node, procedureOffsets);
typeParameterStack.length = 0;
};
}
int getAndResetTransformerFlags() {
int flags = _transformerFlags;
_transformerFlags = 0;
@ -1416,8 +1462,7 @@ class BinaryBuilder {
..fileEndOffset = endOffset;
if (lazyLoadBody) {
_setLazyLoadFunction(result, oldLabelStackBase, variableStackHeight,
typeParameterStackHeight);
_setLazyLoadFunction(result, oldLabelStackBase, variableStackHeight);
}
labelStackBase = oldLabelStackBase;
@ -1427,8 +1472,8 @@ class BinaryBuilder {
return result;
}
void _setLazyLoadFunction(FunctionNode result, int oldLabelStackBase,
int variableStackHeight, int typeParameterStackHeight) {
void _setLazyLoadFunction(
FunctionNode result, int oldLabelStackBase, int variableStackHeight) {
final int savedByteOffset = _byteOffset;
final int componentStartOffset = _componentStartOffset;
final List<TypeParameter> typeParameters = typeParameterStack.toList();
@ -1447,7 +1492,7 @@ class BinaryBuilder {
result.body?.parent = result;
labelStackBase = oldLabelStackBase;
variableStack.length = variableStackHeight;
typeParameterStack.length = typeParameterStackHeight;
typeParameterStack.clear();
if (result.parent is Procedure) {
Procedure parent = result.parent;
parent.transformerFlags |= getAndResetTransformerFlags();

View file

@ -179,6 +179,12 @@ class CanonicalName {
void unbind() {
if (reference == null) return;
assert(reference.canonicalName == this);
if (reference.node is Class) {
// TODO(jensj): Get rid of this. This is only needed because pkg:vm does
// weird stuff in transformations. `unbind` should probably be private.
Class c = reference.node;
c.ensureLoaded();
}
reference.canonicalName = null;
reference = null;
}