Yeah, thinks like morphic will remain slow for non allocation related reasons (though morphic also doesn't do arena allocation which maybe could help its perf a bit).
Is morphic just alias analysis?
I think morphic does analysis on uniqueness to enable inplace mutation without checking the refcount. It might also enable removing some refcount increments/decrements. That said, I don't think it currently works. I had to disable a chunk of its analysis due to causing bugs and it basically had no impact on perf for most benchmarks.
Cool, that would allow us to remove a dependency :smile:
Once Roc works consistently, we can re-enable it
But we are definitely not there right now
This is probably the biggest roc app I know of https://github.com/lukewilliamboswell/roc-htmx-tailwindcss-demo
I had one more advanced that this I was messing around with for a work project/prototype... I should probably back-port features from that into this demo.
I think it would be great to double check with someone who understands what it is supposed to do better before just removing it.
To be fair, it may be the case they we aren't leaning on it enough. Potentially more zig biultins could be using it's inplace vs not indicator at comptime. On top of that, it may not show huge gains until hit in a hot loop where removing the refcounting completely could be huge....but I don't know enough about it to be sure.
From what I know of it currently, it is bugged, it really slows down compile time (all the worst compile times I have ever seen have been due to morphic), and it doesn't generally affect perf much.
So that does push towards removing it assuming we can't easily fix it and make it algorithmically sound.
EDIT: all that said, not trying to be mean to the morphic folks. It might be a great piece of tech we are holding wrong or that has some minor bugs that happen to lead to scary roc issues.
If you did a wc -l **/*.roc
, what do you get?
$ wc -l **/*.roc
49 src/Controllers/Product.roc
50 src/Controllers/User.roc
130 src/Helpers.roc
14 src/Models/Product.roc
16 src/Models/Session.roc
12 src/Models/User.roc
51 src/Sql/Product.roc
67 src/Sql/Session.roc
53 src/Sql/User.roc
49 src/Views/Layout.roc
3343 src/Views/Pages.roc
234 src/main.roc
4068 total
Yeah, that's big-ish. But I have Go lang apps that are 10k+ (and still compile in the low 100s of ms)
My other one is 5908 total
I don't know who understands Morphic...
I'm assuming you haven't migrated this app
I'd love to see a time roc build
on it after you do
@William Brandon I think is one of the morphic authors
And I think @Folkert de Vries had solid understanding of morphic at a high level at least.
They also have a published paper and talk, but I think it is mostly on lambasets and not on the smarter inplace mutation without checking refcounts.
With the new pipeline design, lambda sets won't exist at that part of the compiler
Just concretely-typed first-order functions and values
Given checking a refcount is 1 is generally pretty cheap, the inplace smarts may not really ever give us gains (except in really hot mutate in place loops maybe). As opposed to lambasets which hugely help llvm understand and optimize our code.
Sam Mohr said:
Just concretely-typed first-order functions and values
So happy for this.
It's more important than any other work we're doing in the next 6 months IMO
24 messages were moved here from #off topic > Bootstrapping Roc? by Richard Feldman.
I like the idea of disabling (or even removing) Morphic before 0.1.0
I think it is already set to trivial analysis due to bugs. So it does the same amount of work for both debug and release builds (which is pretty minimal)
So it is kinda already disabled
right, but even in that mode it's been a source of bugs, right?
Not to my knowledge, but I'm not sure
hm, I could be misremembering then
This is all the context I have:
// TODO(#7367): Change this back to `morphic_lib::solve`.
// For now, using solve_trivial to avoid bug with loops.
// Note: when disabling this, there was not much of a change in performance.
// Notably, NQueens was about 5% slower. False interpreter was 0-5% faster (depending on input).
// cFold and derive saw minor gains ~1.5%. rBTreeCk saw a big gain of ~4%.
// This feels wrong, morphic should not really be able to slow down code.
// Likely, noise or the bug and wrong inplace mutation lead to these perf changes.
// When re-enabling this, we should analysis the perf and inplace mutations of a few apps.
// It might be the case that our current benchmarks just aren't affected by morphic much.
gotcha
I don't know of any bugs since we changed it to solve_trivial
I forget, does it operate on traits?
or do we actually build an IR for it
build an ir for it
and that happens after specialization, so that's definitely a compiler perf downside :sweat_smile:
but yeah maybe it's not urgent to actually skip that work
if there aren't bugs in the trivial solver
Given we haven't noticed any measurable perf gains from the alias analysis, I am for just ripping it out. Might be worth trying to figure out a worst case example first just to see if it every makes a meaningful difference.
Theoretically, that would be some sort of hot loop with inplace updates. One version where morphic leads to inplace mutation without checking the refcount and one that just checks the refcount. Maybe I can just recursively loop a list calling List.set
?
Even without morphic, I can probably force the inplace flag just to make sure it is getting set ignoring any potential morphic bugs.
the thing is, I just don't think we're in a position to make it reliable anytime soon
Yeah, but if it could lead to large gains, may be worth just leaving it wired up in trivial mode. If not, probably worth ripping out.
I'm just not sure it's worth the dependency, the extra IR generation, the concerns about bugs, the build time, etc.
to be able to experiment with it cheaply
because in my mind, the real question is "can we someday have a version of it that runs reliably and quickly?"
if we can, then sure we might as well enable that because we already know we're set up for it
but that version of it has to exist first, and right now that is a completely hypothetical future thing :sweat_smile:
because the original authors haven't touched it in years, and none of us know how it works well enough to fix or rewrite it
so it's not like even if we were able to experimentally confirm that it was really great in some scenario, that we'd want to enable it in the compiler in general - because we know it'd just start causing bugs again
in other words, knowing that it has big benefits isn't actionable for us until we have a version that we can actually use long-term
so being able to experiment on it is almost closer to a curiosity - like "hey, cool, someday a hypothetical future version of Morphic that we could use for real will get some benefits in this one scenario!"
I guess if we started discovering some really big benefits it might be motivating to try to learn how it works in order to build out a real version, but that also seems like a very hypothetical scenario right now haha
Lambdasets are also from morphic, but we definitely want to keep those despite not 100% being sure we can make the compile in a reasonable about of time. Only recently with some of Ayaz's work did we feel we could compile them without bugs after a rewrite. I don't see how the rest of morphic is any different (assuming it can show similar perf gains)
oh for sure
I mean the Morphic solver specifically
the difference is that we actually understand how to do lambda sets
and we don't understand the rest of the solver
so we have problems but no way to fix them, and also no way in sight for getting to a point where we can fix them
(and also a payoff of unclear magnitude for what would definitely be a big project no matter what)
(big project being getting it to run fast and without bugs)
morphic and lambda sets are two different things
morphic is about specializing borrows
I'm not saying we should never revisit it or anything—there's a paper and a reference implementation, so it's certainly possible that we could figure out how it works someday—just that I think if we didn't have it in the compiler right now, I don't think we'd add it
lambda sets are about specializing indirect jumps
that's a cool way of putting it! :smiley:
I thought both came from the morphic research?
yes but the technologies are distinct
they implemented both in the language
yes
the morphic pass in roc does nothing wrt lambda sets
it could but it doesn't
Morphic does need all functions to be first-order, and lambda sets are a way to get that
which I believe was what led the authors of Morphic to come up with the idea of lambda sets
(which turned out to be valuable on their own to us)
because of LLVM etc.
well, it doesn't need first order functions. it just guarantees full borrow specialization if you have first order functions
you can run morphic over programs with indirect jumps, that's not a problem
i would think of them as fully orthogonal
cool
Ayaz, what are your thoughts about keeping it in the code base in its current state?
i would remove it if it's possible
idk if stuff breaks
but you know my stance on simplification haha
So this is a benchmark trying to trigger morphic. So this is probably about the best possible gain from morphic in general:
Benchmark 1: ./check-refcount
Time (mean ± σ): 68.6 ms ± 0.3 ms [User: 65.6 ms, System: 2.2 ms]
Range (min … max): 67.9 ms … 69.6 ms 100 runs
Benchmark 2: ./no-check-refcount
Time (mean ± σ): 21.9 ms ± 0.6 ms [User: 19.0 ms, System: 2.2 ms]
Range (min … max): 20.8 ms … 23.2 ms 100 runs
Summary
./no-check-refcount ran
3.13 ± 0.08 times faster than ./check-refcount
So morphic can be a big win.
Not sure what numbers could realistically come up in practice though.
The above code is from a hot loop that only calls List.set
. One version uses morphic to be statically inplace. The other is doing refcount checks for uniqueness.
Not saying this makes morphic worth keeping, might be too slow or buggy in practice (or simply have no one to actually own and fix it up eventually).
I always just worry that ripping something like this out will make it exceptionally unlike to ever be re-added. Leaving it (even in the trivial only state), may lead to us eventually looping back to it when the language has otherwise settled, fix it up, and see nice perf gains.
But I'm a sucker for things that go fast... :shrug:
i think a helpful thing to think about would be what performance ceiling are you all comfortable for a 0.1 release
if it is a hard requirement that it is the fastest thing out there sans ref counting that is one thing
if it needs to be comparable to existing functional languages/python/other web server players/etc maybe a different story
yeah I'm definitely comfortable with our current performance for 0.1
and how important is performance relative to stability
Part of the wierd reality that we live in is that ocaml exists and is reasonably performant yet essentially no one uses it. So perf likely isn't the big selling point to most people.
I'm very not comfortable with our current stability other than the parser (thanks to its fuzzing giving a lot of confidence, and setting aside the fact that we know it's temporarily in a more volatile state at this exact moment than it had been for a long time prior)
Brendan Hansknecht said:
Part of the wierd reality that we live in is that ocaml exists and is reasonably performant yet essentially no one uses it. So perf likely isn't the big selling point to most people.
well, 10% of the equities market relies on ocaml ;)
Another simple fact is that essentially no one who will use roc v0.1.0 will be using it for perf. They will be using it for the rest of roc with perf maybe being a nice afterthought.
So yeah, always compiling without crashes/segfaults, not having cffi bugs, having good errors almost all the time, having robust features will be much much more important for v0.1.0 and likely for many releases after that.
I personally care a ton about perf, but I am the exception who would be using rust or c++ instead of roc. Most folks will come from the other side (js, python, maybe go or ocaml) instead of roc.
I don't use Ocaml because it feels like a kitchen sink language. Most of the code I read for it is a different flavor than every other example I see. It's nice, but it's somewhat C++-y in that way. Not much experience, tho
I also personally care a ton about Roc's performance :grinning_face_with_smiling_eyes:
Aside how is ocamls perf in practice? Roughly in the same grouping as go?
yes ocaml is similar to go ime
compiler perf and runtime
i will also say ime performance doesn't really matter until you are at some scale. even then, most often performance is due to IO (can be solved separately), load (can be solved separately, horizontal or vertical scaling), or algorithmically (fix it, or if you need compute, easy hack is shell out). I love that Roc keeps in mind as a priority, but at the same time I have used many python webservers and CLI apps that were fine - and I'm sure Roc will be a better experience than that from day one
Yeah, perf is normally more about how much you have to scale (so cost, but servers are cheap) rather than language implementation. That and saving energy/battery life (kinda the reverse of scale: phone, embedded).
embedded is a good point
i don't know the distribution of realtime constraints in embedded, but if you need hard realtime roc probably isn't the language
Otherwise, it is mostly design rather than language (good io scheme, good algorithm).
Unless you need/want to be scrappy for some reason (like rewriting games from python to c++ so that they can simulate faster for alphazero implementation that can be trained on a single laptop). Most of the time perf is kinda optional, but nice to have a reasonable baseline for.
but if you need hard realtime roc probably isn't the language
Yeah, though I guess you could theoretically make guarantees about timing due to refcounting instead of gc.
That said, a lot of embedded is not true hard realtime. I mean a lot of embedded is running linux which even in real time mode really stretches the definition of real time.
fair
As a note, morphic is still being maintained. So there is a chance we could get someone interested from their team to help us.
Otherwise, yeah, probably should just rip out morphic. We don't have any idea why it is broke (could litterally just be a silly mistake in the ir we generate) and even when it does work, the core algorithm can get way too slow to be worth running during a roc build.
there are definitely bugs in the ir generation
there is at least one i remember
I am lightly looking into what it would take to remove this. I think it is only tangled into the llvm backend
That said, due to generating lots of extra specializations, it is tangled in reasonably deep. So it won't be simple to remove. That said, I would expect most of it to just be pretty manual, not necessarily hard.
I do think a solid amount of mapping exist that would be best to cleanup, but that will be harder to untangle than just removing morphic.
Last updated: Jul 06 2025 at 12:14 UTC