Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

> GC/runtime

1. Runtime: A runtime is any code that is not a direct result of compiling the program's code (i.e. it is used across different programs) that is linked, either statically or dynamically, into the executable. I remember that when I learnt C in the eighties, the book said that C isn't just a language but a rich runtime. Rust also has a rich runtime. It's true that you can write Rust in a mode without a runtime, but then you can barely even use strings, and most Rust programs use the runtime. What's different about Java (in the way it's most commonly used) isn't that it has a runtime, but that it relies on a JIT compiler included in the runtime. A JIT has pros and cons, but they're not a general feature of "a runtime".

2. GC: A garbage collector is any mechanism that automatically reuses a heap object's memory after it becomes unreachable. The two classic GC designs, reference counting and tracing, date back to the sixties, and have evolved in different ways. E.g. in the eighties and nineties there were GC designs where the compiler could either infer a non-escaping object's lifetime and statically insert a `free` or have the language track lifetimes ("regions", 1994) and have the compiler statically insert a `free` based on information annotated in the language. On the other hand, in the eighties Andrew Appel famously showed that moving tracing collectors "can be faster than stack allocation". So different GCs employ different combination of static inference and dynamic information on object reachability to optimise for different things, such as footprint or throughput. There are tradeoffs between having a GC or not, and they also exist between Rust (GC) and Zig (no GC), e.g. around arenas, but most tradeoffs are among the different GC algorithms. Java, Go, and Rust use very different GCs with different tradeoffs.

So the problem with using the terms "runtime" and "GC" colloquially as they're used today is not so much that it differs from the literature, but that it misses what the actual tradeoffs are. We can talk about the pros and cons of linking a runtime statically or dynamically, we can talk about the pros and cons of AOT vs. JIT compilation, and we can talk about the pros and cost of a refcounting/"static" GC algorithm vs a moving tracing algorithm, but talking in general about having a GC/runtime or not, even if these things mean something specific in the colloquial usage, is not very useful because it doesn't express the most relevant properties.





It's pretty obvious from the context that runtime/GC means having a runtime with a tracing GC - and the tradeoffs are well known. These discussions were played out over the last two decades - we all know GC can be fast, but there were and are plenty of use-cases where the tradeoffs are so bad that it's a non-starter.

Not to mention that writing a high quality GC is a monumental task - it took those decades for C# and Java to get decent - very few projects have the kind of backing to pull that off successfully.

In practical terms think about the complexity of enabling WASM that someone mentioned in this thread when you reuse C runtime and skip tracing GC.

I'm kind of venting in the thread to be fair, Walter Bright owes me nothing and it's his project, I had fun playing with it. I'm just sad we couldn't have gotten to Zig 20 years ago when we were that close :)


The "GC is slow"/"JIT/VM's are slow" is such a tired, dated take at this point.

Look at C#'s competitive placings with C++/Rust on The Computer Benchmark game due to .NET's ruthless optimization.


Ironically those optimizations came from .NET avoiding GC and introducing primitives to avoid it better.

And .NET is moving heavily into the AoT/pre-compilation direction for optimization reasons as well (source generators, AoT).

If you look at the change logs for the past few versions of the framework from perf perspective the most significant moves are : introduce new primitives to avoid allocating, move more logic to compile time, make AoT better and work with more frameworks.


So what, that is exactly the point.

A programming language having a GC doesn't mean every single allocation needs to be on the heap.

C# is finally at the sweet spot languages like Oberon, Modula-3 and Eiffel were on the late 90's, and unfortunely were overshadowed by Java's adoption.

Go and Swift (RC is a GC algorithm) are there as well.

D could be there as well on the mainstream, if there was a bit more steering into what they want to be, instead of having others catching up on its ideas.

That is what made me look into the language after getting Andrei Alexandrescu's book.


The point is that if you need performance you need to drop below tracing GC, and depending on your use-case, if that's the majority of your code it makes sense to use a language that's built for that kind of programming (zero cost abstractions). Writing C# that doesn't allocate is like wearing a straightjacket and the language doesn't help you much with manual memory management. Linters kind of make it more manageable but it's still a PITA. It's better than Java for sure in that regard, and it's excellent that you have the option for hot paths.

I rather have the productivity of a GC (regardless of which kind), manual allocations on system/unsafe code blocks, and value types, than going back to bare bones C style programming, unless there are constraints in place that leave no other option.

Note that even Rust's success, has triggered managed languages designers to research how far they can integrate linear, affine, effects, dependent types into their existing type systems, as how to combine the best of both worlds.

To the point that even Rust circles now there are those speaking about an higher level Rust that is supposed be more approachable.


This is essentially how I feel -- GC by default with user control over zero-copy/stackalloc behavior.

Modern .NET isn't even difficult to avoid allocations, with the Span<T> API and the work they've done to minimize unnecessary copies/allocs within the std lib.

(I say this as a Kotlin/JVM dev who watches from the sideline, so not even the biggest .NET guy around here)


> Writing C# that doesn't allocate is like wearing a straightjacket

