Stream: ideas

Topic: Displaying loctations of memory clones in the editor


view this post on Zulip Eli Dowling (Feb 29 2024 at 03:45):

So I was thinking, the most confusing part of writing roc is knowing when some data is going do get very speedily in-place mutated and when it's going to have to be totally cloned. I'm sure with enough time you would mostly be able to figure that out, but I think it's always going to have occasion cases where people would be caught of guard and have worse performance because of it.

I haven't looked into this, but how feasible would it be to extract, from the compiler, the exact location that memory gets copied rather than in place mutated?

I think it would be great if you could press a button and highlight any locations where you do a memory copy.
Or even just have a tiny little gutter icon to denote it like this in goland:
image.png

Just looking for a vague feasibility estimation from someone who knows more about the inner workings, on whether this is even possible

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 06:36):

I think it would mostly likely be a runtime feature. So a perf tool that runs an annotates time spent copying by line.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 06:36):

Cause most of these copies are dynamic at runtime. Though I guess it would also be good to statically label guaranteed copies.

view this post on Zulip Eli Dowling (Feb 29 2024 at 08:10):

The compiler doesn't usually know when a reference count will be 1? I thought that would usually be obvious for variables within the local scope, and a big performance boost to just not have to reference count most stuff. I actually thought you guys were doing that already.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 15:45):

Morphic definitely removesa number of the refcount checks (not sure the percentage in practice), but refcount checks with potential copies that require runtime knowledge are still exceptionally common.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 15:47):

So definitely significant room for improvement at least in the code/ir I have analyzed.

view this post on Zulip timotree (Feb 29 2024 at 16:07):

I think I would want an editor feature like this to highlight function calls like List.append that return a "heavy" type but sometimes reuse an allocation when it can be statically guaranteed that their input is shared so can't be in-place mutated

view this post on Zulip timotree (Feb 29 2024 at 16:12):

I think it should be quite feasible to statically detect sharedness. Some complications I can imagine are that it's not obvious what constitutes a "heavy" type in general. I want it to mean types that have deep copy operations that take linear instead of constant time. If you just looked for transitive containment of a List *, that might be an overapproximation because you could have a tag union that doesn't always contain a list

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 19:26):

when it can be statically guaranteed that their input is shared so can't be in-place mutated

I think only doing static guarantee will miss many locations with high cost. That is why I expect profiling to be quite important here.

view this post on Zulip Eli Dowling (Feb 29 2024 at 21:12):

Okay, I see. So I believe nim does this very aggressively. I imagine you may have read this, but it's documented here https://nim-lang.org/docs/destructors.html#move-semantics.
As far as I know nim basically hits C/rust level performance in most cases where something like rust's borrow checker would be happy with the code, and uses reference counting for long lived shared objects. Basically it means you almost never actually do any recounting. Particularly not in hot code paths.

view this post on Zulip Richard Feldman (Feb 29 2024 at 21:27):

we want to be more aggressive about borrow inference, but haven't implemented it yet

view this post on Zulip Richard Feldman (Feb 29 2024 at 21:28):

I don't think there are any blockers to starting on that project if anyone's interested in getting into it (or at least there weren't any blockers the last time I talked to @Folkert de Vries about it!)

view this post on Zulip Eli Dowling (Feb 29 2024 at 21:36):

Okay, cool, I think I somehow thought Perseus would automatically involve that, but it does make sense that the simplest implementation is to do it at runtime.

Well it's exciting to think there are a lot of big low hanging fruits as far as perf. I personally can't think of any reason you guys shouldn't be able to compete on a similar level to nim,

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:41):

The sink and lent annotations allow us to remove most (if not all) superfluous copies and destructions.

This is the really important line

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:41):

We have no such annotations. So we have a lot more cases that will have runtime reference checks.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:42):

That said, we want to automatically derive there lent annotation in function that can never mutate or return an input value. That is the borrowing inference Richard mentioned above.

view this post on Zulip Eli Dowling (Feb 29 2024 at 21:48):

Yeah, fair, though I imagine in many cases move information could be inferred by knowing that a particular variable was created in the local scope and isn't returned from it. Or that it was passed into the current function using an inferred move.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:49):

Sure, that happens in Roc today. If you pass a value into an owning function and it isn't used again, we won't touch the refcount.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:51):

The big issue we have is that you really want to distinguish unique and shared. We have to check refcounts regularly to distinguish this case.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:52):

In a language like nim, they don't have this problem. They will mutate a value even if it technically is referencing shared memory.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:52):

