-
Notifications
You must be signed in to change notification settings - Fork 205
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add first version of a view
proposal
#1617
Conversation
view
proposal
(Other things came up, so the new version of this document has not yet been uploaded. I'll upload it after the language meeting). |
Uploaded a version of the document where many typos and glitches have been fixed. It should be in rather good shape now. |
@leafpetersen, I believe we agreed to land this document now? We can then handle further updates/discussions in separate PRs. |
10b4635
to
5c10128
Compare
yield self; | ||
} else if (self is List<dynamic>) { | ||
for (var element in self) { | ||
yield* element.leaves; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this code is quadratic, since every time you yield an element, you re-traverse element.leaves. Might be better to use a different example, or not use sync*?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The number of invocations of leaves
is actually identical to the number of nodes in the given tree, so if there is anything which is too expensive it would be the use of sync*
(it is known that sync*
ought to be optimized better than it is today).
I ran the following:
class TinyJson {
final Object self;
TinyJson(this.self);
Iterable<num> get leaves sync* {
var self = this.self;
if (self is num) {
yield self;
} else if (self is List<dynamic>) {
for (var element in self) {
yield* TinyJson(element).leaves;
}
} else {
throw "Unexpected object encountered in TinyJson value";
}
}
}
var b = false;
void use(dynamic d) {
if (b) d.fooBar(); // Pretend that `d` is not ignored.
try {} finally {} // Prevent inlining.
}
void testFlatList() {
print('---------- FlatList ----------');
for (int length = 1024; length < 1000000000; length *= 2) {
var tiny = TinyJson(List<dynamic>.filled(length, 1));
var sw = Stopwatch()..start();
use(tiny.leaves);
print('Length: $length, time: ${sw.elapsed}');
}
}
void testTwoLevelList() {
print('---------- TwoLevelList ----------');
for (int length = 1024; length < 1000000000; length *= 2) {
var tiny = TinyJson(List<dynamic>.filled(length, <dynamic>[1, 2]));
var sw = Stopwatch()..start();
use(tiny.leaves);
print('Length: $length, time: ${sw.elapsed}');
}
}
void testManyLevelList() {
print('---------- ManyLevelList ----------');
var max = 1000;
List<dynamic> tinies = List.filled(max, [<dynamic>[0]]);
for (int depth = 1; depth < max; depth++) {
var tiny = tinies[depth] = <dynamic>[tinies[depth - 1]];
var sw = Stopwatch()..start();
use(TinyJson(tiny).leaves);
print('Depth: $depth, time: ${sw.elapsed}, tiny: $tiny');
}
}
void main() {
testFlatList();
testTwoLevelList();
testManyLevelList();
}
I got the following output, which doesn't seem to imply anything excessively slow:
---------- FlatList ----------
Length: 1024, time: 0:00:00.001751
Length: 2048, time: 0:00:00.000004
Length: 4096, time: 0:00:00.000001
Length: 8192, time: 0:00:00.000001
Length: 16384, time: 0:00:00.000001
Length: 32768, time: 0:00:00.000001
Length: 65536, time: 0:00:00.000003
Length: 131072, time: 0:00:00.000005
Length: 262144, time: 0:00:00.000002
Length: 524288, time: 0:00:00.000002
Length: 1048576, time: 0:00:00.000002
Length: 2097152, time: 0:00:00.000002
Length: 4194304, time: 0:00:00.000008
Length: 8388608, time: 0:00:00.000008
Length: 16777216, time: 0:00:00.000006
Length: 33554432, time: 0:00:00.000007
Length: 67108864, time: 0:00:00.000008
Length: 134217728, time: 0:00:00.000008
Length: 268435456, time: 0:00:00.000007
Length: 536870912, time: 0:00:00.000008
---------- TwoLevelList ----------
Length: 1024, time: 0:00:00.000006
Length: 2048, time: 0:00:00.000000
Length: 4096, time: 0:00:00.000000
Length: 8192, time: 0:00:00.000000
Length: 16384, time: 0:00:00.000000
Length: 32768, time: 0:00:00.000008
Length: 65536, time: 0:00:00.000003
Length: 131072, time: 0:00:00.000003
Length: 262144, time: 0:00:00.000003
Length: 524288, time: 0:00:00.000002
Length: 1048576, time: 0:00:00.000001
Length: 2097152, time: 0:00:00.000001
Length: 4194304, time: 0:00:00.000002
Length: 8388608, time: 0:00:00.000004
Length: 16777216, time: 0:00:00.000007
Length: 33554432, time: 0:00:00.000007
Length: 67108864, time: 0:00:00.000007
Length: 134217728, time: 0:00:00.000007
Length: 268435456, time: 0:00:00.000007
Length: 536870912, time: 0:00:00.000008
---------- ManyLevelList ----------
Depth: 1, time: 0:00:00.000008
Depth: 2, time: 0:00:00.000000
Depth: 3, time: 0:00:00.000000
Depth: 4, time: 0:00:00.000000
Depth: 5, time: 0:00:00.000000
Depth: 6, time: 0:00:00.000000
Depth: 7, time: 0:00:00.000000
Depth: 8, time: 0:00:00.000000
Depth: 9, time: 0:00:00.000000
Depth: 10, time: 0:00:00.000000
...
Depth: 990, time: 0:00:00.000000
Depth: 991, time: 0:00:00.000000
Depth: 992, time: 0:00:00.000000
Depth: 993, time: 0:00:00.000000
Depth: 994, time: 0:00:00.000000
Depth: 995, time: 0:00:00.000000
Depth: 996, time: 0:00:00.000000
Depth: 997, time: 0:00:00.000000
Depth: 998, time: 0:00:00.000000
Depth: 999, time: 0:00:00.000000
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I'm surprised. I think yield* TinyJson(element).leaves;
is equivalent to for(var e in TinyJson(element).leaves) yield e
, and with the right input, I think TinyJson(element).leaves
should be a stack of iterables of depth n
, and so the ith
.next
call should have to traverse i
stacked iterables to find the next element, hence quadratic.
@rakudrama am I getting myself confused here (I admit to finding sync* confusing) or is this one of the patterns that dart2js and the VM are now able to recognize and optimize?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I didn't see this comment before I landed, I'll make changes as needed in a new PR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, if I build lists of increasing length using:
dynamic mk(int i) {
if (i == 0) {
return [];
}
return [i, mk(i-1)];
}
then the VM seems to exhibit linear time growth, but DDC exhibits quadratic growth:
128 elements takes 3000 ms (23.4375 ms/elements)
256 elements takes 11000 ms (42.96875 ms/elements)
512 elements takes 51000 ms (99.609375 ms/elements)
1024 elements takes 212000 ms (207.03125 ms/elements)
2048 elements takes 889000 ms (434.08203125 ms/elements)
4096 elements takes 4496000 ms (1097.65625 ms/elements)
So looks like maybe this is quadratic, but the VM (and maybe dart2js?) are able to optimize it away?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sync*
is so much easier to write than your own Iterator/Iterable, especially for interesting structures like trees.
So I think it is important that it is not quadratic for trees. Otherwise sync*
is more foot-gun than feature.
We have a benchmark for this: Iteration.deeptree.syncstar.
If you scroll back to change 85484 on the Golem chart you can see where the VM got a 30-40x improvement by adopting the fringe-hugging scheme.
(@eernstg your benchmark creates an Iterable but does not iterate it, hence the minuscule constant time independent of size.)
The dart2js and VM implementation of the Iterator for sync*
hug the fringe by keeping the leaf iterator 'at hand' when doing yield*
of another sync*
Iterable. The Iterator keeps a stack internally, making tree traversals linear. The scheme falls apart when sync*
and other Iterables are interleaved by level, since we have no idea what moveNext
does on an abstract Iterator.
One way to 'fix' that would be to always use sync*
, but sync*
Iterators, being general and calling into a coroutine to advance, have a higher constant overhead than tailored Iterators. This overhead is often ~10x, so sync*
is still slow, but 10x slower than a hand-crafted Iterator on deep trees is better than 300x slower.
The key language aspect of this is that the types must match up so that no element-wise type check is ever required by yield*
.
What I don't know is whether other languages, especially JavaScript, have, or allow, the same fringe-hugging implementation.
… `E` is an extension. Add `box as` clause.
This PR adds a proposal for a
view
language construct which uses the concept of a 'view' to provide support for zero-cost abstraction.The basic idea is the same as that of an extension type, but the word
view
matches the actual intention behind this language construct much better thanextension type
. So it's basically a re-wording of the existing proposal.Apart from the terminology there is one substantial difference between the extension types proposal and this proposal: With the extension types proposal, a plain
extension
declaration (available since Dart 2.6) will declare a type as well as a set of extension methods. This means that an existing extension declarationE
can be re-interpreted as an extension typeE
, and the members ofE
can be invoked on a receiver of typeE
as well as via the existing extension method invocation mechanism (where the type of the receiver is some type that matches the on-type ofE
). With this proposal, no new affordances are provided for existing extension declarations. In short, "an extension is not a view".This means that the extension types proposal offers a slightly greater amount of expressive power and flexibility. However, that particular affordance has been eliminated in this proposal because it seems unlikely that a declaration
E
which works well as a set of extension methods would also work well as the type of a receiver. In other words, that particular affordance will probably just be a constant source of confusion, and rarely if at all a useful feature.