Yes, and that's where D is majorly superior to C# -- it's flexible enough for you to go down to the metal for the critical parts.


That is the no longer the case in C# 14/.NET 10, D has lost 16 years counting from Andrei's book publishing date, letting other programing languages catch up to more relevant features.

You are forgetting that a language with a less mature ecosystem isn't much help.


> C# 14/.NET 10

Yes, they added AOT but it's still challenging to do anything that requires calling into the OS, because you're going to need the bindings. It will still add some overhead under the hood and more overhead will you need to add yourself to convert the data to blittable types and back.

Mixing C# with other languages in the same project is also difficult because it only supports MSBuild.

> You are forgetting that a language with a less mature ecosystem isn't much help.

Fair.


You also need bindings in D, nothing new there.

Rust also has issues using anything besides cargo.


> Rust also has issues using anything besides cargo.

D also has its own build system but it's not the only option. Meson officially supports building D sources. You could also easily integrate D with SCons, though there's no official support.


Well, you can do the same with Java and C#, assuming you actually know the ecosystem.

> You also need bindings in D, nothing new there.

You don't. Any D compiler is a C compiler too, so it can take C headers without bindings or any overhead added.


You have forgotten the footnote that not everything has a C API, not BetterC supports everything in ISO C, or common extensions.

Hint, before keeping to discuss what D can and cannot do, better go look how long I have been around on D forums, or existing projects on my Github.


> not everything has a C API

Anything that has stable ABI does.

> go look how long I have been around on D forums

You claimed higher up in this thread that D requires bindings to interoperate with C API. You don't seem that well informed really.


Does Rust really require reference counting? I thought Rust programs only used reference counting when types like Rc and Arc are used.

Swift seems to require reference counting significantly more than Rust.


Op saying Rust has a kind of GC is absurd. Rust keeps track of the lifetime of variables and drops them at the end of their lifecycle. If you really want to call that a GC you should at least make a huge distinction that it works at compile time: the generated code will have drop calls inserted without any overhead at runtime. But no one calls that a GC.

You see OP is trying to murk the waters when they claim C has a runtime. While there is a tiny amount of truth to that, in the sense that there’s some code you don’t write present at runtime, if that’s how you define runtime the term loses all meaning since even Assemblers insert code you don’t have to write yourself, like keeping track of offsets and so on. Languages like Java and D have a runtime that include lots of things you don’t call yourself, like GC obviously, but also many stdlib functions that are needed and you can’t remove because it may be used internally. That’s a huge difference from inserting some code like Rust and C do. To be fair, D does let you remove the runtime or even replace it. But it’s not easy by any means.


> If you really want to call that a GC you should at least make a huge distinction that it works at compile time: the generated code will have drop calls inserted without any overhead at runtime. But no one calls that a GC.

Except for the memory management literature, because it's interested in the actual tradeoffs of memory management. A compiler inferring lifetimes, either automatically for some objects or for most objects based on language annotations, has been part of GC research for decades now.

The distinction of working at compile time or runtime is far from huge. Working at compile time reduces the work associated with modifying the counters in a refcounting GC in many situations, but the bigger differences are between optimising for footprint or for throughput. When you mathematically model the amount of CPU spent on memory management and the heap size as functions of the allocation rate and live set size (residency), the big differences are not whether calling `free` is determined statically or not.

So you can call that GC (as is done in academic memory management research) or not (as is done in colloquial use), but that's not where the main distinction is. A refcounting algorithm, like that found in Rust's (and C++'s) runtime is such a classic GC that not calling it a GC is just confusing.


P.S.

I should add that the JVM (and Go) also infers lifetime for non-escaping objects and "allocates" them in registers (which can spill to the stack; i.e. `new X()` in Java may or may not actually allocate anything in the heap). The point is that different GCs involve compiler-inferred lifetimes to varying degrees, and if there's a clear line between them is less the role of the compiler (although that's certainly an interesting detail) and more whether they generally optimise for footprint (immediate `free` when the object becomes unreachable) or throughput (compaction in a moving-tracing collector, with no notion of `free` at all).

There are also big differences between moving and non-moving tracing collectors (Go's concurrent mark & sweep and Java's now removed CMS collector). A CMS collector still has concepts that resemble malloc and free (such as free lists), but a moving one doesn't.


> A refcounting algorithm, like that found in Rust's (and C++'s) runtime is such a classic GC that not calling it a GC is just confusing.

But is it not easy to opt out of in C, C++, Zig and Rust, by simply not using the types that use reference counting?

And how does your performance analysis consider techniques like arenas and allocating at startup only?


> But is it not easy to opt out of in C, C++, Zig and Rust, by simply not using the types that use reference counting?

In C, Zig, and C++ sure. In Rust? Not without resorting to unsafe or to architectural changes.

> And how does your performance analysis consider techniques like arenas and allocating at startup only?

Allocating at startup only in itself doesn't say much because you may be allocating internally - or not. Arenas indeed make a big difference and share some performance behaviours with moving-tracing collectors, but they can practically only be used "as god intended" in Zig.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: