Reference Cycles in Rust, C++, and C# (safety part 2)

Sdílet
Vložit
  • čas přidán 3. 07. 2024
  • Part 1: • Memory safety in C++, ...
    Source code: github.com/contextfreecode/sa...
    0:00 Intro
    0:33 C++ (cpp)
    3:18 Zig
    3:41 Rust
    5:13 C#
    7:16 C# with structs
    9:48 C# structs attempting cycles
    10:55 Outro
  • Věda a technologie

Komentáře • 25

  • @contextfree
    @contextfree  Před rokem +18

    And as one errata point among many, you can get pointers in C# with unsafe, and out and ref parameters are also a limited form of pointers you can get to structs. Alternative perspectives in other languages on out/ref parameters might be interesting to consider in future videos as well.

  • @ArtemShoobovych
    @ArtemShoobovych Před rokem +5

    There is this impossibility triangle, similarly to CAP theorem, where developers have to balance performance, effort and (memory) safety. In a browser, with (hundreds of) thousands of nodes and constant re-rendering of half of the DOM tree, you might not want garbage collection (for the performance reasons) and you might not want to spend days and hours trying to make a trivial change. So pointers (raw or smart pointers, fwiw) might be the best option.

    • @nguyen-dev
      @nguyen-dev Před 9 měsíci +1

      I totally agree. I am looking for a modern programming language that balances these things.
      Rust development time is too unproductive, Go is oversimplified, and JVM languages are too heavy in runtime (Kotlin native heavy in packaging too).
      I really want to have a language that has a beautiful syntax like Kotlin but is fast and light like Go.
      Kotlin (a beautiful syntax, powerful standard library/data structures) + Go (with a light GC runtime, no VM, and with primitive concurrency) would be suitable for most cases in reality (from CP/Interviewing to production delivery).
      Only in special cases of performance and safety, something like Rust would be needed. Most of the time, I don’t need the level of Rust performance and memory safety for the high price of productivity and effort.

  • @kuhluhOG
    @kuhluhOG Před 6 měsíci +2

    I think the biggest gotcha here is C# since you normally don't really expect unsafe behaviour like this from a managed language (well, at least I wouldn't).

  • @doctorpropain8902
    @doctorpropain8902 Před rokem +15

    My initial inclination for something like a tree would be to use unique_ptr from parents to children, then use a raw pointer from child to parent. This is consistent with the ownership semantics of the situation as well: The parent node *owns* the child node, and it's the only owner of the child node. The child node neither owns nor cares about ownership semantics when accessing its parent, so it uses a raw pointer. It also knows this raw pointer is valid, since the parent node is guaranteed to live until at least the same point in time as its children.
    You mentioned that using the shared_ptr gave you the additional safety of requiring to access a parent node via a weak_ptr, but I don't think there's actually any additional safety when compared to using a raw pointer as long as you hold to the invariant that a parent node will always live at least as long as its children.
    In Rust, you also used the reference counting pointer (Rc) rather than Box (which I believe is Rust's equivalent of unique_ptr). Is there a straightforward way to use Box instead, since the memory of a node is really only owned by its parent?

    • @contextfree
      @contextfree  Před rokem +3

      Yeah, good thoughts. I guess what the shared_ptr provides here is a way to hand off extra references that can be retained. Which might also make safe usage more likely if not bringing in a lifetime checker. And I'm skeptical you could get the parent pointer in Rust just using Box for kids without either unsafe or Rc/Arc, but I haven't tried it.

    • @shimadabr
      @shimadabr Před rokem +3

      I agree. I stumbled upon this recently when thinking how to implement a doubly-linked list with unique_ptr. What i concluded is very similar: the "next" pointer is better as a unique_ptr and the "prev" pointer as a raw pointer. The header pointer is also a unique_ptr, so we have all the nodes with ownership semantics. The node which links to the next node owns that node, while that nodes that are linked to don't own the ones they are being linked to.

    • @kevindelnoye9641
      @kevindelnoye9641 Před rokem +3

      This is a correct solution be carefull for out of stack memory error because of recursive deallocation of nodes.
      See herb Sutter leak freedom talk at cppcon from 12:00 to 22:00 where this is discussed. The rest of the talk is also great

    • @wanderingthewastes6159
      @wanderingthewastes6159 Před rokem

      “It also knows the parent pointer is valid” What if my rebalancing promotes a child node to root?

    • @contextfree
      @contextfree  Před rokem

      Yeah, you'd have to update parent pointers for any surgery. But you'd have to do that anyway. Just have to make sure it's all sufficiently atomic.

  • @diadetediotedio6918
    @diadetediotedio6918 Před rokem +6

    9:33
    C# lists would only reallocate internal arrays if they max out the capacity, in both your actions the list array need not to change

    • @contextfree
      @contextfree  Před rokem +2

      Thanks for adding the extra detail! Adding *might* cause reallocation if already at capacity, which I didn't check. As for clear, I reviewed later and saw that yeah, it specifically says it doesn't change capacity. Which is what I'd want, so no complaints from me. But it seems they like to zero the memory on clear (at least sometimes?), which improved safety this time around.

  • @Bloodthirst
    @Bloodthirst Před rokem +1

    btw for C# in your Box workaround , instead of creating a reference type to wrap the struct's value , you can directly type "Node? parent" , it's syntax sugar for "Nullable parent"
    EDIT : it actually wont solve the issue , explanation in the responses

    • @contextfree
      @contextfree  Před rokem +1

      Thanks for the tip! I guess the Box might still be useful if nullable isn't wanted. Although in this case, I did want nullable. And maybe being explicit for video purposes is also nice here. While working the video, I did find some existing type from MS that I based this one on, but it was in a library that's not part of dotnet core, and it didn't seem to be available for me on Linux. I'm having trouble finding the link again to it at the moment.

    • @MarioAndWeegee3
      @MarioAndWeegee3 Před rokem +5

      Nullable is a struct in C#, so it wouldn't actually work

    • @Bloodthirst
      @Bloodthirst Před rokem +2

      @@MarioAndWeegee3 Oh you're right ! i just checked and it will actually cause the same "circular definition" since Nullable is just a bool and a T.
      I'm used to using the "Compare" method in Nullale class , so i thought Nullable was just a generic version of the same class , that's good to know , thnx for the info

  • @neuralwarp
    @neuralwarp Před rokem +2

    I'm sure this all makes perfect sense if you already know it.

  • @antronixful
    @antronixful Před rokem +1

    i actually hate rust

  • @kamertonaudiophileplayer847

    How can you program in Rust? The code is twice slower than C++. Terrible, terrible language.

    • @sporefergieboy10
      @sporefergieboy10 Před rokem +6

      GOATed comment

    • @kamertonaudiophileplayer847
      @kamertonaudiophileplayer847 Před rokem +3

      @@sporefergieboy10 Thanks.

    • @tropicbliss1198
      @tropicbliss1198 Před rokem +9

      I know you are joking but to those who are unaware debug mode rust is extremely unoptimised, sometimes even slower than python. yes it’s a problem 😅

    • @kamertonaudiophileplayer847
      @kamertonaudiophileplayer847 Před rokem

      @@tropicbliss1198 Sure, C++ is also a terrible slow in the mode. However I didn't find any key to change it. Did you mean -C opt-level=2?

    • @tropicbliss1198
      @tropicbliss1198 Před rokem +3

      @@kamertonaudiophileplayer847 By default the optimisation level of release mode is -O3, it’s kinda like gcc in this regard. However you can also use a -r flag to set it to release mode and it will be optimised, though if you want even more optimisation (and even slower compile times) you can set lto to true or even “fat” in Cargo.toml