-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
making .=
work for immutable types?
#19992
Comments
|
Although this seems like it could work, if |
The semantics would be redefined to "mutate in-place if contents are mutable". I agree that this is a downside, but it's unfortunate not to be able to write generic code that is efficient for both |
The semantics of It gets complicated with immutables with multiple bindings to the same value, but assuming that there is only one reference to For mutables (heap-allocated), the memory is mutated, not reallocated. The type of For non-isbits immutables, we have to re-allocate because of the limitations of current compiler technology, but this would go away automatically once they are inlined. But the type can stay constant. Fusing
|
I worry that people would write correct code for mutables this way and then apply them to immutables and get subtly different semantics, giving incorrect behavior. I still believe that a large part of the reason that generic code in Julia "just works" is that we've been really strict about semantics being completely uniform in the language (e.g. the classic "why can't |
I'm ok with that, in fact that could be the most beautiful way to do it, but I'd need escape analysis or a "linear type" ref in order to have an immutable array remain stack allocated. (I'm not sure if linear/unique types could be a third option besides OTOH, the OP seemed like something that works now. |
I have looked into this solution a lot in JuliaDiffEq. The reason is that we also have to handle user-defined function, which is either |
It's important to note an asymmetry here: immutable objects cannot provide mutating semantics, but mutable objects can be used in an immutable way, and reusing their space efficiently is "just" an optimization. It's clear that the long term goal should be to make it less necessary to use mutation just for performance. The main way I can see to do this is to automate tracking of whether you have the last reference to a value. For example, given
where this is the only reference to
and |
Right, I've been thinking about this a lot lately. We currently have type inference, and better type inference means less boxes (and better performance) We could follow that by "reference inference" and if it can prove certain things about object lifetimes, this step would make certain transformations (perhaps like above) and could annotate to use unique references, non-mutating references, weak references, reference-counting, etc, for itself and for during the codegen phase (which would stack allocate mutables if the annotations indicate that is safe/performant, etc). When its not sure, the standard GC could be invoked like now. Implementing better "reference inference" would mean less GC usage and less (but not zero) heap allocation. If you wrote your code in a way such that every object lifetime could be inferred, object deletion could become fully deterministic for that code. (I believe you guys have thought of this under the name of "escape analysis", but I had only seen this discussed for the purpose of stack allocation of mutables). Having extra bits in the header is a bit like reference-counting, which could be invoked when the "reference inference" step feels this would be more performant than tracing GC, but I think just inferring unique references at compile-time and making it deterministic at run-time would be a nicer first step (and more generalizable to a fully-fledged reference inference engine). The cool thing is that we would more-or-less inherit some of the nice properties of Rust on an opt-in basis (similar to static typing is opt-in in Julia but dynamic by default/fallback; in this context tracing GC is the default/fallback). |
So I think of this as a bit like the proposed improvement to But there are big wins to be made in the fully-inferable cases (where you can prove there is exactly one reference/binding, for example), since you can skip tracing GC for these cases entirely, and I would argue to deal with them first. But maybe that is a lot of work compared to your suggestion... |
This is sometimes known as "approximate reference counting" where you distinguish only between "there are additional references" and "there are not additional references". You can also do things like support values 0, 1, 2, 3+ using two bits and the semantics are that if the refcount ever goes to 3 it doesn't come back – in which case the object gets GC'ed as normal. |
One question that arose in our class yesterday was whether we can make
.=
usable in generic code that is supposed to work for both arrays and numbers, or arrays and immutable arrays like @andyferris's StaticArrays.jl.Right now,
x .= f.(y)
turns intobroadcast!(f, x, y)
, so there is no way the broadcast call can be made to work for immutables.One option would be to turn
x .= f.(y)
intox = broadcast!(f, x, y)
. Then, if you have an immutable type, you could define abroadcast!
method that just ignores the second argument and callsbroadcast(f, y)
and it would work. This is kind of an abuse ofbroadcast!
, though, and requires that library authors implement methods for all immutable types that they want to use with.=
. Also, I'm not sure if there are any problematic performance implications to the extra assignment in the mutable case?Probably a better option would be to turn
x .= f.(y)
intoand assume that the compiler can optimize out the
isimmutable
check iftypeof(x)
is known. This has the advantage that it will automatically work with every immutable type without requiring explicit definition ofbroadcast!
methods. Is there any downside?The text was updated successfully, but these errors were encountered: