-
Notifications
You must be signed in to change notification settings - Fork 209
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
A fast and safe List.cast #1189
Comments
cc @alexmarkov @mraleph @a-siva for thoughts on possible approaches using targeted optimizations to make an existing pattern fast. cc @lrhn @natebosch @jakemac53 @zichangg for thoughts on the library API |
I can't think of a reasonable way to support this on user defined |
user-defined list implementations aren't going to have good performance anyway, so their implementation can be: List<R> castAndClear<R>() {
List<R> result = cast<R>().toList();
clear();
return result;
} |
From a library API perspective, it looks like you want a "ListBuilder" for a We can probably create such a builder class. It would basically be a I don't think we can do something directly on a fixed-length list. The VM implementation implement those as a single object, and we will not change the type parameters of an object that others might have a reference to. Especially if we get any kind of variance control, that'll be unsound. With unsound covariant generics, it's more of an annoyance issue if someone else can change the type of a list that you created. Growable lists are easy on the VM. There you can create a new growable-list wrapper for the internal fixed-length list backing store, and change the type parameter of the internal backing store if necessary, because nobody else has access to it. |
In the updateChildren and mount cases, we only ever write to the list, which would be consistent with that design. I'm not sure whether that's always the case though. In general I think it's simpler if we have fewer APIs; adding a list builder API when the list API works fine would be unfortunate IMHO. |
(i should say... the benefits here pale in comparison to what we could get with the ability to seal a list, dart-lang/sdk#27755) |
Do you fill in the list in sequence and just want to allocate it at a predefined size, or do you arbitrarily assign entries at any index (while earlier indexes might still be null)? If the former, I could imagine an api that just allows you to pre-allocate the list at a certain size, but doesn't set its |
Lists with non-null elements are definitely more painful to use. I'd like to see this problem improved. I think in most cases a It looks like mount could be written with Inlining of One of the problems of introducing a new kind of List is that it tends to degrade the performance of all code handling Lists through the disease of polymorphism. In addition to being horribly slow due to the extra checks, final List<Element> _children;
...
SomeConstructor(List<Element> children)
: _children = List.of(childen, growable: false) There is a chance here with the direct initialization of a final field with a value directly coming from a constructor that the compiler should know (or could be taught to know) the exact kind of list stored in I wish there were more type-safe ways to generate unmodifiable lists. Unmodifiable lists are great for protecting algorithms from modifications and are as compact as fixed length lists. They share a lot of implementation details with Now that we have more pain points in creating fixed length lists, please let us solve them in a way that also works for unmodifiable lists. Imagine, if you will, that something like |
The latter, at least for the updateChildren case. For the former there's a constructor which makes this trivial. |
What we could do on the VM side (and I think on dart2js side) is to add a recognition of the following pattern: given This optimisation is rather trivial to implement but it has some degree of fragility - e.g. it depends on escape analysis which might fail for various reasons silently. So the pattern can go from 0 allocation to list allocation rather easily. One way to address this is to have a special method for this sort of "transmutation" and mandate that compilers issue warnings/errors when they can't perform a 0 cost transmutation. Notice that the only way to satisfy type system here is to perform a scan over the whole list to verify that the list does not contain null values. I am looking at this code which builds list from both ends - meaning that traditional approach with a unidirectional builder pattern (building list from A more provocative question here would be: is it really important that Finally, I would like to highlight that workaround |
Banning nulls from these codepaths has led to some interesting features like we can finally make some of these classes have const constructors since we can remove the null-checking asserts. This will have huge performance benefits for static UIs. If we had the ability to run non-trivial code in const expressions then this wouldn't be an issue and we could use Obviously any additional scan is O(N) work. This is an O(N) algorithm so we're just increasing the constant factor. It's pretty performance-critical though. If this was Rust I'd mark this code as "unsafe" and go crazy. |
+1 for the
This should be feasible to implement on all major platforms efficiently. |
If there are any benchmarks that demonstrate this it would be worth mentioning them here for posterity - so that we for example can refer to them if we were to prototype some solutions. |
Most of the benchmarks that involve layout (so e.g. those that scroll or do transitions) should be exercising this code. |
There is also the option, a little far out perhaps, to make all
That makes every list a list builder. You create it in uninitialized state, write to every slot, then you can pass it on without worrying about anyone getting errors. We could potentially detect that every slots has been filled, and then transition the object to an implementation which doesn't check. It would mean a non-null check on every read, or (more) polymorphism of lists. It would avoid having a |
I think destructive cast / move constructor is by far the simplest solution for this problem, still having only 1 object allocation + O(n) pass extra overhead which is hard to beat. Ordinary growable Of course, we can also introduce specialized Re: making lists |
I don't know how we can accomplish this ergonomically. This is a contract that can't be expressed in the signature, and may be something that leaks into other API boundaries in ways that also can't be expressed in the signature. |
@natebosch Yeah, the fact that growable vs. fixed-size list distinction is not exposed explicitly in public types is inconvenient (but also has its advantages of List APIs being simpler). This shouldn't be a big problem when building lists, as source list is likely to be allocated in the same scope. We can describe that limitation in the constructor's documentation and enforce by throwing |
I don't think it should implement It is a pretty fundamentally different thing, imo.
How is this meaningfully different from |
As far as the The constructor should still work I think regardless of the original list type and just fall back on slower behavior if it can't be optimized based on that. Note that even a |
It is not possible to implement a It seems hard to write a builder that would be better than a List. As @lrhn says, the A small library change we could make for using final List<Element> children = List.empty(capacity: widget.children.length);
...
for ... {
...
children.add(newChild);
...
}
_children = List.of(children, growable: false); // or List.unmodifiable This would not help dart2js but might help the VM avoid intermediate allocations, and leave an internal |
Wouldn't the |
The title of this issue is 'A fast and safe Seriously, use copying where necessary to avoid If list copying is a bottleneck and we decide some clever optimizations are the solution, I want to design something that works equally reliably for dart2js and AOT. mount can be written using a growable list, updateChildren It is probably easiest to use a temporary If there are no oldChildren, run a simplified algorithm that creates the first child before allocating the If there are some elements in oldChildren, use Looking at the code, I believe that the list copying cost will be swamped by the other costs, and the code could be sped up in other places to more than compensate. Consider mount void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_children = List<Element>(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element>(i, previousChild));
_children[i] = newChild;
previousChild = newChild;
}
} The innocent-looking
There is a chain of super calls
and finally
This cost can be reduced from N+1 calls to one call by using a local variable:
|
Why isn't the compiler just doing that for us already? Shouldn't it see repeated access in a loop that doesn't modify the accessor and just throw that into a single accessor that's cached for the loop? |
Similar question if the compiler sees that I've created a nullable list that ends up filled completely with non-nulls, and then I cast it to non-null. Why can't the compiler just optimize my code so that it works out fine? Why do I need to coax it by filling a list with a dummy non-null sentinel (like the first item in the list in @rakudrama's example, or the private sentinal in @Hixie's)? |
@dnfield Similarly, a compiler really has no chance figuring out that a nullable list is going to be completely filled by non-nulls, not unless it's being filled using a particularly trivial scheme (like |
FWIW at least on |
@lrhn - within the lexical scope, how could the IOW, why can't the virtual call just be made once and then cached for that lexical scope? This should work regardless of whether there are any subclasses or not, right? For the list case, the more I think about it, the more I suspect what we want is an |
See this gist as an example, |
Ohhh. I forget that I think of |
A pattern we see in some of Flutter's hottest code paths is:
The cast introduces a hidden expense: every access to the new list in the future checks for nulls.
Right now we're planning on working around it like this:
...but it's awkward to do this and doesn't really reflect the desire, which is for an equivalent of "late".
Proposal: List could have a new method, "castAndClear" or "destructiveCast" or some such, which returns a new List cast as specified, but using the backing store of the old list, and neutering the old list (so it doesn't provide a back door to breaking the type system).
The text was updated successfully, but these errors were encountered: