Earlier quoted context omitted.
> I wouldn't dismiss the JS ECS frameworks without measurement. I think the burden of proof is on the part of JS ECS frameworks to show they do have better performance by virtue of DoD and, if so, why. JS engine programmers have been optimizing object-oriented code for literally forty years, all the way back to when they were making Smalltalk VMs. If somehow a couple of folks hacking on ECS frameworks have managed to…
Okay, here you go https://github.com/thi-ng/umbrella/tree/master/packages/ecs Optimized Typescript ECS with a demo rendering 100,000 live 3D particles
An Introduction to Data Oriented Design with Rust
151–160 of 161 posts
Re: An Introduction to Data Oriented Design with Rust
#152Earlier quoted context omitted.
> the Rust game dev community is overly fixated on ECS. Not just Rust. The game dev community everywhere is infatuated with ECS. It's basically cargo culting. There is a large base of amateur or indie game developers who want to feel like they are doing game development the "right" way. One big aspect of that is performance. ECS has a reputation for efficiency (which is true, when used well in a context where your pe…
> ... so you see a lot of game devs slavishly applying it to their code in hopes that the "go as fast as a AAA game" Gods will land on their runway and deliver the goods. I watched this sentence unfold with bated breath, waiting to yell "and ze sticks the landing!", only to see it end with "goods" instead of "cargo". It's frustratingly close to perfect, though perhaps to end the paragraph with "cargo", you'd have to…
Re: An Introduction to Data Oriented Design with Rust
#153Earlier quoted context omitted.
Okay, here you go https://github.com/thi-ng/umbrella/tree/master/packages/ecs Optimized Typescript ECS with a demo rendering 100,000 live 3D particles
That one's pretty interesting. Here you can see they are putting real effort into thinking about the performance of the underlying VM. Using typed arrays is neat.
Super nice guy too, always willing to share information or explain stuff to you if he's around.
Re: An Introduction to Data Oriented Design with Rust
#154When i first started programming and before i had learned of structs, my intuition led me to organize data with arrays of fields. Turns out, i was on to something.
As they say, you can write FORTRAN in any language.
Re: An Introduction to Data Oriented Design with Rust
#155Earlier quoted context omitted.
Other approaches aren’t more complicated, and this is worse even if you never need to send books in batch because it tightly couples “book” to the Amazon AWS SDK client. Anything that interacts with books now has to take a dependency on the Amazon SDK. A much simpler, better design would be to just call client.buyBook(book) or client.buyBooks(books). But more importantly, my point is that you have your definition of…
Actually, the Amazon client could be a component that is managed by a sane dependency injection system (e.g. a factory of "books that can be bought on Amazon") and is used by a book to implement the abstract book operation "buy a copy of me". This would be basically equivalent to a "book buyer" object with a "try to buy this book" operation, with small advantages and disadvantages (either the books or the bookstores…
Moreover, inevitably someone downstream from the author of the Book class will have a use case for dealing with books that the author hasn't accounted for, so they have to extend the book class for their own use case, tacking ever more fields onto that book even though their use case only cares about a few of the fields.
Additionally, some of the things you might want to do with a book might also involve some other plain-old-data-structure--how do you determine which plain-old-data should host the method? Why is it `Book.doThingWithCar(Car c);` and not `Car.doThingWithBook(Book b);` or simply a static method `doThing(Car c, Book b);`?
Further still, why create a Book.buy() method that's just going to delegate to `Retailer.buyBook(this)` anyway? If the advantage is that you don't have to explicitly pass the `retailer` around, that's fine enough but there are ways to do that without baking retailer details into every book instance (e.g., a closure: `buyBook = function() { retailer.buyBook(book) }` or if you're a glutton for punishment, you can create a class weds a book and a retailer together:
class BuyBookOperation {
private Retailer _retailer;
private Book _book;
public BuyBookOperation(Retailer retailer, Book book) {
this._retailer = retailer;
this._book = book;
}
public do() { this._retailer.buyBook(this._book); }
}
Further, you might want to buy a book from many retailers--why should you have to do `book.setRetailer(amazonClient); book.buyBook(); book.setRetailer(barnesAndNobleClient); book.buyBook();`? Why not simply `amazonClient.buyBook(book); barnesAndNobleClient.buyBook(book);`? Even if you abstract away the mutation by creating a `Book.buyFromRetailer(Retailer r)` method that sets the retailer and then calls Book.buy(), you now have a potential race condition in parallel code and you still have no advantage over retailer.buyBook(book).Lastly, if at some point you do need to buy books in batch, how do you support that? Does each book class also need a reference to every List that references it so it can do book.buyInBatch()? Do you make a book.buyInBatch(List otherBooks) method? Do you just eat the performance hit and make individual calls to book.buy() even though the Retailer interface has a buyManyBooks(List books) method that could buy all books in a single HTTP request? What advantages do these approaches have over `retailer.buyManyBooks(books)`?
So this "plain old data needs to depend on everything that might be necessary for any activity involving a book" pattern has no obvious benefits, but it creates a lot of unnecessary coupling, creates a lot of awkward interface decisions when there are other objects involved in an action (including efficiently dealing with collections of your plain old data structure), and it probably pushes you into mutation unnecessarily which makes it a lot more difficult to parallelize your code.
This pattern seems to be a pretty transparent anti-pattern to me, and if it's "inherently OOP" then OOP is problematic.
Re: An Introduction to Data Oriented Design with Rust
#156Earlier quoted context omitted.
DOD has simple solutions for this, too. A popular one is to group your entities by "archetype"- you have two arrays of Point2Ds, one for those without a Z coordinate and one for those with a Z coordinate. Now if you want all Point2Ds, you loop over both arrays; if you want all Point3Ds, you just loop over one. (This generalizes cleanly to larger numbers of "components.") In some ways, this is actually quite a bit eas…
Excuse me? If it's called Point2D, it must have two coordinates. The type should be called Point and it should have an arbitrary number of coordinates (which seems a bit silly, since a Point2D and a Point3D are different types, as are points and vectors that have exactly the same representation)
Re: An Introduction to Data Oriented Design with Rust
#157Earlier quoted context omitted.
Most of what I see about ECS is how much easier it is to have dynamic behaviors without inheritance, and it is, so I don’t see why it would be bad for newcomers to use it or to have an ecs lib written in js.
> how much easier it is to have dynamic behaviors without inheritance I think you're getting at the idea that instead of having objects differ in their behavior by overriding methods, you have them differ by having fields bound to objects that implement different behavior. Assuming I understand you right, that's an excellent insight, but it's just the classic principle: Favor object composition over class inheritance…
Re: An Introduction to Data Oriented Design with Rust
#158Earlier quoted context omitted.
Excuse me? If it's called Point2D, it must have two coordinates. The type should be called Point and it should have an arbitrary number of coordinates (which seems a bit silly, since a Point2D and a Point3D are different types, as are points and vectors that have exactly the same representation)
I'm just running with the original example, contrived as it may be. Nobody here is actually talking about anything specific to points of any dimension.
Re: An Introduction to Data Oriented Design with Rust
#159Earlier quoted context omitted.
Is there any particular case in which you'd want Point and Point3Ds to be indexed interchangably in the same array? This is just as cumbersome in the array-of-structs case: some structs are larger than others in the same array, you need some signalling mechanism to know which structs are and aren't Point3Ds (lest you invite the wrath of your optimizer), etc. In Rust the AOS case would be an Enum of Point and Point3D,…
> This is just as cumbersome in the array-of-structs case: some structs are larger than others in the same array, you need some signalling mechanism to know which structs are and aren't Point3Ds Strong disagree. The traditional OOP approach is passing indirect pointers to everything and incurring an inefficient level of indirection. Ex: vector blah; blah.push_back(&somePoint2D); blah.push_back(&somePoint3D.xy); foo(b…
ISPC sounds pretty cool.
How much of a performance penalty is scatter/gather into and out of an SOA? I assumed you'd want to store all of your data-parallel fields in SOA format, rather than gathering into one every time you wanna do some maths-heavy operation.
Re: An Introduction to Data Oriented Design with Rust
#160Earlier quoted context omitted.
> This is just as cumbersome in the array-of-structs case: some structs are larger than others in the same array, you need some signalling mechanism to know which structs are and aren't Point3Ds Strong disagree. The traditional OOP approach is passing indirect pointers to everything and incurring an inefficient level of indirection. Ex: vector blah; blah.push_back(&somePoint2D); blah.push_back(&somePoint3D.xy); foo(b…
Right, but your example isn't an array of structs anymore. It's an array of pointers. You are correct that it's easier to take an array of structs and turn it into an array of pointers, or to take a single struct and get a pointer to one of it's members, whereas a struct of arrays would have to be scattered in order to get anything referenceable. ISPC sounds pretty cool. How much of a performance penalty is scatter/g…
But if you are doing any big o bigger than O(n) (ex: matrix multiplication, which is O(n^2.4) or so), then it's relatively cheap. Indeed, optimized matrix multiplications transpose the data to speed it up!!
There is something to be said about gather and scatter patterns as a methodology, especially as a first step into optimizing your algorithms. It's not the ultimate speed form, but relatively straightforward and simple to implement.