genaray / Arch

A high-performance C# based Archetype & Chunks Entity Component System (ECS) with optional multithreading.
Apache License 2.0
953 stars 79 forks source link

Set and enforce a consistent code style #41

Closed just-ero closed 1 year ago

just-ero commented 1 year ago

Arch is currently using unconventional code styles in a lot of places. I would like to propose some more consistent styles, as well as some other things to potentially look at. Enforcing a consistent code style allows for a better overall coding and reading experience.

Major things this project needs to tackle:

Below, I've listed some conventions this project can enforce. These are up for discussion though. I'm happy to help implementing this.

The below can also be found here as an .editorconfig file.


Naming Conventions and Accessibility Modifiers

Fields should never be internal or public. Properties should never be private, protected, private protected, or protected internal.
Accessibility modifiers should be required at all times.
readonly should be added whenever possible.

var Preferences

.NET Conventions

Expressions

Pattern Matching and null Checking

New Line and Braces Preferences

andreakarasho commented 1 year ago

Fields should never be internal or public.

Except for structs!

genaray commented 1 year ago

Hey ero ! :)

Thanks for pointing this out, i could actually need every help i can get with this. I already updated the official readme yesterday to fix some misspellings, however theres still a lot more to it. I also havent looked at the code yet, probably my rider code settings are messed up. Thats the first think im gonna check.

Fields should never be internal or public

Thats really something i use quite frequently. I actually did that since in some cases fields are actually being faster compared to properties. Especially when the propertys get is not being marked inline. So the question here is actually what to prefer, however such cases are pretty rare.

  • Overuse of the MethodImpl attribute.

Understandable, how would you decide whether its necessary or not ? Unfortunately we can not test the performance effect on every single method. I think atleast query related methods should keep that since it lowers the cache miss in such cases. I already benchmarked this months ago and it was the case for such scenarios.

However the effect on other methods is still unknown, based on experience methods marked with inline always performed better which is kinda important for this lib. So probably we should just ignore that inline is being spammed everywhere, you can see similar decisions in other ECS libs like "Default.ECS".

  • Do not use var anywhere.

Why is that ? I can understand that it might make the code harder to understand at first, however its way more convenient to use. Probably we should focus on ensuring that the code is readable so that everyone automatically understands what type the var is e.g. : var entity = world.CreateEntity(...); instead of Entity entity = world.Create(...);. ( furthermore it makes refactoring easier )

I agree on the rest however and could need some help here. Thanks for the editorconfig, just saw that. Im gonna use it :)

EDIT : Most misspellings ( readme, wiki ) are fixed now, used grammarly. However, there may still be some funny-sounding sentences.

just-ero commented 1 year ago

Fields should never be internal or public

[...] in some cases fields are actually being faster compared to properties. Especially when the propertys get is not being marked inline.

The big issue here is really the ref modifier for struct fields. I'm unsure of why it's used as much as it is (I just genuinely lack the knowledge), but unfortunately there's not really a way around this one at all. Properties simply can't use the ref modifier even if they're auto-properties.

Properties' getters and setters can be marked inline just like any other method:

public int Foo
{
  [MethodImpl(MethodImplOptions.AggressiveInlining)] get;
  [MethodImpl(MethodImplOptions.AggressiveInlining)] set;
}
  • Overuse of the MethodImpl attribute.

Understandable, how would you decide whether its necessary or not?

This is all very difficult to test.
Generally, you want to be entirely sure that the method to be inlined is either very small (properties, indexers, constructors) or on a hot path. You want to verify that inlining generally generates smaller code.
To really make sure the attribute is necessary at all, you'd want to compare the generated JIT ASM before and after inlining and make sure from there that the newly generated code is worth it at all.

Slapping the attribute on every method you feel like can actually have negative effects. Think about it this way: if you inline a method which is on a path that's hit rarely, then the code generated is going to be bloated with unnecessary instructions that don't get executed.

  • Do not use var anywhere.

Why is that?

All of these were merely suggestions. I like being explicit personally. If you'd like to keep using var, then you can, of course. It's just important to be consistent: either use var everywhere, or nowhere (in my opinion).

genaray commented 1 year ago

The big issue here is really the ref modifier for struct fields. I'm unsure of why it's used as much as it is (I just genuinely lack the knowledge)

Well ref fields are great for various purposes, for example to reduce copies... or to return a ref which is kinda important for the ECS since its more efficient to write directly to a adress :

var references = entity.Get<Position, Velocity>();  // Returns public struct References{ ref Position t0, ref Velocity t1 }, one single operation...
references.t0.X += references.t1.X;
references.t0.Y += references.t1.Y;

alternative

ref var pos = ref entity.Get<Position>();       
ref var velocity = ref entity.Get<Velocity>();   

// 2 Operations to receive a ref to the structs we wanna modify... two lookups instead of 1 = bad for hotspots

It might violate several code conventions, but is extremely usefull and more performant since it basically batches operations ^^

Properties' getters and setters can be marked inline just like any other method: In this case we should probably use this more often, there is a need for action here :D

Generally, you want to be entirely sure that the method to be inlined is either very small (properties, indexers, constructors) or on a hot path.

Thanks, im gonna take a look at that... most methods are actually being used in hotspots (e.g. all Chunk and Archetype related methods, they will end up in the game loop for iterations and are mostly branchless). However probably other methods like Create, Destroy,... could be non inlined. I think i will test this with a benchmark in the next few days ^^

andreakarasho commented 1 year ago

Properties are just syntax sugar of: get_PropertyName() and set_PropertyName(value). I think Arch needs some code refactor, but I suggest to do not touch structs fields

genaray commented 1 year ago

but I suggest to do not touch structs fields

I support this, structure fields should not be touched or converted to properties. I also suggest that they stay public, this depends on the struct itself. However there a lot of usecases for properties inside classes. But theres a lot of other refactoring to do... im currently fixing more misspellings in the World.cs file.

just-ero commented 1 year ago

Do you have some branch you're using to work on this?

genaray commented 1 year ago

Do you have some branch you're using to work on this?

Yep, its still local however ^^ Unfortunately I'm not home today, but any other branch will also do the trick.

just-ero commented 1 year ago

I've been doing some work over on just-ero/Arch#experimental-code-style. Here's a summary of what I did:

bbdb94eaddffd44e3733245a8183a5d0d306cff0:

914697f1299ccb43f9c7ecbeadd754a71b653b6e:

188eabee0a296eaf7f56f9dbd4faa299590f246b:

genaray commented 1 year ago

I've been doing some work over on just-ero/Arch#experimental-code-style. Here's a summary of what I did:

Took a quick look at it, looks promising :) Thanks ! The next few days are quite busy for me, but i will merge it.

Just a small side note, SparseArray is intended to be non generic and I'm aware of this ^^ A lot of low-level stuff or ECS stuff is probably not valid with code convention but its actually intended and useful.

The SparseArray aswell as the Chunk are non generic since making them generic is not really possible. Well it is possible, but it would require reflection which is slow and a no go. So the easier and more efficient way is to use the current approach, its probably harder to understand, but requires no reflection, is fast and multi purpose oriented. Just a quick side note :D

Since this is mostly low-level and high-performance, we often need to decide between efficiency and code conventions... in this case we should always focus efficiency.

genaray commented 1 year ago

I've been doing some work over on just-ero/Arch#experimental-code-style. Here's a summary of what I did:

Sooo looks great :) Thanks ! I already pulled it into the main repo and recently pushed the documentation for the chunk. You may want to look at it, hopefully its better and more streamlined now. https://github.com/genaray/Arch/blob/feature/code-style/src/Arch/Core/Chunk.cs

I also moved the properties directly behind the constructor. Somehow they were not moved. Other files will follow soon.

genaray commented 1 year ago

Archetype now also received documentation... i also made some fields private and moved them above the constructor to make them follow the order :) https://github.com/genaray/Arch/blob/feature/code-style/src/Arch/Core/Archetype.cs

genaray commented 1 year ago

Documentation is finished and was merged with 7f2b07c :)

Now I'm gonna work on the notes and other mentioned code style issues.