3 ms·
Most GC langs have these features too they're just not in your face. Technically C# has true value types like C++. Rust makes the distinction between stack and
by consteval 2y ago
Most GC langs have these features too they're just not in your face. Technically C# has true value types like C++.
Rust makes the distinction between stack and heap references, like C++. Other, more high-level languages don't - there's only one kind of reference, you can't take a reference to a stack object. Maybe you implement that by making all objects heap allocating (Java) or you just say they have to be copied every time (C# struct). That's really where the difference is.
There's a lot of juicy, juicy performance there. The problem is taking references to stack variables is problematic. Tracking heap objects with a GC or a ref counter is really trivial in comparison IMO, at least when you try to combine the systems.
- neonsunset 2y agoThe assessment on C# does not match language spec at all. Not only instance methods on C# structs are implicitly byref, you can easily pass structs by reference via ref, out and in keywords. On top of that, ref structs can hold `byref` pointers aka 'ref' keyword which can point to arbitrary memory, or have references to other structs/variables/anything. There is also regular C syntax with &T and T* for unmanaged references/pointers. On top of that, .NET's compiler has gotten very good at struct optimizations and pretty much ensures they stay in registers all the time unless address-taken, including SIMD registers for Vector<T> and Vector128/256/512<T> even their "deconstructed" form when specified width is not supported so they get handled as e.g. 256x2. There was a big jump in codegen quality in .NET 8 which can now sometimes trade blows with GCC and Clang around struct optimizations. All these features are first-class and are heavily used by all kinds of performance-sensitive code. Also, structs can implement interfaces and can be generic arguments that satisfy interface constraints, which works exactly like generics with trait bounds in Rust - you get a generic instantiation aka monomorphized function body, making the abstraction zero-cost.
- consteval 2y agoWow, I was not aware of the monomorphization of structs in C#. That's very interesting, I wonder how you're able to mix generic structs and generic classes seamlessly. > you can easily pass structs by reference via ref, out and in keywords. On top of that, ref structs can hold `byref` pointers aka 'ref' keyword which can point to arbitrary memory These are not features I've encountered. I wonder how you solve dangling references when those references could point to automatic stack variables.
- neonsunset 2y ago> Wow, I was not aware of the monomorphization of structs in C#. That's very interesting, I wonder how you're able to mix generic structs and generic classes seamlessly. The handling is transparent, in a way. As implemented by CoreCLR, class-type generic arguments have shared representation named __Canon. This means that, for example, a `Dictionary<int, string>` has a generic instantiation indicated as `Dictionary<int, __Canon>` where __Canon is an implicit generic type argument passed alongside relevant calls. Statics referencing that do get exact address, and there is quite a bit of complexity regarding runtime handling of this as far as virtual calls and other edge cases are involved, but as a programmer you are never exposed to that directly. It's an implementation detail, and even un-monomorphized cases work rather fast in most situations, like standard data containers. > These are not features I've encountered. I wonder how you solve dangling references when those references could point to automatic stack variables. Not sure what you mean by automatic stack variables, but the idea behind byref pointers\managed references\'ref's is that they are not allowed to be boxed or otherwise placed on the heap. This lifetime restriction enables key scenarios: - byrefs can point to object interiors without hindering GC throughput - byrefs can point to stack memory, allowing `var span = (stackalloc byte[32]);` and more - byrefs can point to any unmanaged memory without requiring excessive range checks by GC This way you can use `ref T` to represent any memory location and `Span<T>` to represent any contiguous memory range up to 2B elements, without having to carry around bespoke overloads and types that disambiguate between containers and memory sources, much like you would usually see in most other GC-based languages. There is also additional lifetime analysis in Roslyn to prevent you from returning 'scoped' 'ref's to an outer scope, like having a ref point to an integer in the current method body and returning it to the caller - this will not compile, unless you override it with unsafe [UnscopedRef] which you should never do unless you are absolutely certain (every time I used it and was absolutely certain, the compiler was right and I was not :D). This also works "through" ref structs and other tricky scenarios, which means you can return a ref that points to a middle of heap-allocated array - the scope of object exceeds current method, and byref can keep the array rooted even if no other reference to it exists. The main restriction is byrefs can mostly flow "downward" as not to escape the scope they originate from, but that's a given in most scenarios in Rust just as much. There is a basic walkthrough about byrefs here: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/ref https://learn.microsoft.com/en-us/dotnet/csharp/language-ref... C# is good at systems programming - it also has portable SIMD and intrinsics, static linking, native compilation and zero-cost FFI. Engineers getting surprised by this fact rather than upset is the happier but unfortunately less frequent outcome :)
- est31 2y ago> Most GC langs have these features too they're just not in your face. That's what I meant by "strong and widely used default". Defaults and syntactic sugar do matter and influence the type of code written.