In roc, we lazily copy or lose inplace mutation optimization when a value is shared.

view this post on Zulip Eli Dowling (Feb 29 2024 at 21:54):

Oh, okay, would you mind linking to, or showing some common example that would prevent us knowing the recount or whether it's shared? Thinking about most code I write, it generally doesn't mutate or perform mutations on any shared values. Mostly things are a little more "data pipeliney"

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:56):

A really simple example, update an element in a list. We have no way to know if the list has unique ownership of the element or if the element is shared. As such, we check refcounts before attempting the mutation.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 21:57):

There are many ways to hit this and many that we hope to optimize away eventually. I would guess that most of them are accidental.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 22:00):

This is a tradeoff between correctness and performance due to roc being immutable. That said, a hugely important assumption of roc is that most code most of the time is written in a way that everything is unique (when trying to mutate). As such, copies are super rare and the code runs fast (which so far seems to be the case). That said, we still will need extra refcount checks compared to an imperative refcounted language like nim cause they assume mutation is always fine.

view this post on Zulip Brendan Hansknecht (Feb 29 2024 at 22:03):

All that said, I would bet there are a number of low to medium hanging fruit related to the design that could do an even better job at avoid refcount checks. Especially true when thinking about what information we could pass between function calls. Like maybe between every function call we should be passing a shared/borrowed/unique flag per param. Not sure what all morphic does today around this.

view this post on Zulip Eli Dowling (Feb 29 2024 at 22:15):

Cool, thanks for the explanation, I appreciate it :).

Well it sounds like this feature is something that would be good at some point later, but not quite yet. Has there been any consideration into allowing those ownership annotations? It seems like purely a win. Like 90% of people never need to use them and the 10% of people riding very high performance code would probably really appreciate it. I know ocaml and swift are both working on integrating that feature for example.

Obviously it does add more complexity but if you are already trying to infer all that and passing that info around it's probably less of a large addition.

view this post on Zulip Kiryl Dziamura (Mar 04 2024 at 16:57):

Speaking of in-place mutation - does roc reorder calls for a possible optimization during comptime? I assume it's possible to propagate OpportunisticMutation type which comes from std and isn't accessible to user tho it might be a nice helper in LSP.

An example of possible reordering optimization:

mutateList = \list -> List.set list 1 42

f = \_ ->
  listA = [1, 2, 3]
  listB = mutateList listA # cannot mutate listA because it's still in use so we get clone here
  itemA = List.get listA 1 # after this line, listA is no longer needed
  (listB, itemA)

The calls can be rearranged to

mutateList = \list -> List.set list 2 42 # from `List.set`, `list` infers the `OpportunisticMutation` type

f = \_ ->
  listA = [1, 2, 3] # `listA` inferred `OpportunisticMutation` from `mutateList`, move all reads from it as high as possible
  itemA = List.get listA 1 # `listA` has `OpportunisticMutation` so either copy `itemA` or increase its RC
  listB = mutateList listA # `listA` will not be used anymore, it's safe to mutate it
  (listB, itemA)

My understanding may be very naive, most likely the compiler is much more nuanced. The assumption is that the runtime RC might be optimized by reordering of some calls in comptime (my intuition is that having read-only calls asap might be beneficial). Yeah, there are also conditionals, references permutations, and in general, my example is probably too synthetic

view this post on Zulip Brendan Hansknecht (Mar 04 2024 at 22:28):

Roc will not do that today. It is probably an optimization we should do. That said, it has one specific complication. What if mutateList has a dbg statement? We don't really want to accidentally re-order dbgs cause that can lead to confusing users.

view this post on Zulip Brendan Hansknecht (Mar 04 2024 at 22:30):

I think this is a case were generally speaking we want to be deterministic and empower users. As in, we probably should teach users not to write that code. Cause at some point, they are likely to write something that doesn't get optimized and greatly hurts perf. So it is better to make users aware of the general class of issue and alert them to fix it.

view this post on Zulip Brendan Hansknecht (Mar 04 2024 at 22:30):

That is at least my opinion.

view this post on Zulip Brendan Hansknecht (Mar 04 2024 at 22:32):

Often times there is a long tail of similar cases like this that lead to sharp edges in the langauge. It would be better to build systems to teach users about this and point out the specific issue (unless we truly can catch enough of the cases that users essentially never hit the sharp edges).

view this post on Zulip Brendan Hansknecht (Mar 04 2024 at 22:34):

I wonder if we could have a cargo clippy like check for this. Even with an auto fix for the simple cases.


Last updated: Jun 16 2026 at 16:19 UTC