-
Notifications
You must be signed in to change notification settings - Fork 0
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
Ref uniform edge case #16
Conversation
I think it's fine, for large value types it's actually an optimization to pass them by (immutable) ref. |
That is indeed interesting. I feel a bit out of my depth with this one, going to read up a bit. A prelim scan though pointed me to the fact that Rc has an as_mut, which basically does the same thing. Since we are almost definitely going to need interior mutability to move mutable refs around, I am curious if this is a workable substitute now to a Saying that though, aren't you just going to end up with lots of bugs? The expected behaviour is that there is 1 mutable shared reference and all access to it will give the same thing. It sounds to me like with a COW you could end up with separate desynchronised mutable copies of the same var, which will no doubt diverge from dotnet behaviour, no? |
@alexswan10k Perhaps, I was thinking about it in the context where we pass everything by value (includinc Rc). I didn't go as far as actually trying it. |
Heh fair play. One to keep in mind anyway. By the way I was thinking more about async functions and I am reasonably confident you could carry on using refs in. The thing is the function could internally just clone everything it needed to keep in scope just before the first continuation, basically the same as what we do for closures. That way we maintain consistency of refs in, value/owned out. |
@alexswan10k Ok, I think I fixed my "closure" issue with generics. |
I wanted to just throw down some thoughts while they are fresh in my head, no action required. Firstly, pretty confident we can get pass by both ref/value based on context working again once function registrations with reftracking is complete, so it is definitely worth keeping that logic around. The way we can get around the uniform generics is by treating all generics as by ref, and aligning mismatched function parameters explicitly where a non-generic function feeds a generic one. We have to pull them apart anyway with the closure processing, so it makes sense to check against the definitions then, and unwrap/wrap, deref/borrow accordingly. That being said, function registrations is one we agreed to delay as it is a complex optimization that can wait. Just one to keep in mind though. Here is an example:
Notice how x_1 is an int (value) but the underlying map abstracts it to a &T, so this now breaks. Since we know this is a ref from the function registration, and the target closure is mismatched (it is declaring x as an int and not an &int) we could (at the boundary) plug the gap by adding in any referencs/dereferences etc. With a raw closure you could just adjust the closure parameters, but with a declared function you obviously can't do this as it is fixed. You would then have to create a bridging closure to convert a int to an &int. On a side note I just read this https://doc.rust-lang.org/nomicon/aliasing.html and perhaps this whole optimisation is not even required as the rust compiler already does something equivalent. Secondly, been thinking a lot about how the hell we do parallelized code. It really is quite clear that there is no way to elegantly abstract over both Rc and Arc, but it does look like the community has some examples to draw from such as this. Interestingly they just drop two libraries for immutable, one for the single threaded context, one for multi threaded. That got me thinking. Since we are generating code as it is, i wonder if we could simply duplicate the output of every type and every function for both the Rc case and the Arc case, and even add conversions in between. Obviously this is something you would want to be able to turn off at the compiler level if you are only working in a single threaded context, but it might give a way to be able to easily transition to a multi-threaded environment without too many compromises. I imagine this would be quite useful for a Rust user too, who can build out their whole domain model in F#, knowing full well that they do not have to commit to either scenario. If they then find they have a Again parallelization is not something that has to be tackled any time soon for this to be useful, but in order to plug into web frameworks and other more high level abstractions, it is probably something that needs a solution down the road. |
@alexswan10k I agree we should delay this optimization, as it might not be needed. I also like the easy mental model of passing everything by reference, it's just easy to remember and reason about when looking at the generated code. I added some basic list support, should be able to extend it further. Also tried to rework the imports, wrangling the Rust module system is not for the faint of heart and is not very fun. Documentation doesn't really help, you just have to try things. |
@alexswan10k We should be able to generate either single-threaded |
I do agree that the simpler models are probably the best fit for now, Let's see where we end up i guess (I am just thinking way ahead). Nice work on list. I might have another look at classes the next time I get some time, although I was really struggling with these generated constructors. Perhaps one of the other language targets has done some legwork we can copy. |
@alexswan10k On the other hand, passing everything by value (including Rc) was a simple model too. And I'm not entirely sure (without doing any benchmarking) that the cost of Rc cloning justifies the additional complexity and edge cases we introduced with the reference tracking. What would be a good benchmark in your opinion to test the performance difference of the generated code (by val vs by ref)? |
I imagine something like this would do it:
An extreme example I know, but you can see how this sort of thing might be problematic in tight loops. Also just bear in mind that Arc has a higher penalty as clones need to be atomically counted. I appreciate your desire to try to simplify the model as much as possible, but it is probably also worth considering what a Rust user would expect from any generated API surface. I think it is generally expected that you borrow if you can. Are references giving you trouble? |
No, not really, I was thinking more about potential future edge cases, and the fact that any new feature we add needs to be included in the ref counting. But that's ok. I was just saying we need empirical evidence to see which one performs better. I've run your example with two magnitudes higher and yes, it looks like passing by value is 5x slower than passing by reference in release mode (7x slower in debug mode), most probably because of the extra cloning in tight loops, not the actual passing by value. So I guess references are here to stay.
|
@ncave it is good to have some quantifiable evidence around this though. I would have been happy to concede if there really wasn't any difference! In some ways passing a Rc by value is sort of like passing by reference anyway (pointer to a value), but details. Yes it was always the cloning that was my worry, pretty sure by-value will actually be faster because you do not have to dereference the pointer to access it, which is why I am still a bit frustrated that primitives have to also be passed by reference now. My initial thought process was really "how do we reduce cloning", and borrowing by reference is a good way to only clone something when you actually plan to hand it over to something else, not when you just read it. Although cloning is very cheap for Rc's, i think the problem is that it still has to hit the allocator as Rc is itself a small struct. |
@alexswan10k Right, I wouldn't worry about passing primitives by ref for now, it will probably be hard to show that it's slower than passing by value, as the cost will be dwarfed by the rest of the code in the callee. In any way it's going to be a very very small cost, if any. |
Agree. Also the aliasing thing implies that (an optimization rust can make is that) values are dereferenced and bound locally on the stack frame anyway, so it might literally be a single dereference for that parameter, even if there are many useages of it in that function. Bigger fish to fry :p |
Yeah are you happy if I try and get generated ctor classes working next? I am making some progress but a bit fiddly. Seems like a nice self contained thing. No worries if you are already in that area though. Just some simple cases like this:
I am just thinking, if we can make some progress with classes then things like Enumerator can potentially be tackled. |
@alexswan10k |
@alexswan10k Pro tip ;) |
I am completely stuck. Any idea how transformClassWithCompilerGeneratedConstructor works? If I make a trivial class, I cannot work out how to get to the inputs
If I follow Fable2Babel transformClassWithCompilerGeneratedConstructor and the python/php equivalents, they all seem to read out FSharpFields and assume that each generated constructor parameter is a field and map 1-1
What makes even less sense is I have just tried this exact code in the online Fable REPL and the js coming out correctly. Somewhere there is an expression that defines what I have tried digging through the ctor also, but no avail. I can find the parameters by name, but not the expressions that set the fields. No other example does this either so I am probably barking up the wrong tree here anyway.
Any ideas? |
@alexswan10k |
There is no constructor body. If I run the sample, there is no output for a constructor at all ModuleMember, although there is an output for the add method interestingly. I am wondering if it could be something upstream? |
@alexswan10k I think |
@alexswan10k I made some changes in option matching, please merge with your work to avoid issues. |
This is great, i will update. I agree, that was my understanding of transformClassWithCompilerGeneratedConstructor too. The problem here is that - from my example I am not sure that this should even be a generated constructor, but the AST does not contain a Constructor, so I have nothing else to go on. I wonder if in the Fable2Babel example there is something happening upstream to put this in, but I could not find anything obviously different in the pipeline of this or any other language output. |
@alexswan10k I'm not sure I follow what the issue is, here is the logic that determines whether to use it or not: match decl.Constructor with
| Some cons ->
withCurrentScope ctx cons.UsedNames <| fun ctx ->
transformClassWithImplicitConstructor com ctx decl classMembers cons
| None ->
let ent = com.GetEntity(ent)
if ent.IsFSharpUnion then transformUnion com ctx ent decl.Name classMembers
else transformClassWithCompilerGeneratedConstructor com ctx ent decl.Name classMembers we shoud be able to use that as is. |
Decl.constructor is none for the case I described |
Yes, so it will call Perhaps I'm missing entirely your issue, please add a small example. |
Compiler generated assumes symmetry between params and fields. My example has different params.
|
@alexswan10k This looks like a normal constructor, not an implicitly generated one. The implicitly generated one will always have the same number of arguments as the fields, cause that's how it is generated (from fields). In other words, the class's |
I agree. So would you expect decl.constructor to have a definition in it? |
@alexswan10k In this particular case, yes. |
Yeah, I figured we might be missing something upstream but cannot see anything different between rust pipeline and fable/Python/php. |
@alexswan10k Perhaps |
@alexswan10k Perhaps things changed over time. |
Oh ok, good to know. The problem is when I do that, the parameters don't line up and the code won't work. I end up with a Constructor with 1 param as I expect it to have 2. Something is missing. Don't worry, I will have another look tomorrow, must be missing something. |
@alexswan10k On a side note (in case I forgot to mention it): |
Most welcome :) hopefully it hasn't complicated stuff too much haha |
@alexswan10k Classic off-by-one error! Just don't do that. (that was a reference to a joke: "A lot of beginner C programmers, what they do is, they write bugs in their code. Classic mistake. Don't do that.") More seriously, I can only guess, without stepping on your toes and trying to implement it myself. |
Yeah funny you say that, when I run the fable repl there is what looks like 2 ctors but neither is consistent with generated. One is a static wrapper fn, and the es6 ctor has the logic. I can't find any reference to the body logic in my fable ast or the static wrapper. I only get instance methods. Let me give it 1 more pass 2mo in case I missed something, just annoying me cos this needs to work heh. We will get there. |
@alexswan10k |
So this is probably the other half of the problem. It is not in the members list. Well, it is on the entity but those are fsharp compiler pass throughs, they are not fable constructs, and there is no expression that can be used. |
@alexswan10k I figured it out after a bit of debugging. It was my fault after all, I'm afraid. I broke it when I enabled the nested modules in Fable for Rust (normally module decls are turned off for JavaScript), so it wasn't attaching the members to the class declarations. Sorry about that, it's fixed now, there should be |
Awesome work. Don't worry, stuff sometimes breaks when you move fast, just super happy we (you) got to the bottom of it! Should hopefully be quite straightforward now to get the example up and running. Thanks! |
Just a small one, couldn't help myself. The uniform parameters thing, I didn't believe you, so suffer and learn the hard way what you were talking about haha. As for the problem, it is quite clear to me now - when you share generic parameters for the params AND the return type, it gets super confused because it infers if it is a ref or not from the generic.
I have added a trivial example of a ( T -> T) passthrough function, which broke everything before. I have tweaked the shouldBePassByRefForParam so this now correctly dereferences etc. Also to clarify, we also do not want to be cloning when borrowing because it returns a value type, so I have tweaked this also.
It sucks that everything has to be uniform, but I cannot see any way around it either. Will have a good think.