Тёмный

Rant: Entity systems and the Rust borrow checker ... or something. 

Jonathan Blow
Подписаться 85 тыс.
Просмотров 234 тыс.
50% 1

Commentary on the closing keynote from RustConf 2018, which you can view here:
• RustConf 2018 - Closin...

Опубликовано:

 

28 сен 2024

Поделиться:

Ссылка:

Скачать:

Готовим ссылку...

Добавить в:

Мой плейлист
Посмотреть позже
Комментарии : 605   
@fschutt247
@fschutt247 6 лет назад
I've worked roughly 16 months fulltime in Rust now. And I have to say - I do agree with most of the points you made. In 30k lines, the borrow checker has saved my ass 3 - 10 times in a way where I in hindsight knew that those bugs would have been very hard to catch. The other times it was more or less just annoying because I knew that some things were safe, but the borrow checker was overly restrictive or didn't "get" what I wanted to express, so I had to refactor my code just to make it pass the checker - but that doesn't mean that the checker "forced me to write good code", no, the borrow checker was just too dumb to get what I mean and I had to write more syntax to make it understand. I also don't hit the borrow checker problems that much anymore, but yesterday was an exception for example where I had a really nasty borrowing problem. I've come to realize that Rust takes a long-ass time to write but once it's written, it's pretty solid and you don't run into last-minute-please-fix-issues. That is both a good and a bad thing. On the one hand, it allows you to schedule and plan things somewhat easily. On the other hand, productivity in Rust is pretty low, because many libraries are simply missing because good code, which Rust forces you to write, ( *not necessarily courtesy of the borrow checker* ) takes long to write and there is a lot of friction. Also compile times are abysmal, because of the whole type-checking (yes, lots of generics and macros take time to type-check), which is simply something that decreases the productivity. I did not use Rust because of the borrow checker though, I find it nice and certainly useful (esp. in regards to preventing iterator invalidation), and it's a new, unique feature in PL design (at least I am not aware of any other language that has a borrow checker) but people give it too much credit. There are other features that Rust has that prevent large swaths of bugs - discriminated unions and exhaustive pattern matching (versus switch on a number with fallthrough default case), or Option / Result error handling - those things are great. On the other hand, I don't write game engines, but I knew that I have to possibly maintain my code for decades. Which is one of the reasons I chose Rust for a project. In hindsight, not the best move, because programming is in the end, a business and you need to ship on time. In hindsight, I'd rather deal with debugging a few NullPointerExceptions rather than spending 6 months writing a certain library that doesn't exist in Rust. So I think that if you want to get your language off the ground, libraries (esp. if game-focused) are a huge deal. There is certainly truth to the argument that Rusts way of preventing mistakes via borrow checking is just *one* way, not *the* way, to prevent logical mistakes. Because in the end, you don't want to prevent pointer mistakes, you want to prevent logic mistakes (which express themselves as pointer misuse). For example, a bug I had in Rust was that I forgot to update a cache after reloading a file - the borrow checker passed fine, but it didn't know about my application logic or the fact that I had to update my cache. You *can* have two mutable pointers to some memory just fine, it's just that it's a common logic mistake to update one and forget that you have another pointer. But that's, at its core, a logic mistake and Rusts way of mutability-checking is just one way to prevent against one class of bugs. However, Rust gives me good tools to at least try to prevent these mistakes, mostly by leveraging the type system. For example, if I have a function B that needs a struct C, and the only way to get a struct C is by calling function A first - then I have a logical "chain" where I can't forget to call function A before function B (Rusts move semantics help a bit here). Otherwise it wouldn't compile. Or (in the example) I want to render something, but need an "UpdateKey" struct first and the only way to get that is by calling the generational-update function. This is how at least I have learned to build APIs that are essentially fool-proof, so that you shouldn't be able to forget to update something before you call something else. But overall, I still think Rust is a decent language that at least tries to push PL research forward and this video highlighted a good argument, so thanks for this video. I am hopeful to see how Jai pans out.
@trolledwoods377
@trolledwoods377 5 лет назад
@Sam Claus Not entirely sure that the optimization is super great though, there was a talk about the stylo css engine for firefox that was written in rust, and there he mentioned how much memory those Option types uses. I think it's in very specific cases those optimizations are made, and not generally. That said I do like how rust does a lot of things, and the borrow checker is pretty neat when you're not used to manually deallocating things in cpp, but instead come from c#
@richardgomes5420
@richardgomes5420 4 года назад
There's no way the borrow checker knows what you are intending to do. I can only know what you've actually done. If you actually done in unsafe way... that is an error. In this case, you have to redo what you were primarily intending to do in some sort of safe way. This is not really "turning off the borrow checker" but "learning how to implement your intentions in some sort of actual safe code". And, if you watched these both talks... this is the primary point defended since the beginning: the borrow checker "forces" you to rethink how to do things and implement them safely.
@Booone008
@Booone008 4 года назад
@@richardgomes5420 As Jon explained, the implementation of ECS that the borrow checker "forced" the original presenter to implement still doesn't solve the stale reference safety problem. She still had to tack the GenerationalIndex mechanism on top of it to actually ensure correctness ("the thing at this location/index is actually the component I am looking for now, not some replaced version of it"). In this case, refactoring your system and only storing a (non-generational) EntityIndex instead of a memory pointer satisfies the borrow checker, giving you a false sense of "this is now safe", but it only solves some symptoms of the problem, not the problem itself -- the index can still refer to the wrong component.
@berthold64
@berthold64 4 года назад
tldr rust is annoying and gets in your way lol
@keldwikchaldain9545
@keldwikchaldain9545 4 года назад
@@Booone008 There's a bit of a misunderstanding here (and in the video in general) of what "safe" means in the rust world. "safe" is a very specific and well-defined term, and in that definition, the non-generational index is "safe", despite being incorrect. "safe" in the rust world is very specific to disallowing code from having mutable pointer aliasing and use after free bugs (as well as a couple other things), which definitely doesn't include "is this thing semantically correct in my program".
@1schwererziehbar1
@1schwererziehbar1 2 года назад
I love hearing ideas on technical stuff from someone who has already proven his expertise by making a full game. Thanks for the upload.
@AndrewRogers
@AndrewRogers 6 лет назад
I feel that your comment at roughly 39:00 is kind of what I experienced from learning Rust this summer. Because the C++ compiler would let me do something that the Rust borrow checker would not, I was forced to reevaluate past habits or patterns and it changed how I code C++ as well to be more mindful.
@Be_Captain
@Be_Captain 5 лет назад
Let us also not forget that array[i] is just syntactic sugar for *(&array+i) in basically any language, which is itself just pointer arithmetic. By using this syntax you may be allowing some compiler warnings about out-of-bounds accesses, but only if your compiler supports it and it's turned on (eg, clang likely won't complain unless you're using clang -analyze).
@Be_Captain
@Be_Captain 5 лет назад
Andrea Proone yes, that's better 😂
@camthesaxman3387
@camthesaxman3387 2 года назад
The same problem you described manifests itself in Super Mario 64 (as the cloning glitch) and Ocarina of Time (as stale reference manipulation) because both games use raw pointers to entities that get deleted, but references to them aren't updated in some cases.
@user-hj6db7fp4i
@user-hj6db7fp4i 6 лет назад
I disagree when you say she didn't realize she was re-implementing an allocator by using Vecs to store entities and indexs to refer to them. She specifically refers to the case of accessing the index of an entity that's been deleted as being very similar to a use-after-free bug, and indexes as being pseudo-pointers (ru-vid.com/video/%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE-aKLntZcp27M.html), and later on calls a new vec implementation (for generation indexes) an allocator (ru-vid.com/video/%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE-aKLntZcp27M.html). I think how she uses the term "safe" may have misled you. Saying something is "safe" has a very specific meaning in Rust: no race-conditions, no double-frees, and no use-after-frees; most else is still fair game. That's why when she says it's "safe" but isn't great, she means it compiles (doesn't violate ownership, lifetimes, etc.), but that doesn't mean it's good (the code may still have serious logical flaws). I think it's really interesting to consider this almost a step backwards (C++ programs very well may segfault in debug builds due to these logical errors, whereas Rust won't), it seems like all higher-level languages would suffer from that too. At 39:00, when you say the borrow checker may have helped by forcing the developer to explore other options, I think that's exactly what Catherine is referring to when she says the borrow checker helps. It's turned a memory access error into a smaller possible set of logic errors. With Rust lifetimes, it wouldn't be possible to double-free or use-after-free an entity, only a pseudo-use-after-free bug is possible (using the index after it's been freed and been re-allocated). The way Vecs are implemented in Rust, it isn't possible for an index to point to a deallocated object (either you have a Vec of optionals, or when you remove an item the Vec shifts later items to the left). I think this naturally forces you to consider this use-after-free logical error, which would naturally lead you towards the generational solution (or some other solution, or at least into a wall) when developing. One last nitpick, I dislike how you're pulling apart code that in the context of the presentation is shown as code with problems in it. If you're going to do a rant on code I think you should at least wait till she shows the best examples, so you don't end up fighting a straw-man. Enjoyed the discussion though overall, it's really educational engaging with C++ programmers as someone who's only written lower-level code in Rust. Hopefully others more knowledgeable about Rust can chime in, I'm definitely no Rust expert
@marvalgames3939
@marvalgames3939 4 года назад
My thoughts exactly
@anotherelvis
@anotherelvis 6 лет назад
At 33:00 he sums up the arguument that the borrow checker does not provide generational indexes out of the box. If you store your entities in a Vec with fixed size and use integers for indexes, then you will eventually need to reuse slots to save memory, and that requires you to add a generation-id to distinguish between the old inhabitant of the slot and the new inhabitant of the slot. AFAICS the project named generational-arena on github provides a nice container type with generational indexes. I haven't tried it but it seems to work. But Jonathans point is that you could have writtten the same code in C++. The usercomment at 49:10 sums up the benefits of rusts borrow checker. You get some aliasing guarantees and some promises regarding parallel code. Jonathan is probably right about productivity. If you need to build your game engine from scratch then rust will provide some friction and force some redesigns before you get it right, but if you use a premade container type such as generational-arena or an ECS-library such as specs or if you copy the design from Catherine's video, then you will experience less friction. So in short rustbecomes more productive when you use existing libraries that enforce nice programming patterns.
@AlFredo-sx2yy
@AlFredo-sx2yy Год назад
"So in short rust becomes more productive when you use existing libraries that enforce nice programming patterns." So... just like any other programming language? So in short, [insert programming language here] becomes more productive when you use existing libraries that enforce nice programming patterns. That's not exactly an argument for or against Rust tbh. Because C is way older and has far more mature premade libraries and support, but that does not say anything about the language itself.
@DigitalDuo2211
@DigitalDuo2211 6 лет назад
Jonathan lives in The Witcher's universe; he went from day-time to night-time in 35 minutes!
@josephp.3341
@josephp.3341 6 месяцев назад
California
@cranil
@cranil 5 месяцев назад
😂
@borkborkas8422
@borkborkas8422 6 лет назад
After looking up a component, in let's say the Vec, the borrow checker makes it impossible for you to accidentally keep or transfer the address of the component. This is automatically enforced at compile time so there's no run-time cost, and you don't need any explicit company policy on that matter. Without this check you might accidentally keep a pointer to a component that gets destroyed somewhere along the way.
@patrolin
@patrolin Год назад
but the Vec does nothing to prevent you from holding onto an old entityId, you still need another system (the generational index) to solve the actual problem...
@ayoubbelatrous9914
@ayoubbelatrous9914 Год назад
just make the checks debug only or release only then strip them away in distribution, or use a sanitizer the checking is not the problem here because we are dealing with runtime problems, and during the runtime there exist side effects from IO meaning the compile time checks will only take you so far as ensuring memory must be always valid and force you to provide a dummy substitute for a component. so in my opinion borrow checking for game engine ECS just makes your life harder for no reason. but still good for other stuff don't get me wrong.
@zxcaaq
@zxcaaq Год назад
this is an invalid solution because you can still hang on to it. you're solving the wrong problem.
@marcossidoruk8033
@marcossidoruk8033 Год назад
Did you watch the video? Getting strong vibes you didn't.
@taragnor
@taragnor 10 месяцев назад
@@patrolin Holding onto an old entityId isn't problematic as long as the entityId's are unique. Because ultimately you call your getEntitybyId function and it returns a failure when you try to use a bad entityId. Ideally for good programming practice you'd stop using that dead Id, but even if you didn't, at worst you're getting a wasted attempt to access a bad entity each frame, which is more of a non-critical optimization issue. The real problem comes in with non-unique ids, like if you try to do something like saying entityId = vector index, in which case when you put in something new into that index, you now could end up getting the wrong entity, because the entities can share the same id.
@teslainvestah5003
@teslainvestah5003 2 года назад
for the purpose of learning where your problems are in development, crashes are very good, but nothing is better than failing to compile.
@jebbi2570
@jebbi2570 2 года назад
Use unsafe and bam you can compile.
@teslainvestah5003
@teslainvestah5003 2 года назад
@@jebbi2570 and _then_ you have an antipattern so simple you can grep for it.
@lucasjames8281
@lucasjames8281 3 месяца назад
@@jebbi2570unsafe doesn’t turn off the borrow checker
@mybigbeak
@mybigbeak 6 лет назад
46:25 Make a component that acts as vec, but store (id,gen, ) enum and replaces get[id] with get[id,gen] and return an option. super safe easily doable generically so all "vecs" act consistently
@thanatosor
@thanatosor 2 года назад
Simple, easy solution
@Plasticcaz
@Plasticcaz 6 лет назад
I think you're right that the borrow checker doesn't help that much. I think the Option is what would actually help. In C, if an entity was deleted, you would return NULL when you passed an invalid EntityId to the Entity System. This is fine as long as every programmer knows that NULL is a possible return value, but if they don't, you could crash if you are lucky, or have a horrible bug if you aren't. Rust uses the Option type to "force" the programmer to consider the fact that something might be missing. Typically Rust tries to offload a lot of checking onto the type system, and I believe this, more so than the borrow checker is one of it's strengths. I understand this way of programming isn't for everyone, but that's the way Rust does it. I do like your philosophy on getting rid of friction, and the friction of Rust is definately a downside. It's mostly your talk of lowering friction in programming that has me interested in your language.
@buttonasas
@buttonasas 6 лет назад
Isn't that what the function description is for? I expect a comment on every function I use from libraries that includes input and output format cases/description. I guess forcing it does make it more mindless, which may be good or may be bad.
@buttonasas
@buttonasas 6 лет назад
I was specifically doubting whether it is good that the compiler forces you to do things that are correct. It's probably good, though.
@connormccluskey9103
@connormccluskey9103 6 лет назад
I think the borrow checker does ease you into a better idea though. Instead of storing direct `&T` to other components/entities, you have an id that you can use on a manager and get back an `Option` which then forces you to check whether the entity is still alive. If you tried to store `&T` then you would get lifetime errors because you can't prove that the other entity lives as long as the entity that is pointing to it.
@connormccluskey9103
@connormccluskey9103 6 лет назад
Watch the talk she did, she talks about generational indices. Basically you have an `(u32, u32)` where these are 2 unsigned integers, the first is the actual index, and the second is the "generation" the entity is. When you grab the component, it checks if that entity is still alive and not "raised" (a new entity allocated in that spot). If it wasn't alive it returns `None`/no component.
@InfiniteQuest86
@InfiniteQuest86 Год назад
Rust is really good for what it does. But it doesn't have to be used for everything like some people think.
@rusi6219
@rusi6219 3 месяца назад
Rust is a Trojan horse that will infect software development with the worst garbage
@FaZekiller-qe3uf
@FaZekiller-qe3uf Год назад
There's no uncertainty in the behavior of the program at runtime assuming no unexpected outside interference. This is because of Rust's safety model. No undefined behavior, I feel is important. The type system is also mostly enjoyable.
@tubebrocoli
@tubebrocoli 6 лет назад
Maybe the best solution in this case is some form of shared Option that records clearing history in debug? Because we have two problems to address here: we want the program to be robust and try to recover itself in production, but in development we want to catch the incoherence as early as possible and know what caused the incoherence in order to debug easily. Just knowing that the memory was cleared is not enough data to address the issue directly.
@MortenSkaaning
@MortenSkaaning 6 лет назад
at least one of the good thing of the generational index is that you can make it fatal to access old indices, so you would never operate on deleted/corrupted mem.
@spiveeforever7093
@spiveeforever7093 5 лет назад
One use case I have found for the borrow checker is equivalent to what is already possible in Jai: linting references lasting between frames/equivalent. Lifetimes happen to be a useful way of expressing "This reference must be destroyed before the end of this loop iteration" I also find this to be the major limitation of lifetimes however.... since most objects that require 'outlives' relationships, have lifetimes that don't map directly to the structure of your program. One thing that I would love to figure out in the realm of static analysis/type checking is a realistically useful way of enforcing "this entity must outlive this other entity", when those entities both live over a variable number of frames. Of course this can be enforced at runtime by simply making the parent entity destroy the child entity during its own destruction code, which will often be a useful (and opt-in!) way of detecting and resolving bugs, but there seems to be an open question in terms of useful static models that could make these kinds of patterns less bug prone. Ultimately modelling the arbitrary-ness of mutable state is just hard and is basically half of the reason programming is a paid profession, but it would be nice to come up with some kind of opt-in static analysis other than "pure functional programming only"
@elahn_i
@elahn_i 6 лет назад
No, this is not "turning off the borrow checker." It is memory safe and free from data races. However, the borrow checker does not prevent logic errors, which is what occurs if you follow a stale "reference" without checking it. She's saying the borrow checker pushes you in the right direction, not that it solves all your problems for you. One way to solve the problem is to use an accessor that checks the generation and make it the only way to access the component (edit: as you mentioned in the Q&A). Using traits and generics, you can leverage the type system to ensure correctness at compile time if that's important to you. Or if you want "fail fast" runtime crashes, simply panic!() when a code path should never execute. The borrow checker enforces lifetimes and prevents mutable aliasing in safe rust. Is it worth moving that checking from the programmer's brain to the compiler? It is to me. If you don't want its help, simply declare the function/block unsafe. Being able to grep for "unsafe" and audit those sections extra carefully is great. Edit: The "pain" caused by the borrow checker goes away after the first few weeks of programming in Rust. To me, it never feels painful, it feels like the borrow checker has got my back and saves me heaps of debugging time.
@davidste60
@davidste60 6 лет назад
Elahn Ientile It seems to me that she has created her own unchecked memory system inside a Vec. How has the borrow checker pushed her in the right direction? Hasn't she deliberately avoided it?
@elahn_i
@elahn_i 6 лет назад
In this case, the push was "don't use raw pointers" which led to using an index lookup scheme. Even if you don't check liveness, the memory is still allocated, initialised and the same type, so it's memory safe. The need to do the liveness check still exists in any index lookup scheme in any language. Also, it's only "unchecked" (for liveness, it's still checked for memory safety) if you index into the Vec manually, using an accessor function to enforce that check is easy enough.
@davidste60
@davidste60 6 лет назад
Good points.
@blenderpanzi
@blenderpanzi 6 лет назад
I see only one small advantage of the borrow checker in this case and it wasn't even mentioned: If you use such an index to "check out" an entity the "pointer" you get is a borrow and you're not able to store it anywhere. You can use it there in the scope you checked it out, but then you have to let it go.
@OMGclueless
@OMGclueless 4 года назад
Doesn't this also create a problem though? Because being a borrow *also* means you can't check out any *other* pointers from the ECS to use alongside which is a totally reasonable thing to want to do.
@blenderpanzi
@blenderpanzi 4 года назад
@@OMGclueless You can have multiple *immutable* borrows.
@OMGclueless
@OMGclueless 4 года назад
@@blenderpanzi Sure, but there are entirely reasonable things you might try to do that won't work. For example if you want to look up two physics components to read and update at the same time -- can't do that. Or suppose you want to call a mutable method on a component, and that method wants to be able to look up other components in the ECS -- can't do that.
@blenderpanzi
@blenderpanzi 4 года назад
@@OMGclueless You could find workarounds for that, but yes, it's a trade off. Depends what you want- Don't know what would turn out best for the given problem.
@OMGclueless
@OMGclueless 4 года назад
@@blenderpanzi The only real workaround is for the ECS to not return mutable references but instead Rc instances or something that you're expected not to store. i.e. Throwing out the borrow checker because it does more harm than good, and also adding reference counting overhead. Any API in Rust that returns mutable references by ID must necessarily have a catastrophic failure condition if you request the same ID twice even in unsafe code (or it must borrow the whole data structure when you get that reference so you can't even make the second request).
@austecon6818
@austecon6818 9 месяцев назад
Okay NOW I understand the power of the compilation time execution of arbitrary code to enforce that every programmer on a large team doesn't forget to obey certain project-specific rules... genius. Perfectly solves the problem without creating friction in every other aspect of the codebase...
@OMGclueless
@OMGclueless 4 года назад
There's actually a more fundamental problem with Rust's memory model than the borrow checker. Which is that the Rust compiler is allowed to assume that mutable references do not alias. So, for example, the following code *cannot* be allowed to compile: "p1 = entities.get(id1); p2 = entities.get(id2);" because what happens if id1 == id2? If you write only safe Rust the borrow checker won't even let you write this, because the first statement ties the lifetime of "p1" to "entities" and borrows "entities" exclusively for as long as "p1" is live. If you use the "unsafe" keyword you can return some kind of handle from "get()" that locks the id for as long as the handle lives, and the second call would then have to fail at runtime. What you can't do is use unsafe code to return a mutable entity with no extra accounting, because that is undefined behavior and the world will blow up -- this is "Rust doesn't allow data races" in practice; even if you don't write code that assumes p1 != p2 the compiler will.
@0xCAFEF00D
@0xCAFEF00D Год назад
That seems crazy to me. But I can't find any information on Rust to the contrary and the Rust book says: "At any given time, you can have either one mutable reference or any number of immutable references.". It seems _incredibly_ onerous to me. Say we've got a spell in our videogame that swaps HP between two entities. Do you actually have to do something like this? Pseudocode: a=get_mut_ref(arr, id_a) b=get_immut_ref(arr, id_b) // is this even allowed? I'd feel dishonest for assuming it's not even if the book seems to say exactly that tmp=a.hp a.hp=b.hp close a close b //close both references because of the "either". I presume closing references happen on scope closure by default as they're destroyed. But if there's no manual closing that would make this even harder b_mut=get_mut(arr, id_b) b_mut=tmp And this is just writing two values. I don't know how Rust programmers manage to program in this environment. And even more confusing to me: I couldn't write this as a function that takes the arguments to swap. I need to send the array and the two ids. Because if I'm passing in two mutable references that presumes I've already broken the rules. And if for whatever reason the entities are in different arrays I need to write another function that specializes for that case.
@OMGclueless
@OMGclueless Год назад
@@0xCAFEF00D Yes, if you want to swap the HP of two entities, the basic API of a hash map doesn't work. You have a few options: 1. Look up the entities multiple times and make a copy of the hp so that you don't hold both references at once. let hp1 = entities.get(id1).hp; let hp2 = entities.get(id2).hp; entities.get_mut(id1).hp = hp2; entities.get_mut(id2).hp = hp1; 2. Use an experimental API that does this specifically. let [e1, e2] = entities.get_many_mut([id1, id2]).unwrap(); std::mem::swap(e1.hp, e2.hp); If you're using a crate like specs for your entity storage that doesn't have the latter API, I think you're just stuck doing the inefficient option #1.
@sithys
@sithys 6 лет назад
I study and implement programming languages at work. My team owns an interpreter hosted in the cloud. For the question about scripting languages, I think your answer was a very good answer. I find that only other engineers can actually use a language that I create, and that the dream of giving a language to a non-programmer doesn't seem possible. Languages do provide a nice layer of abstraction that can hide many details from the user, though this is most important in situations where the underlying technology (like C++) is causing so much pain that the engineers think that a new language is worth the maintenance effort. I can see this would be a common occurrence in C++, though hopefully in Jai the language stands on it's own, and if programmers have problems with it, you just fix the language (if the fix is consistent with the stated philosophy of the language) instead of hiding problems in the language behind another language.
@chickenduckhappy
@chickenduckhappy 4 года назад
It's such a typical noob mistake to try to eliminate crashing bugs in a way that makes code keep running no matter what it does by hiding problems.
@chaquator
@chaquator Год назад
rust has the c++ same smart pointers under a different name (box) and ive seen plenty of rust code deal with the borrow check by regressing to that, basically back to the same messy OOP style as java and c++
@rafapiotrowski3542
@rafapiotrowski3542 Месяц назад
In my opinion its all about performance/speed! Storing thing in vec is cache friendly and thats the benefit.
@FaZekiller-qe3uf
@FaZekiller-qe3uf Год назад
Rust's std::rc::Weak returns an Option and is a weak pointer so it can be used as a reference to an Entity.
@0xCAFEF00D
@0xCAFEF00D 6 лет назад
It would be nice to have a link to the talk in the description.
@GaMatecal
@GaMatecal 6 лет назад
ru-vid.com/video/%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE-aKLntZcp27M.html
@Jokler13
@Jokler13 6 лет назад
ru-vid.com/video/%D0%B2%D0%B8%D0%B4%D0%B5%D0%BE-aKLntZcp27M.html
@GaMatecal
@GaMatecal 6 лет назад
4m too late :p
@jblow888
@jblow888 6 лет назад
Good point. Adding it.
@GaMatecal
@GaMatecal 6 лет назад
Good joke jon, good joke :p
@TomKaitchuck
@TomKaitchuck 6 лет назад
You are correct that the core issue that needs to be dealt with is that relationships need reflect the possibility that entities can go away. Unlike C++, the Rust compiler will reject code that doesn't deal with that issue. If you do deal with it, either in the way suggested in the original talk or either of the two alternatives you mention it will work in Rust. (Sometimes with a modicum of extra typing to explain it to the compiler.) I think the disconnect is you have enough experience to know not to write the code incorrectly. Having compiler that raises the issue forces me to think about it and come to the same conclusions as you even though I lack your domain knowledge. Just as strongly typed languages force you to think about types, Rust forces thinking about ownership and lifetimes, you can't forget that entities can go away. Once you think about this it is easy to design it correctly. You mentioned that using the map is a way to "turn off" the validation of lifetimes. This is true in a sense, but it also "turns off" type checking. Another way of phrasing the same thing is that indirection via an id allows you to generalize over both lifetime and entity type. The entity with the reference no longer needs to care about such things, but you must handle the case when acting on the reference. Of course it is possible to erroneously assert that the type is what you expect and still exists. However this fails in an obvious way. This is why the 'needing to use generations' is not a problem. A new developer would start with a string or a uuid for an entity id and use a map to store them. And later optimize by moving to an int and an Vector. When making the optimization, you would obviously add generations because the problem is clear. Forgetting to add generations is possible, and the borrow checker won't stop it, just as the type checker won't prevent you from asserting it is an incorrect type. But given that you've been forced to identify the issue and deal with it it's an unlikely mistake, and basic testing would quickly detect it. Rust did achieve it's goal: you thought about the problem more, and came up with good solutions.
@Ian-eb2io
@Ian-eb2io 2 года назад
I can say that no amount of experience makes the problem go away in complex C++ code. The Rust compiler seems to pick up a lot of those cases in Rust code.
@Boopers
@Boopers 4 года назад
This is not how a weak pointer works though. There is no list of pointers that get nulled, which would be pretty much impossible to implement anyways. It's usually just two reference counts. One for the owners and one for the weak references. The object still remains in memory, but it is basically flagged dead once the count of owners reaches zero.
@user-dz6il2bx5p70
@user-dz6il2bx5p70 10 месяцев назад
let's hope in 2050 zig and jai will be production ready
@arma5166
@arma5166 7 месяцев назад
Odin is production ready
@Phantom-lr6cs
@Phantom-lr6cs 5 месяцев назад
@@arma5166 odin seems pretty but it has indentations so in systems programming i dont think they will go anywhere . its too stupid to have python-ish like systems langauge which is annoying like rust is . rust has moronic syntax meantime odin inherited stupidity from python i think . and nim lang also has indentaion's as i remember which is too moronic . syntax is good but indentaions are too annoying .
@flyingsquirrel3271
@flyingsquirrel3271 6 лет назад
33:10 those are Vecs of "Option" enums. To use one of them, you have to check if it contains a Value or not explicitly. The only way around that is to use the "unwrap" method - by using it you tell the compiler explicitly that you don't want to check it, although your program could panic. Edit: formatting
@ZeroZ30o
@ZeroZ30o 5 лет назад
@Jonathan Blow - I know you said it's not the subject of this video but I'm interesting in knowing your stance on entity component systems, since I haven't seen many arguments against them. If you've already answered on this I would be fine with just being pointed to whatever video you've done it in.
@seditt5146
@seditt5146 5 лет назад
One con in my opinion is that the code just feels like it loses structure. With OOP you can trace the structure of your code so easy it is second nature but with ECS I personally just find it more difficult. Yes you have your components and you can see what Entities consist of however attempting to navigate around and piece together a Single entity is a pain in the ass depending on how you implemented it. OOP on the other hand all your data for a single object is right there in front of you in a nice neat box. I am working on Bench marking the two methods as we speak so sorry I can not provide pros and cons there, just overall user friendliness is a bit lower.
@yagamiraito8621
@yagamiraito8621 2 года назад
wow this guy has a point. I watched that talk he mentioned earlier and although I had that 'meh' feeling about it, I didn't actually realize what the problem really was. But this is just brilliant explanation.
@ibrozdemir
@ibrozdemir 4 года назад
yes, at the beginning B was following A, after A dies, B (instead of crashing) will go to a random point on the map (if a new A' comes and assign this GameState vec list).. i agree; this is only getting rid of the crashing problem, but you can not prevent player saying "hey what the hell is B doing, where is he going, didnt A died"
@jepkofficial
@jepkofficial 5 лет назад
This is actually ALL (the borrow checker, her talk, Jon's points) a conversation about concurrent access to a data structure. That data structure can be memory itself or the Vec objects that she used to skirt around the borrow checker. In other words: the fundamental problem is the same as they are anywhere else: concurrent access management. You should not concurrently access something (memory, array entity, etc.) without having that access managed, else you could not be getting the behavior or data you are expecting.
@shavais33
@shavais33 4 года назад
re: smart pointers around 26:00 - I don't think the bug he describes here is what happens? My understanding of smart pointers is that they essentially implement reference counting? So anything that points at B is going to actually be pointing at a pointer to B that is coupled with a reference count, and the act of referencing B increments the count and the act of dereferencing B decrements the count. And this is done in a thread safe way. So I don't think this bug he's describing is what happens. Unless he's talking about some kind of smart pointer arrangement that I'm not familiar with, which could easily be the case. But I've been listening through JB's playlist on "A Programming Language for Games" along with some other JB talks, and in them I've encountered this general memory management topic a few times, and my current understanding of his actual objection to reference counting is that if you do reference counting you end up doing some form or another of garbage collection because of the need to deal with circular references, and GC in general has just been kind of a performance quagmire for games, every which way its been tried and it's been tried a lot of ways.
@Nate77HK
@Nate77HK Год назад
Wouldn't an obvious solution be to register a callback or something so that when person B 'goes away', all the other entities trying to interact with B receive an event and know to handle their now-invalid reference when they receive that event. That avoids the problem of checking every frame to see if person B has perished, and also means that the memory of B doesn't have to continue on for several frames after death.
@ip6289
@ip6289 Год назад
It's very expensive. Registering requires maintaining subscribers for every entity being followed. That's N*N memory complexity if everyone follows everyone. And if you have frequent object creation destruction it will also translate heavily into performance.
@kruel9669
@kruel9669 4 года назад
Man, you are a fountain of wisdom and I thank you for sharing these thoughts. I however disagree on the scripting language/blueprints part. You are absolutely correct in saying that you need to be a real programmer to use a scripting language so why not use a real programming language? But that is precisely why I think blueprint and visual scripting languages are even worse. They are one more step removed from a real programming language and really are nothing more than form over substance. You're still writing code only with visual boxes. It may look easier but it absolutely is not and as soon as you start tackling "20 lines of script"-level problems they turn into an intractable mess of label and wires... The only way these make sense to me is from a marketing standpoint.
@ardawanx
@ardawanx 5 лет назад
Can i know more about the language you’re creating ?
@MyAce8
@MyAce8 6 лет назад
the way I see it is that there are a few minor quality of life improvements the option type is boxed so you'll never accidentally use a "none" object. In order to unbox the object you need to do a pattern match which can be handled gracefully in a way that easier to debug. What it doesn't prevent is bad references to non "none" objects. Also because the borrow checker steers you away from idiotic decisions it's arguably easier to arrive at the correct implementation even if there is very little discrepancy in the end product.
@MyAce8
@MyAce8 6 лет назад
P.S. I'm not a rust programmer, but I just started watching the talk cited and it seems more of a defense of the borrow checker rather than the borrow checker makes these specific patterns easier. or in other words in most cases the borrow checker is great, and in situations where its not great you actually just have shitty programming patterns. While I can't vouch for how great writing code with a borrow checker is, I can say that if you can't write what you want with one then you probably don't understand the problem too well.
@taragnor
@taragnor 10 месяцев назад
The only possible bad outcome is that you give it a non-unique id and it returns an object you didn't want, but that's more a pitfall of reusing ids. Ideally you have some kind of system to ensure your ids are unique, so you can't get a repeat.
@NeutronNoir
@NeutronNoir 6 лет назад
Hmm in this case if the entity is "freed" the Option in Vecs would be None instead of Some(blah blah blah), so the script or update function or whatever looking for this entity would "see" that it's not there anymore. There would be no reuse of invalid data I think.
@OMGclueless
@OMGclueless 4 года назад
Once it's freed something else can come along and use that memory. So if I have a handle to it and expect to see Some(blah blah blah) but someone else has put Some(bloop bleep) there instead my program would then be incorrect (though still safe by Rust's semantics).
@soulstudiosmusic
@soulstudiosmusic 5 лет назад
There is another, simpler way to do this without the overhead of an entity manager - it may not work for all situations, but should for most. What you use is a packed array, ie. some sort of container which does not free memory blocks and has a free list of erased elements, whose memory space gets re-used upon subsequent insertions. You make sure the free list object is smaller than the original object, put the id of the original object at the end of the struct/whatever so that writing the free list data into the object space will not overwrite the id of the original object that was stored. Add to that id numbers, and you're good to go. Object A stores a pointer or index to Object B + it's id number. If Object B is erased, it's id gets set to 0 (unused). If it's memory space gets reused later on, it's id number will be something different. When object A goes to look for object B via pointer (or index), it checks that it has the same id number that it has stored. If object B is still alive the id number will be the same. If object B gets erased, the id number will be different, and Object A knows that Object B is erased instantly.
@rokker333
@rokker333 8 месяцев назад
I do not know Rust, but I have over 10 years day-to-day c++ experience in big systems, e.g. CAD. It is not nit picking what he is saying, it is true. The big help of shared pointers is that they hide the complexity in code writing and help sometimes making accidentally men management errors. But it is still necessary to understand the ownership and lifetime of an object. If the problem is inherently in the logic then reference counting does also not help but instead hides the bad effect and makes it more difficult to track down. Referenced counted pointers should definitely be used but it is still necessary to know what happens.
@carlsmith8593
@carlsmith8593 Год назад
Swift is looking to add an optional borrow checker.
@kim15742
@kim15742 4 года назад
Wow, that is a very interesting take on exactly the issues I am facing
@haroldhamburgler
@haroldhamburgler 2 года назад
32:50 I don't think you understand what the Option type in Rust does. The bug you are talking about where you write some piece of code that tries to dereference an entity/component without writing any logic what happens if that entity/component has been freed (effectively) would no longer compile at all. When you accept an option type from the return value of a function or by accessing a struct in rust, you must "consume" both the Some(...) and the None() values in order for the code to compile. This means that every piece of code would access the component/entity now must have some logic for what happens when it is freed.
@haroldhamburgler
@haroldhamburgler 2 года назад
I also don't think the idea here is that you will reassign the Vec indices to different version of the same entity after it is freed. I think the idea is that the index of a free entity permanently points to a None. This effectively cost a single enum (u8) type in memory for every entity you've had to free in your level. I also think the component vector are supposed to expand when new entities are spawned in.
@porky1118
@porky1118 Год назад
37:00 The borrow checker nudges you in the direction not to store pointers directly, since it's either unsafe or too complicated to manage. So especially if you're new to programming, or you're used GCd languages, where storing pointers directly is totally fine, this can teach you to do it in a different way, and also why you should do it in that way.
@roberthickman4092
@roberthickman4092 6 лет назад
Getting entities from a central database is really just an implementation of the single source of truth idea.
@haruruben
@haruruben 5 лет назад
I'm interested in learning Rust, seems promising. Maybe we just need to understand this borrow checker better, sounds like this speaker didn't really use it as it was intended
@StealerSlain
@StealerSlain 4 года назад
Have you started learning the language?
@haruruben
@haruruben 4 года назад
StealerSlain I started to for WASM but then I discovered AssemblyScript... sorry, Rust
@StealerSlain
@StealerSlain 4 года назад
@@haruruben 🥺 ⚙️ That was a quick reply tho
@MrAbrazildo
@MrAbrazildo Год назад
2:20, agree. 15:42, it would be better to do the opposite: when B dtor is activated, the entire app waits till a whole checking is made (through all containers), to match its address. Only after this upgrade B is destroyed. This way, nobody will be caught with invalid target. 37:45, really?! What? 1:00:35, what exactly do you mean by this "friction"?
@EvanTeran
@EvanTeran 5 лет назад
You say at about 38:00 that it's important to be aware of what other languages are doing so that you know the cost/benefit analysis of the features they offer. Allowing you to make well-informed decisions about what is or isn't good. I agree entirely. But at the same time, I find it deeply ironic that you consistently say things like along the lines of "I don't know what C++ feature is supposed to address this, but I'm sure it's bad". Why not apply the correct mentality of "know and understand what others are doing" to C++ then?
@jblow888
@jblow888 5 лет назад
C++ has a track record of doing everything badly. After some point you say, "I get it, every new thing you guys do has been terrible, the next N things after I stopped paying attention will all have been terrible too, not least because they have to interoperate with all the previous terribleness". If you have a limited amount of time and attention over the course of your life, should you spend it on seeking out and understanding the smart things being done by people who show competence, or the consistently awful things being done by a bureaucratic committee? If you don't agree that most of what's in C++ is terrible, then I would suggest it could be because you don't have enough exposure to other programming languages, or to what really needs to be done to solve problems. So maybe it's you who needs to apply the "correct mentality" of knowing and understanding what others are doing.
@jblow888
@jblow888 5 лет назад
Let me explain it a different way: Someone introduces you to a new band. You listen to their first album; it's kind of mediocre, but it's something to listen to, so okay. Later their second album comes out; it sucks. Then their third album sucks. Then their fourth album sucks. Everytime you listen to this band, you get annoyed and depressed, but nevertheless you give their fifth album a try. It sucks. Years later someone comes along and says "hey you should really be paying attention to this band's important work; their ninth album is really great, even better than the fourth album, and it really builds upon the foundations established in the third album." Are you going to listen to albums 6-9, or just conclude that this guy doesn't know what he is talking about (or, most charitably, that you have hopelessly incompatible tastes)?
@EvanTeran
@EvanTeran 5 лет назад
​@@jblow888 I appreciate the replies! I would have responded earlier but today I was traveling. (Sorry this became a quite long post): To address the questions about experience and exposure to languages... well I've developed professionally for just shy of 20 years using about a dozen languages over the years. I watch videos such as yours not because I agree with everything you say, but because I do what I can to keep up with the latest ideas in programming. I mention this just to point out that I'm not exactly a "newbie" programmer. So, with that out of the way, I'll address your thoughts. I agree that having to maintain backward compatibility is a challenge of C++. There are plenty of features that I would just chop if it were up to me to get rid of a few of the common pitfalls. But the design to "interoperate with previous terribleness" is not all downside. It means that people can take old, battle-tested code and still use it with more recent variants of the language. If we look at the python 2.x/3.x debacle, we can see how badly this can go. Where companies basically can't upgrade from versions of the language that are rapidly approaching end of life because porting to 3.x is just too large of a lift. I agree that bureaucracy is not great either. Design by committee often leaves everyone at least a little dissatisfied. But it also means that features don't tend to make it into the language until they have the corner cases have been thoroughly thought out. Of course, this is a double-edged sword, the language is slower to evolve but more stable. I do wish when extensions have been supported by the big-3 compile vendors for years, that these defacto standards would more rapidly become real standards I can rely on. Oh well. So yea, C++ has some warts. Here's my perspective. There is no language among the multitudes that I've used that offer anywhere near the amount of control in runtime performance that C and C++ do. It's not even close. C is nice and all, but C++ offers much better types of zero runtime cost abstractions. It isn't always perfect, sometimes I wish the syntax was different, but nothing is perfect. I would say that your suggestions of what a "modern C++ guy" would suggest to solve problems is a bit of a strawman. The prevailing wisdom in the C++ world is **not** to use shared_ptr everywhere! For non-owning pointers, the experts consistently say just use raw pointers. shared_ptr/unique_ptr are for expressing **ownership** and the situation described isn't that. So they are the wrong tool for that particular job, it certainly wouldn't pass any of my code reviews. Personally, I'd recommend numeric IDs for the same reasons you enumerated. That being said, I also often see in your coding videos you lamenting doing something in C++ one way and thinking to myself "if he used feature X, he'd get the same code-gen with much less code". I do like your band analogy, but I would say that there are a couple of flaws with it. 1. If an everyday person said that about a band, I wouldn't think twice about it, but if a musician said it about a band, I'd be a little suspect of it. 2. C++ hasn't had a TON of revisions. There was essentially a 13-year gap in innovation of the language. ('03 was VERY minor tweaks to the '98 standard). '11 is in a lot of ways a totally different language from '03. Most of the template craziness that took place in the in-between years were hacks around the fact that features didn't exist to support what people wanted to do. ALL of that being said. I'm not suggesting that you should suddenly love the language! I'm not even suggesting that you have to keep up with C++ if it's not your thing. That's fine by me. I do think that a lot of the problems you express experiencing with C++ would be addressed by using different techniques and features of the language, but that for each of us developers to decide on our own. Really, I was more pointing out that not really a fair statement to say "I don't know what they are doing" AND say "what they are doing is terrible" at the same time. I like that you are trying to innovate in language development and appreciate your opinions even when I strongly disagree! I would just hope for more fair criticism. C++ has plenty to be criticized for, as just about any language does...I just don't think the things you happen to choose are always things which are STILL problems when using newer methodologies and features.
@nephew_tom
@nephew_tom 5 лет назад
@@EvanTeran C++ is nowadays simply madness. Don't want to waste more time with it. Basically.
@EvanTeran
@EvanTeran 5 лет назад
@@nephew_tom can you be more specific? I mean, is the concept of ranged for loops "madness"? Is compile time functions written as normal functions madness? Or is it lambdas? I'm just trying to figure out which part of the newer standards is so crazy? To be fair, there is also a lot of complexity in there too, but the vast majority of that is put in so that library writers can provide APIs which don't force the library user to pay costs for things they don't use. I dunno, that seems pretty sensible to me 🤷‍♂️
@jack-d2e6i
@jack-d2e6i 4 года назад
The borrow checker ensures you have no memory corruptions, and minimises the set of possible solutions to the fundamental problem. The solution given is, like Jon said, the proper solution used by many game programmers. I’d consider this enough of a win.
@HairyPixels
@HairyPixels 6 лет назад
I have little experience with game engines but I found that ref counted entities with a "dead" flag seemed to work ok. Instead of doing a look up in a table I just tested the flag and handled the condition in the same way you would with the entity index system. Maybe the system breaks down in larger systems but I didn't have any problems. I never tried using the entity index though so maybe it would be better. Thanks for sharing.
@buttonasas
@buttonasas 6 лет назад
I don't think he said it's particularly problematic but that it is somewhat more complicated and less performant. Though he did say that "A isn't authorized to keep B alive", which was not elaborated on; I don't understand that part. Remember to make sure that when you have a reference to B in entity A and delete A, it subtracts it from the ref count towards B.
@aescafarstad
@aescafarstad 6 лет назад
I think what he meant is that both A and B are equal in their rights and thus shouldn't control each others lifetimes. Technically they can but it would be unexpected, so it's a good idea to never let them do that..
@aescafarstad
@aescafarstad 6 лет назад
The problem with the "dead" flag is that it's easy to forget that you need to check it. Of course this is all very subjective but I think it's much harder to forget that entity index system can return null. Also the "symptoms" of mishandled "dead" flag are obscure and don't get noticed immediately. As opposed to the program crashing in case of dereferencing the null pointer.
@taragnor
@taragnor 10 месяцев назад
@@buttonasas Basically, you want a global list of all the entities in your game. And that global management system handles when an entity is destroyed or created. However if you instead do a reference counted pointer (or a garbage collector) that manages it, then you have instances where entity B may never get deleted as long as A exists and keeps referencing it. The real danger of course is if A references B and B references A, where in a simple reference counter, you now have a cyclic reference, and a memory leak.
@kitastro
@kitastro Год назад
she was only saying that its the most common way to do it
@0x1337feed
@0x1337feed 2 года назад
33:30 is the important part if u already know about the problematics and workings of game engines
@anon_y_mousse
@anon_y_mousse 8 месяцев назад
It's interesting that the more I see of Jai and Rust the more I see you went in a similar direction to them for some things. However, you opted for a significantly simpler syntax that works a lot better, even if I still don't like it over C++. While I do like some of the toolkit you came up with, I actually kind of feel like less should be included in the core language itself, even if it's something useful that most, if not all, would end up using. Since you're developing it to make games easier to write, you'll probably disagree with this stance, but if the language is less involved with regards to implementing a compiler then it'll be less likely to be relegated to the DSL category. Maybe you don't care, even if you were to agree, but I would want the language I'm putting out there into the world to last more than just a few years, or be used by more than just a few hundred people. It's hard to restrain oneself when adding to your standard library, because there's so much that I want it to be able to do out of the gate, but I'm trying real hard to do that in my own language's design.
@porky1118
@porky1118 Год назад
28:08 Yay, totally valid critique on Rust, even I agree to :)
@camthesaxman3387
@camthesaxman3387 2 года назад
The way I'd implement such an entity reference would be to use a 32-bit value where the lower 16 bits are an index into the entity array and the upper 16 bits are the entity's unique serial number. Dereferencing an entity is as trivial as indexing the array and verifying that the entity's serial number matches. When an entity is destroyed, you can set its serial number to something like 0 to mark it as dead.
@ishdx9374
@ishdx9374 3 года назад
Only problem with rust is horrible compile times and maybe crate dependencies
@peppeppeppepp
@peppeppeppepp 6 лет назад
The thing that he talking abound since around 30:00 for several minutes would be true if there is no generation in the entity id type. It is crucial there and it was not emphasized enough in the original talk.
@mbabuskov
@mbabuskov 3 года назад
I have been using the observer pattern to handle this. It's super simple. Don't store the pointer, store the observer object which holds the pointer. The observed object holds pointers to all the observers and notifies them (zeroes out the pointer in the observer object) before deletion.
@spaceowl5957
@spaceowl5957 2 года назад
I feel like this whole “safety” craze is just the new OOP. I’m 20 years people will say it’s a bad idea. It’s already a bad idea. I use Swift a lot and in an initializer it makes you - first init all instance variables - then init super - only then you can access functions and variables of your Instance This means I constantly have to write initializers where I need to first set EVERY instance property to some garbage, so I can then init super and then use properties on super for the real initialization. The ivars can’t be nil either unless they are optionals so you have to actually create and initialize some garbage object for each Ivar. It’s so dumb. It might be prevent some occult theoretical issues that no one’s ever dealt with irl but it’s not worth it because the cost to productivity is huge and it just makes for annoying code. At least in Swift I never felt like these safety features brought me anything but a headache. Optionals are bearable because the syntax is so minimal and streamlined but they are pretty unnecessary on whole as well. When I’m writing in another language I never have issues with some value being nil unexpectedly. And when I do it’s fixed I like 5 minutes. The safety checks that Swift does are either very high level and therefore not that useful or low level but badly implemented so they annoy you and you have to work around them. All these safety features try to keep you from making mistakes but any non-trivial mistake is also non-trivial to prevent. That’s how you end up with annoying “safe” languages.
@____uncompetative
@____uncompetative 2 года назад
"I feel like this whole “safety” craze is just the new OOP." Yes. I just checked _The Rust Programming Language_ and chapter 17 is about the Object Oriented Programming features of Rust. Would safety be needed were it not dragging this baggage along with it? slice("Hello, world", 5) => "Hello" I could imagine that a pure function could take a Literal String, S and an Integer, Z (both Constants) as its parameters and output the first Z characters of that String, S with the compiler optimising the code body and inlining the result at the invocation site, automatically allocating any resources it needed as well as freeing them afterward as the function body went out of scope, even if there were variables defined in it which happened to both refer to the same resource and the language did not copy the contents as both were Constants, as it ought to be able to manage to 'free' both without one being an alias of another then leading to a _double free_ error, as the user-facing semantic action of 'freeing variables in local scope' is independent of the system-facing optimisation undertaken by the language designers who bear all of the responsibility to not burden the user with quirky behaviour, and either optimise in a way that the user has no knowledge or care about (such as Copy-on-Write semantics), or not bother with the optimisation if the language designers lack the talent. You shouldn't have to put &r in front of a variable r because all should be references, and all functions should be pure, and all parameters to a function should be Constants, so if you actually want it to change an Array in place you should be using a procedure, not a pure function, or better yet, as you are then introducing state, break that state up by encapsulating it within modules that can only communicate amongst each other by unidirectional events that carry the information about who sent them, so your Array update is either local to that module (separating the procedure-call-and-return pattern into one that is a Command followed by a later Query of its own module's resources), or in another module which may be on another system, in which case it may make sense for the interrogating module to be mobile during its transaction with the foreign system, and pay for the computational resources it uses whilst an agent providing a service to the original system it will have to return to. Kinda like a leech drawing blood. In this I'm not mentioning Inheritance (Implementation Inheritance causes Fragile Base Class syndrome, and Structure Inheritance is an over elaborate way to use abstractions to specify interfaces of datatypes which ought to be built-in by the language designer and just used through HAS-A composition without an impulse for IS-A extension and overriding of methods, as most programmers aren't language designers). It is ridiculous that C++ forces programmers to #include because Strings are not built-in to the language by default, and then make it so that they can be extended to become custom Strings. Why? I don't need that. If Strings were built in, but didn't work the way I wanted to, I could make my own Document module with all the behaviour and representation I needed, then Clone that module and attach it to a name S for easy reference with: S := Document("imagine a string that _knows how to do italic text_ for example") This doesn't need a Class. It could be programmed blind, from scratch without code reuse. It would likely be better quality code because it was done from scratch. It would be more efficient as it wouldn't resolve some polymorphic late-bound calls through slow vtables. Why do I need this to quack like a duck? I don't. I am not making _Zoo Tycoon_ and if I was I don't know I would need an OOP to do it. So, I think opt-in Encapsulation through modules has some merit, but that is Agent Oriented Programming not Object Oriented Programming if they can move themselves over to a foreign system to do some transaction and later move back transformed in their state ("Yes, I managed to secure your PS5" / "No, sorry I kept trying but each allocation sold out as I moved up to the 7th place in the queue. So close. Transaction took 6hr") and all forms of Inheritance seem dubious, and people got by without them for years prior to OOP design so they can't be necessary for large scale software engineering (maybe, the real issue is that juniors are bad at collaborating with senior programmers and OOP offers managers a double reassurance of being an abstraction of the Business Model, allowing them to pretend that they are Systems Architects or they are Systems Analysts when they lack the experience in Management Consultancy to be either, and some sense that a broken up system of encapsulated units limits the damage one rogue programmer can do), when the truth is that they need to hire only experienced developers and then not disrupt their focus as they work or plan long narcissistic meetings about how they expect everyone to hit some scheduled project milestone, only to have to change it because of sick staff, unforeseen technical complications, or the Business Requirements changing mid-project. I think some parts of FP are okay, but Haskell takes things too far. All language paradigms seem to be in love with their idea and want to demonstrate its efficacy by having it be the only idea they use. This is sad as we could have a multiparadigm language that wasn't an agglomeration of dialects few programmers knew how to use in its totality, and therefore needed to read _The C++ Programming Language_ or the ISO Standard Documentation to remind themselves what some weird line noise did when maintaining some old code (that may be their own). We need to be able to get away with saying less and it inferring more, and indirectly controlling assumptions through optional defaults per function scope. Automatic.
@FaZekiller-qe3uf
@FaZekiller-qe3uf Год назад
50:00 You can Box::leak anything you want to live forever.
@bobweiram6321
@bobweiram6321 2 года назад
Are you related to Curtis Blow? He's one of the early rap stars who did the song "These are the breaks. Break it up, break it up, break it up..."
@espadrine
@espadrine 4 года назад
The Vec she uses is indeed a bit awkward, since it does not shrink. However, a HashMap would work great. In fact, it could be an implementation of the neat temporary_storage you dig into in another video: an array of types allocated temporarily, pointing to a HashMap of instances of the type. That way, we don't fallback to fragmenting the heap; it is fast to reset (O(T) with T the number of types); and you avoid table growth by setting the size above your known high watermark per type.
@shitheadjohnson2797
@shitheadjohnson2797 2 года назад
Have u got a better AI than GOAP - that looked really good to me! But I bet you have something in mind for that, (goal oriented ai.) cause then that could really use some kind of OO structure, if you wanted, but of course you can always just code everything procedural, makes no difference to me. When is a system complicated enough - to validate using OO? Never.
@buttonasas
@buttonasas 6 лет назад
Disclaimer: I haven't watched the original yet and have no idea what the borrow checker does and it just might be that I'm describing it right now (edit: nope, it's not related); I'm basing this only on the screens that were in this video. I think this approach to this problem is good _if_ you include this small bit: store a unique ID _inside_ every of those slots for components and when you have a reference to that, also store the unique ID; if they don't match, it means the object has been replaced with an another object, that's it (there's no need for an ID table, the ID is just something that's different for every object that ever existed, alive or dead or replaced or deallocated; however, this will absolutely run out of IDs eventually after a long time but such a case is detectable; also, don't shrink the Vec, please, thank you) Any input is welcome.
@egonelbre
@egonelbre 6 лет назад
I was just thinking of that. Some concurrent queues use similar approach by having a sequence number. Effectively use a version number that gets incremented every time a slot is reused. The EntityIndex becomes struct { uint32_t version; uint32_t index; } and slot becomes struct { uint32_t version; T value; }. Version number can double up as a marker for dead/alive -- e.g. even version number means it's dead.
@noxabellus
@noxabellus 6 лет назад
I mean you are introducing a level of indirection but its always going to the same region in memory so it should be an extremely cheap indirection.
@buttonasas
@buttonasas 6 лет назад
@@noxabellus I believe the overhead is minimal
@pm71241
@pm71241 2 года назад
I saw one of your older videos where you ranted against "big ideas" and mentioned (though not by name) 2 examples - one being pure functional principles and the other clearly being Rust's memory guarantees. I of course heavily disagreed with you that those are equal examples. No one should be against having the compiler check as much as possible if it can. And Rust have clearly shown that ownership and memory-safety can be checked at compile-time by including lifetimes in the type system. (and having the compiler insert de-allocation) statically. In essence... sure, crashes are not necessarily bad since they make you see the bug - but even better is failure to compile. I'm happy that you seem to have changed your mind and Rust now has "more of a reason to exist". So - sure... one way to solve this is to implement your own entity-allocator which ensures that any reference will not point to un-mapped memory. But why is that bad? First of all, they'll probably all be of the same size ... but secondly, translating an integer to a real pointer doesn't guarantee that the pointer you get back is valid for long (depending on whether there's any concurrency or not). So the way of doing it which she does seems just like a natural solution to the problem. You don't really give an alternative way of solving it which wouldn't include a manual allocator. She didn't "turn of" the borrow checker. She was just pushed by the borrow checker to make a safe implementation. That implementation happen to involve an allocator system for a lot of same-sized objects ... but wouldn't that be the solution in other languages too? (just without help from the borrow-checker that it was implemented correctly?)
@bobweiram6321
@bobweiram6321 2 года назад
Crashes are not a reliable way of detecting correctness.
@UGPepe
@UGPepe 6 лет назад
You're contradicting yourself about scripting languages: if all there is to them is good libs and you have to be a programmer anyway, then how can you say that it is Lua that which enables level designers to do crazy complicated things that they couldn't do otherwise, and how come it is so hard to translate that in C eventually? Why not just give level designers C with "good libs" and see what they can make out of that? :)
@drdca8263
@drdca8263 6 лет назад
UGP I thought that was what he was advocating? Like, one might expect that the level designers won't do complicated things with the scripting language, and so reason that it doesn't matter that it is a bit slow, but in reality, they will do complicated things regardless of whether you give them a scripting language or c with good libraries, so, because they will do complicated things, you should give them a real language so that it won't take too much processing time?
@UGPepe
@UGPepe 6 лет назад
@@drdca8263 but the reason they can do complicated things at all is because they have a scripting language with tons of built-in abstractions like dynarrays, hash maps, closures, iterators, coroutines, memory safety, interned strings, etc. These aren't features that you can just code in a C lib. Which is why translating from a complex Lua code to C is complicated, you'd have to reinvent all these abstractions in a very clunky way.
@marcel_pi
@marcel_pi 6 лет назад
Do you think using 16-byte strings as entity identifiers is a good idea? Or should I stick to simple integers?
@TetraluxOnPC
@TetraluxOnPC 6 лет назад
Comparing two strings involves comparing every character of the string which is typically a single byte at a time. Even if you hash the two strings and compare the results instead, this is much slower on modern machines compared to a simple integer comparison which is generally a single machine instruction.
@TetraluxOnPC
@TetraluxOnPC 6 лет назад
Also, comparing two bytes is a little slower than a word-sized integer comparison, but you're also doing at least one of these for every character of the string, for every string you're comparing, in every frame of your game. Strings are also usually encoded in some version of Unicode, which will be slower still because you have to decode the characters of the string before a comparison can happen. It might not matter for really small games, or if you have a really fast processor, but once you get any real number of entities or whatever, and/or do any kind of serious graphics, you'll have serious trouble keeping up an acceptable framerate. After all that, just using a integer index for comparison is just simpler, easier to understand, and easier to keep track of what is happening, and a whole lot faster. Plus you'll have better code morale too.
@marcel_pi
@marcel_pi 6 лет назад
Weeeeeeeell, you could think of it as a 128-bit integer too i guess :P But yeah I agree, i'll probably change that. Thank you :3
@stanislavblinov8454
@stanislavblinov8454 6 лет назад
To add to what Tetralux is saying, 16 bytes is two 64-bit integers. Even one of those would cover over 18 quintillion entities, so the question is, do you need two? ;)
@marcel_pi
@marcel_pi 6 лет назад
Maybe :P
@angeldude101
@angeldude101 6 лет назад
If A is following B, B dies and A doesn't handle it gracefully, one of 2 things will happen: a language enforced check will cause the program to crash, or it will keep going with undefined behavior that will be different every time it happens. I don't know about you, but deterministic crashes are WAY better than nondeterminism. The user will have bad things if they don't check? Why do you think the Option is there? It's so the programmer is FORCED to check.
@zeikjt
@zeikjt 6 лет назад
That's only one aspect of the problem. That's the "data that was at B is now cleared and replaced with something completely different" scenario. The other scenario Jon mentions is "data that was at B is now replaced by C, which has the same structure as B". Which means that any sanity checks will pass since the data is laid out in the exact same format. If the programmer doesn't know to do some kind of ID or other check then they'll think they're working with B but actually reading/writing C instead.
@angeldude101
@angeldude101 6 лет назад
That's what abstraction is for. You don't index the array directly; you don't have access to that information. You ask the API to give you the value and it checks the id. Jon seems to be making everything public so the programmer can mess anything up. When fields are private, library developers can vet what comes in and out.
@zeikjt
@zeikjt 6 лет назад
He first mentions this kind of pattern around 12:45 and how it was used in games he worked on. Then comes back to it around 47:14 and mentions how the code in the slide is likely saved from the actual bug because of it. I think the main point of Jon's video here is that he's saying that the presenter solved problems but didn't articulate the actual problem nor why the solution was solving the underlying bug. They were discussing the symptoms and not the cause.
@n0us3rn4m3s4v41l4bl3
@n0us3rn4m3s4v41l4bl3 Год назад
This wasn't a rant. It was a lecture on pointers and garbage collection.
@enhex
@enhex 3 года назад
12:00 weak pointer doesn't count towards the ref count
@hasen_judi
@hasen_judi 3 года назад
But probably needs to be cleared when the object it points to gets deleted, so it does incur book keeping overhead.
@GendoIkari
@GendoIkari 6 лет назад
I'm not very convinced of what you said about scripting languages. There are lots of things that increment productivity not related with just built-in libraries. The point is, I think, you have just less constrains overall. In python I can create a list with two characters "[ ]" and I don't have to worry about memory location, types, and so on. In c++ I have to specify the type, so it must be some kind of template, with rules, constrains and so on. This generates a lot of side effects that I have to deal with. Of course is a trade-off, nothing is free. If I'm crazy and I do crazy magic things with my code, I will pay in debug time. But overall, more freedom means faster prototyping possibilities. Unit tests usually are a good substitute for compile time static checks: you are still checking the validity of your code plus a lot of things. Overall I think the trade-off is positive. It's not just built-in libraries. Even because probably you couldn't create the same good built-in libraries with c++. Dealing with c++ libraries is inherently more complicated: memory issues, allocators, type and templates to follow... rules rules rules. I can create awesomely easy to use libraries in python because the language allow me to do that. Of course performance is a problem and this is way you have to avoid dynamic languages when performance is important, I agree with you on that.
@jalapenobomber
@jalapenobomber 6 лет назад
I think the main problem with your argument is that you are only comparing scripting to C++ not what a compiled programming language can be. For instance you could have compared python to C in which you would complain that C does not have a builtin dynamic array type and that you need to roll your own. This is a "problem" specific to C not compiled languages, much like the list example you give above. Your last comments about good built in libs in C++ is simply not true. Python is written in C. The dictionary, and list structures that give you so much freedom can be used directly from C/C++. I do agree that scripting can make for fast prototyping but I would be extremely hesitant to push a complex commercial project written solely in python. And while test should be written they are not a good substitute for compile time checks simply because it take too much effort to produce the level of coverage that compiled time check provide and take far too much time, in both development and the running of the extensive test.
@GendoIkari
@GendoIkari 6 лет назад
Of course Python is written in C and you can just use the C api and I dare you to use it instead of programming in Python ^_^ is it equally simple? Of course not. Using a list in Python and using the same list on the C level is a very different experience, I guarantee you. You don't have duck typing in C (and if you are going to mention void pointers I'll be very mad, I warn you xD). And in C++ it's even worse because your library should take into account not only how to deal with your types, but also code that uses const and code that doesn't do that, and OOP vs procedural, etc... Python is simpler and way more powerful. You can't easily attach methods to an object or a C struct at runtime in C and C++, for example (or Java, Rust, Go, etc...). And you shouldn't because the language is not made with these kind of liberties in mind. I agree with you that C is far more simpler than C++ or Java, but most of the issues are still there: memory checks, allocations, type checks... And this is not a bad thing, it's just different and... slower for the programmer. It's faster for the CPU, but slower to program, on average. About unit tests, they are a wonderful substitute, but I didn't say they are a perfect 1:1 replacement with the same features. What I'm saying is that in the trade-off, using two different languages with different problems, you can achieve the same level of stability without static checks. Plus you can still use tools for static checks, like pyflake, pylinter, and so on. You are comparing the features, but I was comparing end results. I see in these kind of arguments against dynamic languages the same incredulity, usually from people that never used dynamic languages professionally in big projects. "No compiler checks? It's IMPOSSIBLE! How can such thing go in production?? IMPOSSIBLE!!" xD Spoiler alert: thousands and thousands of big projects are shipped and are working perfectly using dynamic languages. It's just different. You just do different things to achieve the same results.
@UGPepe
@UGPepe 6 лет назад
Game programmers will always bash dynamic languages because they're too slow for games so they dismiss them for any app. What they often don't realize is how narrow and specifc type of problems are games, which are simulations, and how many more types of problems are out there which would be served better by different languages.
@jalapenobomber
@jalapenobomber 6 лет назад
Yeah using the C api for your standard C/C++ code would be much harder than using python, but I brought up more to say that you could have a compiled language where that stuff is built in, first case as it is in python. Same for duck typing. In Rust similar solutions can be constructed rather painlessly with Rust Traits. I think my main point is that many of the things you've brought, many of which I used to think myself when I started working seriously in python, could be handled in a compiled language if the tools were present. And maybe my stance is a bit weak because there is no major compiled language that tries to be that programmer friendly, but that's kinda the point of building a new language which Jon is currently doing. **Though I should note that I don't think he's trying to tackle that problem specifically.** For a bit of context I work in python daily in a data science capacity. And I've been doing this for the past 3 years and much of that time I've held the a very similar view point to your self. It wasn't until recently when I started getting more into systems level programming with C and Rust did I realize many of the pros of scripted languages ( memory check/allocation, dynamic typing) aren't that large, atleast for me. Memory management in python has been more a pain in the butt for me than any thing because I work with large data sets and I often need force the garbage collector to remove things and/or be very particular about the methods I call on a data set of avoid unnecessary copies. Even in less memory intensive situations I find compiled programming languages modeled to free when you leave scope just as effective and easy to use as python. The real question is, is it possible to have a compiled language that feels as free as python and not be as scary or rule intensive as C, C++ respectively. I think there can be. I think the JAI, Odin, and Go languages are proof that it can be done if that's what you want.
@UGPepe
@UGPepe 6 лет назад
That's more of a criticism of python than of scripting languages in general. LuaJIT for instance has all the C types including structs, arrays, unions and you can allocate them with malloc or make your own arenas, heaps, stacks etc. And your high-level code that uses them is JIT-complied to C-level speeds. The downside is that not all code can be JIT-compiled, so you need to learn a little about the compiler in order to avoid that.
@bitskit3476
@bitskit3476 2 года назад
@17:30 Another solution, which happens to be my favorite, is to use observer chains. Implementation-wise, you need 2 functions: subject.startObserving(object); subject.stopObserving(object); Each object has at least 3 pointers in it's data structure. The first pointer is to the last observer of "this" object. The second and third pointers form a pair; which point to "that" object and the previous observer of "that" object. When you call subject.startObserving(object), the subject becomes the new head of the observer chain. Basically, subject.thatObject = object; subject.previousObserverOfThat = object.lastObserverOfThis; object.lastObserverOfThis = subject; When you call subject.stopObserving(object); it just removes itself from the observer chain and sets subject.thatObject = NULL; If it was the last link in the chain, it sets object.lastObserverOfThis = NULL; When you call destroy(object); The object iterates over the observer chain and forces each observer to stop observing. Because of the way that this works, you never get dangling pointers and determining whether or not something has been destroyed is just a simple NULL pointer check. All of the computational cost happens at the moment that observation starts or stops, and size-wise, you need 1 pointer + 2 for every object observation "slot". E.g. if you want to want to observe 3 objects at once, you need 1+2(3)=7 pointers.
@nexovec
@nexovec 4 года назад
Why shouldn't an entity have a list of all of its subscribers, so that you can notify all the subscribing entities in the destructor that this object will no longer exist and to do something about that?
@nexovec
@nexovec 3 года назад
The answer is that that would induce a lot of memory overhead, because entities are really just a number
@mdsmatheus
@mdsmatheus 5 месяцев назад
Your talk about pointers and frames is probably the reason why the NPCS in Dragon's Dogma 2 is a resource hog mess and killing the NPCS makes the game run faster lmao
@clumsyjester459
@clumsyjester459 6 лет назад
18:39 Concerning the weak pointer: If you have references, that can get obsoleted by destruction of the referenced object (B), I deem it a good idea to have your programming language keep a backwards reference (from B to A). When B's destructor is called, you call a method in A that handles the invalid pointer. So each pointer would consist of a raw pointer and a handling method in A, that listens to B's destructor. What do you think of that idea? Or is that basically the same as what you said? Not an expert here. This should basically perform as good as a raw pointer alone, but it might have a significant performance drop when B gets destroyed and was referenced by many objects. Maybe you could work around that by not executing the invalid pointer handling method immediately, but instead just flagging it and executing it lazily before the next operation on A is necessary.
@XnecromungerX
@XnecromungerX 6 лет назад
Hey Jonathan, for what you were talking about at 10:50 am i right in thinking that optional nullable classes sort that all out? like from the perspective of unity in C# every second line is if (unit !== null) moveTo(Unit); EDIT: ah im so sorry, you mention this method at 19:15
@RDK0Urshack
@RDK0Urshack 6 лет назад
I think that you are misunderstanding how Unity deals with destruction of Objects. In Unity, a pointer to a Unity Object does not become null magically, but it will point to what they call a "fake null" object. This is very important is because the check if something is "alive" takes time. You should not have Update methods that check many Untiy Objects for validity all the time. blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
@RDK0Urshack
@RDK0Urshack 6 лет назад
Of course you have to check them "all the time" -- I'm trying to say that you should try to minimize how often you do it per frame.
@XnecromungerX
@XnecromungerX 6 лет назад
i apologise, i see and agree.
@yyny0
@yyny0 5 лет назад
29:50 Smartest thing i've ever heard anyone say about the Rust borrow checker.
@smallbluemachine
@smallbluemachine 5 лет назад
The only time I deallocate anything is when the program exits. Otherwise all entities are recycled or newly instanced to a hard design limit. Deallocating pointers dynamically is like juggling with napalm.
@warever9999
@warever9999 5 лет назад
@Jonathan, No offense but IMO discussing complex topic in monologue format isn't a very good idea. Besides, wring more code in Rust may help you to grok why borrow checker helps. In this example, at least, it cleanly separates reads and writes on GameState. Borrowing does not occur when component is referenced by ID but when this ID is being used to access referenced object. Also, you mentioned that you don't like ECS. Isn't it helping to avoid cache misses? Can you mention alternatives from your experience?
@davidboreham
@davidboreham 2 года назад
Interesting. I had watched the Rust talk a while back. I think it all boils down to "in a complex long-lived program you end up needing some sort of GC, for some of your data". Java et al get it wrong by forcing GC on _everything_, which eventually results in a bloated heap, GC pauses etc. Experienced C/C++ coders know that you're often going to have to roll some kind of your own GC for highly shared data. The point about the borrow checker I think is that there are sharing constraints that can't be enforced at compile time. You can do plenty at compile time, but not everything.
@timothyvandyke9511
@timothyvandyke9511 4 года назад
What is your new language?
@timothyvandyke9511
@timothyvandyke9511 4 года назад
ahh, I see it's Jai
@3rdGen-Media
@3rdGen-Media Год назад
I use Vulkan and I'm like wtf is the Vulkan Memory Model so I look it up and it has no relevance to what he's talking about
@thanatosor
@thanatosor 3 года назад
Summary : Story about Jonathan against the speaker of Rust example of how useful "Borrow-Checker" is. He wasn't very convinced by the way she used this technique to solve the problem of invalid pointers.
@stysner4580
@stysner4580 7 месяцев назад
Also the real take-away should lifetime of resources, not the validity of pointers...
@thanatosor
@thanatosor 7 месяцев назад
@@stysner4580 we should look into the big pictures to transform tools to serve us, instead of focusing into frictional details.
@charlesalexanderable
@charlesalexanderable 6 лет назад
12:28 why not use a weak pointer
@nickgennady
@nickgennady 2 года назад
I know this video is old but why does John not like ECS? I think it’s cleaner that OOP and performance benefits are amazing.
@WIImotionmasher
@WIImotionmasher 6 лет назад
This is the first time Ive listened to Jonathan Blow talk about programming and actually understood what he was talking about.
@RumataEstor
@RumataEstor 5 лет назад
I think Catherine's main point was that borrow checker right from the beginning showed all the problems of the OO design, which can be implemented in C++ without any complaint from the compiler but causing various problems down the road. It shows that using C++ developers were able to spot those problems by means of very careful thoughtful implementation/design or serious debugging afterwards. However with Rust's borrow checker these problems are obvious compilation errors, which allows to try some other designs with less efforts without affecting correctness. And yes, the ECS solution she proposed mostly works around borrow checker and replaces pointers with "manual index allocation in Vec", however the borrow checker would still prevent the consumers from keeping the references temporarily obtained from systems for later usage.
@secondson4536
@secondson4536 7 месяцев назад
4:13
@philipkristoff
@philipkristoff 3 года назад
I really like Rust, and I have been making small games in it in my spare time. Johnathan is right, what is presented does not solve the problem, it solves the crash symptom. The real problem will be the same no matter the language: pointers, gc, borrow checker or something new. Entities might die, and you have to handle that, in a safe and reliable way. But this critique, of the struct of vecs, does not disqualify Rust as a great language for game dev (or other things). You can still use Rust to solve the real problem, and Rusts borrow checker is going to help you in other ways (concurrency being one). One of the major advantages or rust is the memory safety, but a memory security flaw in a game is probably not nearly as critical as one in a database, web server or a operation system. So it comes down to personal preference. I personally like the strictness of the Rust compiler, but that is what I value, others might value the flexibility of a language like c or even python. You do you. To sum up : Jonathan says "this is not a silver bullet" and he is right, but that doesn't mean that Rust is a bad language or tool for the job.
@Ian-eb2io
@Ian-eb2io 2 года назад
But I think Rust is flexible. It's key difference is that the compiler enforces all the things that they've been trying to cram into C++ over the last 15 years. The main problem with C++ is that they've resisted moving towards a design where the compiler can check a lot of ways to fail for you. In both languages you have to always be thinking does this thing still exist, what happens when I let another thing use it and when will it stop existing? But the Rust compiler helps you by doing a lot of checks for you to make sure.
@georgeokello8620
@georgeokello8620 10 месяцев назад
@@Ian-eb2ioFlexibility and rust don’t go together especially when it’s sacrificed at the altar for memory safety. Rust is a potentially good language mainly for domains where other languages have tackled use cases where the language has its rules of how it addresses resource problems and rust can offer optimized way to solve that problem. Rust is going to greatly fail at the areas where the domain problem within the language is unknown and you have to experiment to solve that problem only to find out that rust just cuts off x percent of certain experiments to adhere to it’s memory safety rules.
@ChaotikmindSrc
@ChaotikmindSrc 3 года назад
"crashes are not as bad as you think", yes, i often try to design my code so that it clearly crash if something is wrong, saves me à lot of debugging time.
@ishdx9374
@ishdx9374 3 года назад
Crashes are good as long as you personally coded it (for example using unwrap)
@TheSulross
@TheSulross 2 года назад
reminds of the original MacOS of 1984 which tried to run in 128K of memory. The heap memory manager allowed memory compaction and so instead of a direct pointer to memory objects, one got a pointer to a pointer (a handle). This made possible for memory blocks to be moved for heap compaction and also made it possible to deal with when the memory object no longer existed in the heap. This was good for managing read-only resources which could always be reloaded from the executable file if had been flushed.
@rogo7330
@rogo7330 7 месяцев назад
Isn't this just like pointers work today in OS on CPU with MMU? Your pointer mapped to page, and page can be moved or even not exist until you really need it?
@MrD0r1an
@MrD0r1an 4 года назад
The main problem the borrow checker solves is memory safety (prevent buffer overflow, use after free, data races etc.) These types of bugs are the most dangerous because they often allow for arbitrary code execution (this has happened a lot in practice). This is especially important in the web, which is the use case rust was designed for. Of course rust will not save you from all bugs, but I think there is value in memory safety, which the borrow checker can guarantee. Unless you use unsafe, which is not always avoidable (e.g. when interfacing with hardware), but at least they limit the potential sources of memory issues.
@exapsy
@exapsy Год назад
Rust, was definitely NOT made for the web. Web = Async + Multithreading. Because it has clients, and clients = a pool of things you have to serve = a server = need as much parallel and asychronous code to serve everybody. Rust hasn't even figured out how to do ASYNC yet 10 years after. It literally has libraries to implement async runtime (tokio, async-std) and it even have had controversies around those libraries. The last time I tried to use TONIC + Rocket together asychronously I just had to encounter tons of async-multithreading issues and the code just never compiled because of async issues or when it did compile it had problems with a thread staying open so you had to kill the program twice etc. You spent time on meaningless things which means much more money spent when you try async+multithreading code, something you dont do in language such as Golang. Golang is made and designed for the web. Rust was designed to be a direct competitor to C++, more like a low level systems programming language rather than a web development language.
@genericuser1546
@genericuser1546 Год назад
@@exapsy Dude...go read up on who made rust, and why. Rust was absolutely made for the web, just not the part of the web you are thinking about :p
@exapsy
@exapsy Год назад
@@genericuser1546 When you say "Rust was made for the web" you mean "Web development". And web development requires a lot of sync and threads. And threads + sync is the ultimate weak point of Rust. It's absolutely ridiculous that Rust has reached a point where you have two -unreleased- runtime libraries to support a language feature that should have existed since its design. Asynchronous code and multithreading with async code. Two parts that are very crucial of the web.
@genericuser1546
@genericuser1546 Год назад
@@exapsy I agree on Rust + async. It so far has sucked anytime I touched it. And is the only part of rust where I encountered a foot gun. But I don't think it's a component that's "very crucial of the web." > threads + sync is the ultimate weak point of Rust I've never had a problem with Rust + threads. So I don't see this, care to elaborate?
@genericuser1546
@genericuser1546 Год назад
@@exapsy Ah, scrap everything I said above. We aren't on the same page lol. No when I said "Rust was made for the web" I was playing with the words (I obviously don't mean web development) :p Again just go read up on who created rust, and why. You'll get what I mean.
@keldwikchaldain9545
@keldwikchaldain9545 4 года назад
There's a lot of great points in this video that I really like, but I do have a gripe that there's some misunderstanding of what the word "safe" is intended to mean. "safe" in rust is a very specific and well-defined term that is exclusively intended to refer to rust without unsafe and the things which that prevents you from doing, namely mutable pointer aliasing and use-after-free. Rust code may be "safe" without being correct.
@Kruglord
@Kruglord 2 года назад
Yeah, good point. 'Safe' really just means 'not undefined behaviour,' it can still be wrong in a logical type of way. And to Jon's credit, he does make that point at around the 40:00 mark or so, when he essentially says "it's a Vec of data, so even if what's in there is invalid, at least it _was_ a correct form of that data" which is exactly the point. It might be 'wrong' but it's not 'undefined'
@bobweiram6321
@bobweiram6321 2 года назад
Ah hah! So the Rustafarians are a bunch of conniving scum! Why are you redefining "safe" instead of picking a term which better fits your definition, or just create a new one? It's so you can trick the unsuspecting victim into accepting Rust as safe under the common definition, but when the victim discovers Rust isn't safe, you reveal your true definition. You rustards need a garbage collector because your bullshit is piling up!
@schelney
@schelney Год назад
@@bobweiram6321 what do you think the common definition of safe is and how does rust's definition deviate
@doyouwantsli9680
@doyouwantsli9680 Год назад
All cult like programming movements do this. Change the meaning of very common English words, so 90% of people think you mean actual safety not specifically rust-safe. Same thing with "clean code".
@sirhenrystalwart8303
@sirhenrystalwart8303 6 месяцев назад
This is so tedious. Rust should have chosen a different word instead of coopting a word with an established meaning, which makes these discussions impossible.
@Be_Captain
@Be_Captain 5 лет назад
Hia, C++ guy here. Reference-counted smart pointers, like std::shared_ptr and it's weak-pointer counterpart std::weak_ptr, don't have to inform all the other pointers that point to the resource when it is destructed. Destruction simply decrements the reference count, which is just a uint that each smart pointer accesses atomically by reference (ie, thread-safe, and they don't have their own copy of the uint). So it's essentially a lazy-check (which we love, since we only pay for the operation when we actually need it): weak_ptrs won't keep resources alive, and in order to even dereference them you must lock them. If the lock fails, it means the resource no longer exists (ie, all the shared_ptrs that pointed to it have gone away, even if weak_ptrs still exist). The danger in all this then boils straight down to whether the programmer is checking for null after locking a weak_ptr, which is programming 101.
@highdownhybrid
@highdownhybrid 4 года назад
To shorten this a little, shared_ptr doesn't just count references, it also counts weak references. weak_ptrs keep a "shared_ptr control block" alive (beyond the live of the actual allocated object). This control block allows to test for an expire.
@OMGclueless
@OMGclueless 4 года назад
@@highdownhybrid If you're creating and destroying a bunch of entities and every time you create a reference to one of them you allocate and keep around a shared_ptr control block that outlives the object that's still a lot of scattered garbage taxing your memory allocator which is exactly what's under contention in most game engines (on the CPU side, there are other things bottlenecking the GPU). It's about the same amount of overhead in bytes and memory accesses as an entity system and leaves a bunch of dangling data structures that get lazily deallocated later instead of one block of memory in the entity manager.
@zemlidrakona2915
@zemlidrakona2915 4 года назад
@@OMGclueless The problem is the std::shared_ptr is generically crappy for most complex stuff. You can do the same thing by putting the reference count in a base class and you can include a weak count there too if you need it. Another advantage of this is that your smart pointer becomes normal sized again unlike std::shared_ptr which is double sized. The only down side is you may keep some unused memory around a bit longer but it will eventually get collected when all the weak pointers go.
@OMGclueless
@OMGclueless 4 года назад
@@zemlidrakona2915 How does the weak reference count help there? Is the idea that you can run the destructor/free other held resources when the strong count goes to zero and only reclaim the memory later when the weak count goes to zero? The main benefit of weak_ptr or entity references in an ECS is that you can reclaim the memory used by an object without waiting for everything that references it to be gone.
@zemlidrakona2915
@zemlidrakona2915 4 года назад
@@OMGclueless Yes basically. I would say the benefit of reclaiming what is often a small piece of memory a bit sooner rather than later is of dubious value. You aren't even reclaiming the whole thing as you still have the control block. Finally in a pointer heavy environment you will use more memory anyway since you will use two 64 bit pointers (on 64 bit architectures ) rather than a single pointer for every smart pointer plus any extra memory caused by the double allocation. Finally if they control block is far from the object you have the possibly of an extra cache miss. If you used make shared to put the block and the object together then you didn't need to double pointer anyway so it's a waste. Fortunately it's isn't too hard to write a better system in C++ yourself.
@FutureShock9
@FutureShock9 6 лет назад
Not used to following Jon all the way through the talk, but I think I pretty much got it this time.
@DeusExAstra
@DeusExAstra 4 года назад
On the subject of weak pointers, what you describe isnt how C++ weak pointers work. In fact, C++ weak_ptr solves this exact problem you're describing... which is why that's the preferred solution for game objects to point to other objects. C++ shared_ptr/weak_ptr work with a shared block of memory that they point to that contains 2 reference counts. One tells you how many strong references are (those that keep the object alive) and the other tells you how many weak references. You dont need any large lists of pointers to objects pointing at your target object, that's totally unneeded. It all works because each smart pointer just goes to the single control object to update/check the references in an atomic way. Also, there's no ambiguity about when to check for null... a weak_ptr must be locked before using it. The process of locking it gives you a shared_ptr, which you then check for null. If it's not null, it will remain valid until it's destroyed. The whole thing works really well and all you have to do is make sure you dont store shared_ptrs/unique_ptrs unless you own that object and want to keep it alive. Anyone who is just observing an object should store a weak_ptr.
@nickwilson3499
@nickwilson3499 2 года назад
The point of having a list of pointers to objects that have a reference to remove the reference from each object. The entity needs to be destroyed regardless of whether or not there are still strong references to the entity. I think you're solving the wrong problem
@holyblackcat7676
@holyblackcat7676 8 месяцев назад
The only problem with using pointers like this is that's it's hard to copy/clone the game state, while it would be easy with IDs. @nickwilson3499 All pointers between objects would be weak, the only strong pointer is in the entity list. When locking weak pointers, the resulting strong pointers are supposed to not outlive the function that uses them.
@_xeere
@_xeere 2 года назад
You can actually use special datatypes to re-enable the borrow checker on the IDs, you can turn them into an opaque type with a lifetime then it will be checked. It would also be impossible to store one of these references at that point so a bit useless, you have to disable the checking if you want multiple mutable references to anything.
Далее
ДЕНЬ УЧИТЕЛЯ В ШКОЛЕ
01:00
Просмотров 1,1 млн
+1000 Aura For This Save! 🥵
00:19
Просмотров 11 млн
Jonathan Blow on scripting languages
9:30
Просмотров 131 тыс.
Jonathan Blow on Simplicity
16:08
Просмотров 62 тыс.
Jonathan Blow on Refactoring
7:10
Просмотров 132 тыс.
The most important talk on programming by Jonathan Blow
22:55
Jonathan Blow on how an operating system should work
14:22
Jonathan Blow on unit testing and TDD
8:02
Просмотров 134 тыс.
Jonathan Blow on work-life balance and working hard
19:18
Ideas about a new programming language for games.
1:55:24
Rust for TypeScript devs : Borrow Checker
8:49
Просмотров 224 тыс.