Stream: beginners

Topic: How can I prevent/detect implicit performance issues?


view this post on Zulip Austin Davis (Jul 16 2025 at 21:03):

... Or at the very least, is there a good guide on how to mentally model the performance characteristics?

I'm currently writing a recursive descent parser in Roc, and I want it to be fast and efficient (hence why Roc seemed like a great choice). But I have a few concerns about my code, because I kinda just assume certain kinds of compiler optimizations are performed, but I don't know how to verify those things. More specifically:

  1. Implicit data copying.

I understand optimistic mutation and vaguely how reference counting is used to determine when mutation is safe, but I don't know how "smart" the compiler is.

For example, in my current tokenizer implementation, I currently have a recursive "main loop" function that hands out work to smaller functions, and I pass around a read-only list of UTF-8 bytes along with a write-only list of tokens (which accumulates the final output). I assume that no list copies are performed as long as I don't attempt to read a list (the Token list in this case) that I'm repeatedly writing to. Is that a safe assumption? Does this optimization break down if the data is passed to other functions that likewise never read from the list? It seems reasonable to me, but I'm just not sure.

  1. Mutually recursive functions.

In some languages, mutually recursive functions can be optimized into imperative loops as long as the overall control flow is tail-recursive. I believe this is true of OCaml, for example. Is this true in Roc? If so, are there any edge cases to watch out for?

I have other minor considerations as well, but those are the main things. One nice thing about TCO for mutually recursive functions is that I can eliminate some struct/tuple allocations for return values in favor of just having the sub-loop functions call the main loop function with multiple arguments. Just curious about what I can safely assume here.

Thanks in advance to anyone with some insight on this!

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 01:12):

Yeah, tools are limited for this sadly.

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 01:12):

For copying, sounds like you have the right model

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 01:16):

Though you can read a list you are writing to. The important part is not having two locations holding onto it.

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 01:17):

Like if you mutate the list and then read an old copy of the list

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 01:18):

As for mutual recursion....I don't think roc guarantees it.

view this post on Zulip Austin Davis (Jul 17 2025 at 03:12):

@Brendan Hansknecht Thanks for the info! Quick follow-up question:

When you say "two locations holding onto it", what do you mean exactly? I am vaguely familiar with Rust's ownership model, which (I think) typically considers the "location" of references to be bounded by block scopes, which means passing a reference to a function introduces a second location. If this is also true in Roc's implementation, then that might mean that it performs a copy, right? Or is it dependent on both locations actually reading the variable?

Sorry for belaboring the point. I just want to understand what to expect.

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 03:21):

Yeah.

So the model really is the same as rust. You can have infinite immutable things (reads) and no copies will every happen. You can have exactly 1 mutable things (writes) and no copies will happen. If you have any other situtation, it will lead to a copy.

Fundamentally, I think the most common issue is passing a copy to a function that will write while also holding onto a soon to be stale copy:

x1 = [ ... ]
x2 = myFn(x1, ..) # this function writes to x1 and returns the modified x2.

# If x1 is ever used down here, myFn will incur a copy.

view this post on Zulip Austin Davis (Jul 17 2025 at 03:58):

Interesting, so maybe to get around this, I could have the function return a value to append to the Token list, and have the outer function actually call List.append, and because append is only called within that outer scope, it should avoid a copy, right?

Assuming that works, then the other question is whether the recursive call to the outer "main loop" function counts as another location. So would this work?

# Reads U8 list and returns the remaining U8 list and the Token to append
process_numeric : List U8 -> (List U8, Token)
process_numeric = |u8_list|
    (u8_list, []) # TODO: Implement process_numeric

tokenize : List U8, List Token -> List Token
tokenize = |u8_list, token_list|
    when u8_list is
        [char, .. as rest] if is_digit(char) ->
            (remaining_u8s, new_token) = process_numeric(u8_list)
            # Does this call to `tokenize` cause `token_list` to be copied?
            tokenize(remaining_u8s, List.append(token_list, new_token))

        [] -> token_list

view this post on Zulip Brendan Hansknecht (Jul 17 2025 at 04:03):

Note, specifically the issue is with reuse. This is just fine:

process_numeric : List U8, List Token -> (List U8, List Token)
process_numeric = |u8_list, token_list|
    # more calculations base on reading u8_list
    remaining_u8s = List.dropFirst ...
    new_token_list = List.append token_list new_token
    (remaining_u8s, new_token_list)


tokenize : List U8, List Token -> List Token
tokenize = |u8_list, token_list|
    when u8_list is
        [char, .. as rest] if is_digit(char) ->
            (remaining_u8s, new_token_list) = process_numeric(u8_list, token_list)
            # the important part is that now we only use new_token_list and never again token_list
            tokenize(remaining_u8s, new_token_list)

        [] -> token_list

view this post on Zulip Austin Davis (Jul 17 2025 at 04:20):

@Brendan Hansknecht I see, that makes sense. Thanks again, this was a big help!

view this post on Zulip Anton (Jul 18 2025 at 09:55):

A critical performance tip in case you didn't already know @Austin Davis, build with --optimize:

roc build my_main.roc --optimize

Last updated: Jul 26 2025 at 12:14 UTC