A week ago, I developed this source generator in some hours for a experiment I thought was interesting, it uses the new .NET feature called 'Interceptors' to intercept the 'prototype-style queries' and turn them into the more 'boilerplate and fast' version as a method.
As an example, it will take this:
public class TestSystem : ISystem
{
public static string SomeExternalSourceSearch(string searchFor)
=> searchFor + " found";
[Optimize]
public void Update(SystemProps props)
{
var (world, _) = props;
var query = new QueryDescription()
.WithAll<TestComponent, OtherComponent>();
world.Query(query, (Entity entity, ref TestComponent test, ref OtherComponent other) =>
{
string searched = Optimizer.OutOfScope(() => TestSystem.SomeExternalSourceSearch("test")); // For any reason
test.Value += 1;
});
}
public void Draw(SystemProps props, SpriteBatch spriteBatch) { }
}
public struct TestComponent
{
public float Value;
}
public struct OtherComponent;
(this ISystem is a part of a game I'm making with arch, it is a custom thing for my needs; the 'Optimize' attribute can be applied on any kind of function with as many queries as you would like)
And will turn into this:
public static class ProjectName_ComponentsUpdate_0_Interceptor
{
[InterceptsLocation(@"C:\(...hiding my directories...)\Components\TestSystem.cs", 26, 15)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Intercept(this World world, in QueryDescription description, Arch.Core.ForEachWithEntity<ProjectName.Components.TestComponent, ProjectName.Components.OtherComponent> _)
{
{
var searched=TestSystem.SomeExternalSourceSearch("test");
foreach(var chunk in world.Query(description).GetChunkIterator())
{
var arr = chunk.GetFirst<ProjectName.Components.TestComponent, ProjectName.Components.OtherComponent>();
foreach(var index in chunk)
{
ref var entity = ref chunk.Entity(index);
ref var test = ref Unsafe.Add(ref arr.t0, index);
ref var other = ref Unsafe.Add(ref arr.t1, index);
{
test.Value += 1;
} }
}
}
}
}
(it's not being generated formatted currently, but it works and kinda the generated code is the same for all instances, so it should not produce any bugs)
So basically, what this does is take the closure AST and turn it into an actual boilerplate-like query, using the closure body as a simple C# block in the generated output. It also replaces any Optimzer.OutOfScope call with an actual out of scope variable, inlining the closure expression as the variable initializer (and the actual variable declaration as the, well, variable declaration).
I made some benchmarks, and it appear to have similar (or better) performance than the source generated queries, and in relation to the closure prototype non-optimized version they have marginal gains, and substantial gains when accessing objects out of the closure (I suppose because of the inlining).
These where some of my tests for it:
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22621.3007/22H2/2022Update/SunValley2)
Intel Core i5-10600KF CPU 4.10GHz, 1 CPU, 12 logical and 6 physical cores
.NET SDK 8.0.101
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Method |
Mean |
Error |
StdDev |
Code Size |
DealDamageQuery |
12.141 ms |
0.0529 ms |
0.0494 ms |
7,815 B |
DealDamageQueryOptimized |
9.261 ms |
0.0355 ms |
0.0296 ms |
9,577 B |
DealDamageManuallyOptimized |
9.391 ms |
0.1259 ms |
0.1178 ms |
9,180 B |
DealDamageManuallyOptimizedSimpler |
10.483 ms |
0.0896 ms |
0.0794 ms |
8,957 B |
DealDamageQueryFromArchExtended |
10.946 ms |
0.0801 ms |
0.0749 ms |
9,415 B |
The 'Query' version is the normal prototype one, the optimized is the prototype one with code generation and my interceptors, the manually optimized one is the version I wrote manually in place, the simpler is not using unsafe and instead querying manually from the entity each iteration, and the last is your version. I think the benchmarks can be enhanced, but it sounds promising from my tests.
I still want to do some improvements here and there, and cover some edge cases (as well as adding compiler errors for non static queries and automatically infering the static method calls to the source type without needing to specify it manually, and add some testing as well), but I thought this would interest you and I'm curious if you would want to export this project if I publish it or is willing to put it inside the extended repository.