| |
| ▲ | lifthrasiir 5 days ago | parent | next [-] | | GC doesn't exactly solve your memory problem; it typically means that your memory problem gets deferred quite far until you can't ignore that. Of course it is also quite likely that your program will never grow to that point, which is why GC works in general, but also why there exists a desire to avoid it when makes sense. | | |
| ▲ | mjburgess 5 days ago | parent | next [-] | | Not sure why you're down-voted, this is correct. In games you have 16ms to draw billion+ triangles (etc.). In web, you have 100ms to round-trip a request under abitarily high load (etc.) Cases where you cannot "stop the world" at random and just "clean up garbage" are quite common in programming. And when they happen in GC'd languages, you're much worse off. | | |
| ▲ | Tomte 5 days ago | parent | next [-] | | That's why it's good that GC algorithms that do not "stop the world" have been in production for decades now. | | |
| ▲ | pebal 4 days ago | parent [-] | | There are none, at least not production grade. | | |
| ▲ | kelseyfrog 4 days ago | parent | next [-] | | There are none, or you're not aware of their existence? | | |
| ▲ | pebal 4 days ago | parent [-] | | There are no production implementations of GC algorithms that don't stop the world at all. I know this because I have some expertise in GC algorithms. | | |
| ▲ | kelseyfrog 4 days ago | parent [-] | | I'm curious why you don't consider C4 to be production grade | | |
| ▲ | pebal 4 days ago | parent [-] | | Azul C4 is not a pauseless GC. In the documentation it says "C4 uses a 4-stage concurrent execution mechanism that eliminates almost all stop-the-world pauses." | | |
| ▲ | kelseyfrog 4 days ago | parent [-] | | Except the documentation says > C4 differentiates itself from other generational garbage collectors by supporting simultaneous-generational con-
currency: the different generations are collected using concurrent (non stop-the-world) mechanisms | | |
|
|
|
| |
| ▲ | worthless-trash 4 days ago | parent | prev [-] | | I have heard (and from when I investigated) erlangs GC is 'dont stop the world'. Maybe my definition is bad though. |
|
| |
| ▲ | immibis 5 days ago | parent | prev [-] | | Java's ZGC claims O(1) pause time of 0.05ms. (As with any low-pause collector, the rest of your code is uniformly slower by some percentage because it has to make sure not to step on the toes of the concurrently-running collector.) | | |
| ▲ | Ygg2 5 days ago | parent | next [-] | | > Java's ZGC claims O(1) pause time of 0.05ms In practice it's actually closer to 10ms for large heaps. Large being around 220 GB. | |
| ▲ | riku_iki 5 days ago | parent | prev [-] | | With Java, the issue is that each allocated object carries significant memory footprint, as result total memory consumption is much higher compared to C++: https://benchmarksgame-team.pages.debian.net/benchmarksgame/... | | |
| ▲ | igouy 4 days ago | parent [-] | | The benchmarks game shows memory use with default GC settings (as a way to uncover space-time tradeoffs), mostly for tiny tiny programs that hardly use memory. Less difference — mandelbrot, k-nucleotide, reverse-complement, regex-redux — when the task requires memory to be used. Less with GraalVM native-image: https://benchmarksgame-team.pages.debian.net/benchmarksgame/... | | |
| ▲ | riku_iki 4 days ago | parent [-] | | > Less difference — mandelbrot, k-nucleotide, reverse-complement, regex-redux — when the task requires memory to be used. yes, I referred to benchmarks with large memory consumption, where Java still uses from 2 to 10(as in binary tree task) more memory, which is large overhead. |
|
|
|
| |
| ▲ | sph 5 days ago | parent | prev | next [-] | | That’s fair, no resource is unlimited. My point is that memory is usually the least of one’s problem, even on average machines. Productivity and CPU usage tend to be the bottleneck as a developer and a user. GC is mostly a performance problem rather than a memory one, and well-designed language can minimize the impact of it. (I am working on a message-passing language, and only allowing GC after a reply greatly simplifies the design and performance characteristics) | | |
| ▲ | charleslmunger 4 days ago | parent [-] | | >My point is that memory is usually the least of one’s problem, even on average machines. The average machine a person directly interacts with is a phone or TV at this point, both of which have major BoM restrictions and high pixel density displays. Memory is the primary determination of performance in such environments. On desktops and servers, CPU performance is bottlenecked on memory - garbage collection isn't necessarily a problem there, but the nature of separate allocations and pointer chasing is. On battery, garbage collection costs significant power and so it gets deferred (at least for full collections) until it's unavoidable. In practice this means that a large amount of heap space is "dead", which costs memory. Your language sounds interesting - I've always thought that it would be cool to have a language where generational GC was exposed to the programmer. If you have a server, you can have one new generation arena per request with a write barrier for incoming references from the old generation to the new. Then you could perform young GC after every request, only paying for traversal+move of objects that survived. |
| |
| ▲ | throwawaymaths 5 days ago | parent | prev [-] | | eh, there are GC languages famous for high uptimes and deployed in places where it "basically runs forever with no intervention", so in practice with the right GC and application scope, "deferring the concern till the heat death of the universe" (or until a CVE forces a soft update) is possible. | | |
| ▲ | lifthrasiir 4 days ago | parent [-] | | That's exactly why I said "it is also quite likely that your program will never grow to that point". Of course you need non-trivial knowledge to determine whether your application and GC satisfy that criteria. |
|
| |
| ▲ | jplusequalt 5 days ago | parent | prev | next [-] | | >Even AAA games these days happily run on GC languages. Which games are these? Are you referring to games written in Unity where the game logic is scripted in C#? Or are you referring to Minecraft Java Edition? I seriously doubt you would get close to the same performance in a modern AAA title running in a Java/C# based engine. | | |
| ▲ | steveklabnik 5 days ago | parent | next [-] | | Unreal Engine has a GC. You're right that there is a difference between "engine written largely in C++ and some parts are GC'd" vs "game written in Java/C#", but it's certainly not unheard of to use a GC in games, pervasively in simpler ones (Heck, Balatro is written in Lua!) and sparingly in even more advanced titles. | | |
| ▲ | jplusequalt 2 days ago | parent | next [-] | | Thanks for the Rust book! | | | |
| ▲ | tuveson 5 days ago | parent | prev [-] | | I think Balatro uses the Love2d engine which is in C/C++. | | |
| ▲ | steveklabnik 5 days ago | parent [-] | | Sure, but you write games in it in Lua. That Love2d is implemented in C++ (GitHub says like 80% C++ and 10% C) doesn't mean that you're writing the game in it. In my understanding, Love2d uses reference counting (which is still GC) for its own stuff, and integrates those into Lua's tracing GC. |
|
| |
| ▲ | Jasper_ 5 days ago | parent | prev | next [-] | | Unreal Engine has a C++-based GC. https://dev.epicgames.com/documentation/en-us/unreal-engine/... | |
| ▲ | neonsunset 5 days ago | parent | prev [-] | | C#? Maybe. Java? Less likely. |
| |
| ▲ | Narishma 5 days ago | parent | prev | next [-] | | > Even AAA games these days happily run on GC languages. You can recognize them by their poor performance. | |
| ▲ | withoutboats3 5 days ago | parent | prev [-] | | This is exactly the attitude this blog post spends its first section pretty passionately railing against. | | |
|
| |
| ▲ | hmry 5 days ago | parent | next [-] | | Neither of those gives memory safety, which is what the parent comment is about. If you release the temporary allocator while a pointer to some data is live, you get use after free. If you defer freeing a resource, and a pointer to the resource lives on after the scope exit, you get use after free. | | |
| ▲ | mjburgess 5 days ago | parent | next [-] | | The dialetic beings with OP, and has pcw's reply and then mine. It does not begin with pcw's comment. The OP complains about rust not because they imagine Jai is memory safe, but because they feel the rewards of its approach significantly outweight the costs of Rust. pcw's comment was about tradeoffs programmers are willing to make -- and paints the picture more black-and-white than the reality; and more black and white than OP. | |
| ▲ | francasso 5 days ago | parent | prev [-] | | While technically true, it still simplifies memory management a lot. The tradeoff in fact is good enough that I would pick that over a borrowchecker. | | |
| ▲ | junon 5 days ago | parent [-] | | I don't understand this take at all. The borrow checker is automatic and works across all variables. Defer et al requires you remember to use it, and use it correctly. It takes more effort to use defer correctly whereas Rust's borrow checker works for you without needing to do much extra at all! What am I missing? | | |
| ▲ | wavemode 5 days ago | parent | next [-] | | What you're missing is that Rust's borrowing rules are not the definition of memory safety. They are just one particular approach that works, but with tradeoffs. Namely, in Rust it is undefined behavior for multiple mutable references to the same data to exist, ever. And it is also not enough for your program to not create multiple mut - the compiler also has to be able to prove that it can't. That rule prevents memory corruption, but it outlaws many programs that break the rule yet actually are otherwise memory safe, and it also outlaws programs that follow the rule but wherein the compiler isn't smart enough to prove that the rule is being followed. That annoyance is the main thing people are talking about when they say they are "fighting the borrow checker" (when comparing Rust with languages like Odin/Zig/Jai). | | |
| ▲ | Rusky 5 days ago | parent | next [-] | | That is true of `&mut T`, but `&mut T` is not the only way to do mutation in Rust. The set of possible safe patterns gets much wider when you include `&Cell<T>`. For example see this language that uses its equivalent of `&Cell<T>` as the primary mutable reference type, and uses its equivalent of `&mut T` more sparingly: https://antelang.org/blog/safe_shared_mutability/ | |
| ▲ | junon 5 days ago | parent | prev [-] | | Rust doesn't outlaw them. It just forces you to document where safety workarounds are used. |
| |
| ▲ | vouwfietsman 5 days ago | parent | prev | next [-] | | > The borrow checker is automatic and works across all variables. Not that I'm such a Rust hater, but this is also a simplification of the reality. The term "fighting the borrow checker" is these days a pretty normal saying, and it implies that the borrow checker may be automatic, but 90% of its work is telling you: no, try again. That is hardly "without needing to do much extra at all". That's what you're missing. | | |
| ▲ | tialaramex 5 days ago | parent | next [-] | | What's hilarious about "fighting the borrow checker" is that it's about the lexical lifetime borrow checking, which went away many years ago - fixing that is what "Non-lexical lifetimes" is about, which if you picked up Rust in the last like 4-5 years you won't even know was a thing. In that era you actually did need to "fight" to get obviously correct code to compile because the checking is only looking at the lexical structure. Because this phrase existed, it became the thing people latch onto as a complaint, often even when there is no borrowck problem with what they were writing. Yes of course when you make lifetime mistakes the borrowck means you have to fix them. It's true that in a sense in a GC language you don't have to fix them (although the consequences can be pretty nasty if you don't) because the GC will handle it - and that in a language like Jai you can just endure the weird crashes (but remember this article, the weird crashes aren't "Undefined Behaviour" apparently, even though that's exactly what they are) As a Rust programmer I'm comfortable with the statement that it's "without needing to do much extra at all". | | |
| ▲ | leecommamichael 5 days ago | parent | next [-] | | I appreciate what you're saying, though isn't undefined behavior having to do with the semantics of execution as specified by the language? Most languages outright decline to specify multiple threads of execution, and instead provide it as a library. I think C started that trend. I'm not sure if Jai even has a spec, but the behavior you're describing could very well be "unspecified" not "undefined" and that's a distinction some folks care about. This being said, yes Rust is useful to verify those scenarios because it _does_ specify them, and despite his brash takes on Rust, Jon admits its utility in this regard from time to time. | | |
| ▲ | tialaramex 5 days ago | parent [-] | | > the behavior you're describing could very well be "unspecified" not "undefined" and that's a distinction some folks care about. Nah, it's going to be Undefined. What's going on here is that there's an optimising compiler, and the way compiler optimisation works is you Define some but not all behaviour in your language and the optimiser is allowed to make any transformations which keep the behaviour you Defined. Jai uses LLVM so in many cases the UB is exactly the same as you'd see in Clang since that's also using LLVM. For example Jai can explicitly choose not to initialize a variable (unlike C++ 23 and earlier this isn't the default for the primitive types, but it is still possible) - in LLVM I believe this means the uninitialized variable is poison. Exactly the same awful surprises result. | | |
| ▲ | leecommamichael 4 days ago | parent [-] | | Your reasoning appears to be: 1. because it is the kind of optimizing compiler you say it is 2. because it uses LLVM … there will be undefined behavior. Unless you worked on Jai, you can’t support point 1. I’m not even sure if you’re right under that presumption, either. | | |
| ▲ | tialaramex 4 days ago | parent [-] | | > because it is the kind of optimizing compiler you say it is What other kind of optimisations are you imagining? I'm not talking about a particular "kind" of optimisation but the entire category. Lets look at two real world optimisations from opposite ends of the scale to see: 1. Peephole removal of null sequences. This is a very easy optimisation, if we're going to do X and then do opposite-of-X we can do neither and have the same outcome which is typically smaller and faster. For example on a simple stack machine pushing register R10 and then popping R10 achieves nothing, so we can remove both of these steps from the resulting program. BUT if we've defined everything this can't work because it means we're no longer touching the stack here, so a language will often not define such things at all (e.g. not even mentioning the existence of a "stack") and thus permit this optimisation. 2. Idiom recognition of population count. The compiler can analyse some function you've written and conclude that it's actually trying to count all the set bits in a value, but many modern CPUs have a dedicated instruction for that, so, the compiler can simply emit that CPU instruction where you call your function. BUT You wrote this whole complicated function, if we've defined everything then all the fine details of your function must be reproduced, there must be a function call, maybe you make some temporary accumulator, you test and increment in a loop -- all defined, so such an optimisation would be impossible. |
|
|
| |
| ▲ | sensen7 5 days ago | parent | prev [-] | | >In that era you actually did need to "fight" to get obviously correct code to compile because the checking is only looking at the lexical structure. NLL's final implementation (Polonius) hasn't landed yet, and many of the original cases that NLL were meant to allow still don't compile. This doesn't come up very often in practice, but it sure sounds like a hole in your argument. What does come up in practice is partial borrowing errors. It's one of the most common complaints among Rust programmers, and it definitely qualifies as having to fight/refactor to get obviously correct code to compile. | | |
| ▲ | steveklabnik 5 days ago | parent [-] | | > What does come up in practice is partial borrowing errors. For some people. For example, I personally have never had a partial borrowing error. > it definitely qualifies as having to fight/refactor to get obviously correct code to compile. This is not for sure. That is, while it's code that could work, it's not obviously clear that it's correct. Rust cares a lot about the contract of function signatures, and partial borrows violate the signature, that's why they're not allowed. Some people want to relax that restriction. I personally think it's a bad idea. | | |
| ▲ | 5 days ago | parent | next [-] | | [deleted] | |
| ▲ | hmry 5 days ago | parent | prev [-] | | > Rust cares a lot about the contract of function signatures, and partial borrows violate the signature People want to be able to specify partial borrowing in the signatures. There have been several proposals for this. But so far nothing has made it into the language. Just to give an example of where I've run into countless partial borrowing problems: Writing a Vulkan program. The usual pattern in C++ etc is to just have a giant "GrahpicsState" struct that contains all the data you need. Then you just pass a reference to that to any function that needs any state. (of course, this is not safe, because you could have accidental mutable aliasing). But in Rust, that just doesn't work. You get countless errors like "Can't call self.resize_framebuffer() because you've already borrowed self.grass_texture" (even though resize_framebuffer would never touch the grass texture), "Can't call self.upload_geometry() because you've already borrowed self.window.width", and so on. So instead you end up with 30 functions that each take 20 parameters and return 5 values, and most of the code is shuffling around function arguments It would be so much nicer if you could instead annotate that resize_framebuffer only borrows self.framebuffer, and no other part of self. | | |
| ▲ | steveklabnik 5 days ago | parent [-] | | > People want to be able to specify partial borrowing in the signatures. That's correct. That's why I said "Some people want to relax that restriction. I personally think it's a bad idea." > The usual pattern in C++ etc is to just have a giant "GrahpicsState" struct that contains all the data you need. Then you just pass a reference to that to any function that needs any state. Yes, I think that this style of programming is not good, because it creates giant balls of aliasing state. I understand that if the library you use requires you to do this, you're sorta SOL, but in the programs I write, I've never been required to do this. > So instead you end up with 30 functions that each take 20 parameters and return 5 values, and most of the code is shuffling around function arguments Yes, this is the downstream effects of designing APIs this way. Breaking them up into smaller chunks of state makes it significantly more pleasant. I am not sure that it's a good idea to change the language to make using poorly designed APIs easier. I also understand that reasonable people differ on this issue. | | |
| ▲ | sensen7 5 days ago | parent [-] | | >Yes, this is the downstream effects of designing APIs this way. Breaking them up into smaller chunks of state makes it significantly more pleasant. What they're describing is the downstream effect of not designing APIs that way. If you could have a single giant GraphicsState and define everything as a method on it, you would have to pass around barely any arguments at all: everything would be reachable from the &mut self reference. And either with some annotations or with just a tiny bit of non-local analysis, the compiler would still be able to ensure non-aliasing usage. "functions that each take 20 parameters and return 5 values" is what you're forced to write in alternative to that, to avoid partial borrowing errors: for example, instead of a self.resize_framebuffer() method, a free function resize_framebuffer(&mut self.framebuffer, &mut self.size, &mut self.several_other_pieces_of_self, &mut self.borrowed_one_by_one). I agree that the severity of this issue is highly dependent on what you're building, but sometimes you really do have a big ball of mutable state and there's not much you can do about it. |
|
|
|
|
| |
| ▲ | junon 5 days ago | parent | prev [-] | | Maybe I'm spoiled because I work with Rust primarily these days but "fighting the borrow checker" isn't really common once you get it. | | |
| ▲ | vouwfietsman 5 days ago | parent [-] | | A lot has been written about this already, but again I think you're simplifying here by saying "once you get it". There's a bunch of options here for what's happening: 1. The borrow checker is indeed a free lunch
2. Your domain lends itself well to Rust, other domains don't
3. Your code is more complicated than it would be in other languages to please the borrow checker, but you are unaware because its just the natural process of writing code in Rust. There's probably more things that could be going on, but I think this is clear. I certainly doubt its #1, given the high volume of very intelligent people that have negative experiences with the borrow checker. | | |
| ▲ | steveklabnik 5 days ago | parent [-] | | "But after an initial learning hump, I don't fight the borrow checker anymore" is quite common and widely understood. Just like any programming paradigm, it takes time to get used to, and that time varies between people. And just like any programming paradigm, some people end up not liking it. That doesn't mean it's a "free lunch." | | |
| ▲ | vouwfietsman 2 days ago | parent [-] | | I'm not sure what you mean here, since in different replies to this same thread you've already encountered someone who is, by virtue of Rusts borrow checker design, forced to change his code in a way that is, to that person, net negative. Again this person has no trouble understanding the BC, it has trouble with the outcome of satisfying the BC. Also this person is writing Vulkan code, so intelligence is not a problem. > is quite common and widely understood This is an opinion expressed in a bubble, which does not in any-way disprove that the reverse is also expressed in another bubble. | | |
| ▲ | steveklabnik a day ago | parent [-] | | "common" does not mean "every single person feels that way" in the same sense that one person wanting to change their code in a way they don't like doesn't mean that every single person writing Rust feels the way that they do. |
|
|
|
|
| |
| ▲ | francasso 5 days ago | parent | prev [-] | | If your use case can be split into phases you can just allocate memory from an arena, copy out whatever needs to survive the phase at the end and free all the memory at once. That takes care of 90%+ of all allocations I ever need to do in my work. For the rest you need more granular manual memory management, and defer is just a convenience in that case compared to C. I can have graphs with pointers all over the place during the phase, I don't have to explain anything to a borrow checker, and it's safe as long as you are careful at the phase boundaries. Note that I almost never have things that need to survive a phase boundary, so in practice the borrow checker is just a nuissance in my work. There other use cases where this doesn't apply, so I'm not "anti borrow checker", but it's a tool, and I don't need it most of the time. | | |
| ▲ | Rusky 5 days ago | parent [-] | | You can explain this sort of pattern to the borrow checker quite trivially: slap a single `'arena` lifetime on all the references that point to something in that arena. This pattern is used all over the place, including rustc itself. (To be clear I agree that this is an easy pattern to write correctly without a borrow checker as well. It's just not a good example of something that's any harder to do in Rust, either.) | | |
| ▲ | francasso 5 days ago | parent [-] | | I remember having multiple issues doing this in rust, but can't recall the details. Are you sure I would just be able to have whatever refs I want and use them without the borrow checker complaining about things that are actually perfectly safe? I don't remember that being the case. Edit: reading wavemode comment above "Namely, in Rust it is undefined behavior for multiple mutable references to the same data to exist, ever. And it is also not enough for your program to not create multiple mut - the compiler also has to be able to prove that it can't." that I think was at least one of the problems I had. | | |
| ▲ | steveklabnik 5 days ago | parent | next [-] | | The main issue with using arenas in Rust right now is that the standard library collections use the still-unstable allocator API, so you cannot use those with them. However, this is a systems language, so you can use whatever you want for your own data structures. > reading wavemode comment above This is true for `&mut T` but that isn't directly related to arenas. Furthermore, you can have multiple mutable aliased references, but you need to not use `&mut T` while doing so: you can take advantage of some form of internal mutability and use `&T`, for example. What is needed depends on the circumstances. | |
| ▲ | Rusky 5 days ago | parent | prev [-] | | wavemode's comment only applies to `&mut T`. You do not have to use `&mut T` to form the reference graph in your arena, which indeed would be unlikely to work out. |
|
|
|
|
|
| |
| ▲ | MJGrzymek 5 days ago | parent | prev [-] | | Not sure about the implicit behavior. In C++, you can write a lot of code using vector and map that would require manual memory management in C. It's as if the heap wasn't there. Feels like there is a beneficial property in there. |
|