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
AOT illegally removes write-only fields #50571
Comments
@rmacnak-google Could you clarify what part of the spec is violated? My understanding is that when WeakReference was introduced, we were careful enough to not prevent optimizations like tree-shaking of write-only fields. So API specification doesn't actually tell anything about reachability, roots etc. From the API specification: Lines 68 to 70 in af1536d
In the program above, At the same time, there's no guarantee that they would be ever cleared, so WeakReferences can always return their targets in JIT mode; or sometimes, depending on the GC timing in AOT mode: Lines 75 to 76 in af1536d
It looks like it is working as intended. |
Yes, the intent was to phrase We could clarify the wording if it is not clear. ECMA-262 makes it more explicit and says that the value is considered live only there is a possible execution of the program, which does not dereference weak references and observes this value. |
@lrhn Any opinion on making the contract more explicit in the doc-comment? Otherwise I am inclined to close as WAI. |
Plenty of other ways: a different weak reference, an ephemeron, Finalizer, NativeFinalizer, OutOfMemoryError, Dart_NewWeakPersistentHandle, Dart_Invoke, getObject, etc. What WeakReference's doc claims is not relevant to most of these. I anticipate you will claim these are unimportant and that allowing this class of optimizations is more important. I claim the straightforward semantics are more important. Global fields are reachable. Local fields that are in scope are reachable. The fields of a reachable object are reachable. Activation pruning is an illegal optimization. Closure environment pruning is an illegal optimization. Field pruning is an illegal optimization. |
What we found important in practise is not having premature finalization (which leads to use after free bugs). We introduced If you want to keep things alive, use class Node implements Finalizable {
...
var leftWeak;
var leftStrong;
var rightWeak;
var rightStrong;
Node get left => leftWeak?.target;
set left(Node value) {
leftWeak = value == null ? null : new WeakReference(value);
leftStrong = value;
}
Node get right => rightWeak?.target;
set right(Node value) {
rightWeak = value == null ? null : new WeakReference(value);
rightStrong = value;
}
} Specifically for fields, see: We do require using With Of course, that doesn't prevent people from using @rmacnak-google Did you run into a specific use case where this is an issue? |
Yes, we should probably clarify docs to say that WeakReference can be cleared when "there is no other way for the program to access the target object except through a target of a WeakReference, Expando or a handle created with Dart_NewWeakPersistentHandle". Otherwise, according to the current wording of the spec it is not correct to clear a WeakReference if there is another WeakReference or Expando pointing to the same object. How can you reach to an object via an OutOfMemoryError? Also, Finalizer and NativeFinalizer do not provide an access to the original object being finalized. Dart_Invoke/Dart_GetField can access members only if they are annotated with |
I agree that it's working as intended. Mainly because the "intended" is very, very loose. It's very hard to describe the intended behavior. The shortest I can come up with is:
(That is, observing the weak reference to have been emptied, after previously having held an object Finalizers are even trickier, because they act when the object goes away, not just when you try to observe the weak reference. So, for a finalizer, it must be:
It's prescriptive, not descriptive, but it's hard to be anything but when abstracting over any possible garbage collection techniques. But even if we take that literally, it still kind-of assumes that we execute every expression in the source program. /*final*/ class Resource {
final ExternalResource _realResource;
final Something otherStuff;
Resource(this._realResource, this.otherStuff);
void use() {
staticHelper(_realResource);
}
}
class Actor {
final Resource _resource;
Actor(this._resource);
void act() {
_resource.use();
}
...
} The That optimization would wreck havoc if there is a finalizer on the The source has an expression which evaluates to the resource object, the So it's not easy to predict when a value is dead. That's usually not a problem, because if it is dead, you can't see that value anyway, and if you can, it's not dead. It doesn't matter whether it's dead when you're not looking. We can't just say that any optimization is fair game, and finalizers should be ready for their objects to die at any time. Object existence isn't that observable to begin with. If you don't call We may want to make some kind of promise about which optimizations are off the table. That is, I think we should say something about how much, or little, you can actually rely on. And again, it might just be "don't assume anything if the class isn't What is it that Finalizable does? I noticed that ECMAScript promises that any value you read out of a weak reference will stay alive for the duration of the current execution, which is probably until you get back to the event loop. |
The contract in
I do think that unused field removal is an important optimisation. It allows you to have debug only fields which are stripped in release mode (see e.g. flutter/flutter#22915). Dart does not really provide any language feature to achieve the same thing (i.e. a preprocessor).
I understand where you are coming from, but I think in reality we have to tread a fine line here. For example, currently we base our closure contexts on scopes. It's a very straightforward model. However we constantly see users being puzzled about this - because they expect closure to only close over referenced local variables. As I see it, there are two forces here pulling in opposite direction: for runtime observability and debuggability you would prefer a very simplistic execution model (similar to what you describe), but for production deployment you would actually want an optimised execution model which is less straightforward and gives compiler more leeway to optimise things - but within well defined bounds. e.g. for fields specifically this means that any field that is not read and does not have
No we did not make such promise. |
Consider
Whole-program analysis currently observes that
leftStrong
andrightStrong
are written to but never read, and concludes it can eliminate them. This is incorrect because these fields also have an effect on the reachablity of their value. With the fields removed,left
andright
sometimes return null depending on GC timing.The text was updated successfully, but these errors were encountered: