-
-
Notifications
You must be signed in to change notification settings - Fork 776
Speed/memory optimizations, better caching #897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Speed/memory optimizations, better caching #897
Conversation
|
Can't you use same approach with static field with Id for ResultFunc altogether? (Probably as Lazy) |
|
@Dreamescaper That was my original idea and I tried that multiple times, but I couldn't get it to work, some tests just kept failing. I thought these changes would be a nice improvement on their own anyway, even if just as a stopgap in case we manage to get that working 🤔 I'll give it another go just in case though 😄 To be honest, I've never been a big fan of the current infrastructure in the first place, from a performance standpoint. I mean, don't get me wrong, I can see how that makes things easier to manage and I'm not criticizing anyone's work, but if this whole lib were to be rebuilt from scratch, ideally I'd just want to see each method having its strongly typed implementation directly from there, without any reflection or dynamic delegate creation being necessary at all. I guess that'd be fairly more complicated to do, not to mention it'd be a breaking change at this point. |
|
Generally I like those improvements.
|
|
We will be wanting to support the C# 9 generators proposal and will need a pretty big rewrite to support that. I suspect that a lot of what we do at runtime could be generated code. We'll need help with all of that, I myself am not an expert in Roslyn enough to write a generator. |
|
Since this is looking at perf, any chance of getting a BenchmarkDotnet project wired up so we can measure before and after? It would be helpful to track improvements and regressions, along with identifying places that can be improved. |
|
@Dreamescaper About the As for the random ids creating diff changes in the stubs at every rebuild, yup I've noticed that too. I'll see if I can fix that by generating a deterministic id from the stubs project that's just based on the other properties of each method, instead of just using a new @clairernovotny I've heard about the "code generators" coming in C# 9 but I'm not familiar with them, and I can't find the related issue right now, would you have a link? Also, that sounds great! As for |
|
I would suggest to use struct similar to (That won't be needed, however, at all, if idea with static field for ResultFunc works) |
|
@Dreamescaper Fair enough, I'll try that! |
|
Here's a link to the current generators design: https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md As for which tests we should benchmark, I think we should have several... Ones that use Open Generics, ones that use concrete types, ones that return ApiRepsonse, ones that return strongly typed content, etc. There can be a few tests that are just about creation of the proxy -- RestService.For, and others that are about invoking it using the MockHttpHandler, so we can exercise the end-to-end. Basically, let's look at normal usage, where we expect normal usage.
|
|
@Dreamescaper I removed the collision risk in 42f4d2b, is this what you had in mind? While I was at it I also realized we could skip the iteration on the arrays as well, since we're using static arrays now. |
|
Closing this due to age and conflicts. I'd love to see any additional work done to improve perf and memory usage though! |
|
Sorry, have been a bit busy lately and kinda forgot about this PR 😅 In hindsight, the improvements in this PR were nice to have, but really just a band-aid. While I do love partial class GitHubAPIsIGitHubService : global::GitHub.APIs.IGitHubService
{
public global::System.Net.Http.HttpClient Client { get; }
readonly global::Refit.IRequestBuilder requestBuilder;
public GitHubAPIsIGitHubService(global::System.Net.Http.HttpClient client, global::Refit.IRequestBuilder requestBuilder)
{
Client = client;
this.requestBuilder = requestBuilder;
}
public global::System.Threading.Tasks.Task<global::GitHub.Models.User> GetUserAsync(string @username)
{
var ______arguments = new object[] { @username };
var ______func = requestBuilder.BuildRestResultFuncForMethod("GetUserAsync", new global::System.Type[] { typeof(string) } );
return (global::System.Threading.Tasks.Task<global::GitHub.Models.User>)______func(this.Client, ______arguments);
}
}This is not efficient at all, and likely a complete no-go in both AOT scenarios as well as when using trimming. Maybe it would help if I opened an issue to use as a starting point for future planning? |
Literally anything we do in Refit is nothing compared to the time it takes to actually make a network call and complete it. This is the equivalent of trying to speed up your International flight by brushing your teeth faster in the morning before you leave
Have we verified this? Afaik the whole reason we use source generation (this was pre Source Generators) was to be compatible with AOT. It would be Unfortunate if the move to official Source Generators broke this scenario |
|
The fact the generated code is not efficient was just a (small) part of the issue I've mentioned. The main issue is that the generated code is a no-go for AOT and for developers wanting to use trimming. This has nothing to do with the time spent there being negligible compared to the time it takes to make network calls. This is not about making the code faster (which would still be nice to have), but just something that could effectively block people.
I've tested this with .NET Native: stripping reflection metadata completely explodes at runtime, because the linker cannot statically analyze the whole dependency graph of the code refit generates, for instance. I'd expect pretty much the same to also happen on AOT and on CoreCLR with trimming enable. This has been a major focus lately, see eg.
Source generators only help AOT if the code they produce can then be statically analyzed. If you just generate code that still relies on reflection at runtime, then the fact you're generating that code using source generating is completely irrelevant.
No, refit has always been broken in this sense. I'm just saying this wasn't improved, but it'd be nice to do so 🙂 |
|
This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
What kind of change does this PR introduce?
This PR includes a few speed optimizations and memory usage reduction. It does so by employing a better caching system for generated rest methods, and by creating specific static fields/types to store reusable type info for the various generated methods, avoiding repeated allocations.
Current/past behavior comparison
As mentioned above, there are a number of improvements included in this PR:
Use Array.Empty() when possible
The current version of
refitends up allocating a lot of empty arrays in the generated services. This results in repeated allocations that can be skipped entirely by usingArray.Empty<T>()instead:Use static cached fields for reused argument lists
Right now
refitends up allocating a new array to represent the types of the parameters for each method being used. This array is passed to theIRequestBuildermethod to build the rest function, and results in repeated allocations for what is essentially the same exact array every time. This PR solves this issue by just using a dedicated field for each generated method, storing a reusable instance of that array:Use static cached fields for reused type parameters lists
Same concept as before, but for type parameters. In this case we can't just use a field, as the concrete type parameters are not known at compile time. In order to still use the previous caching system, in this case
refitwill now generate a dedicated generic type that will act as a container for the static fields storing the reusable arrays. This lets us use this technique even for generic methods:Better caching in default IRequestBuilder implementation
The current
RequestBuilderImplementationclass was doing some repeated work every time theIRequestBuilder.BuildRestResultFuncForMethodmethod was being called (specifically, quite a bit of reflection, and some unnecessary closures being allocated). This PR fixes that by using theHashCodetype to quickly calculate a method-specific hash code used as key for the cachedFunc<HttpClient, object[], object>instance to retrieve, and then includes a fast path to quickly get the cached instance if present and return it immediately.What might this PR break?
There should be no functional differences, and all the tests are passing.
Please check if the PR fulfills these requirements
Other information:
cc. @clairernovotny @Dreamescaper @martincostello