-
Notifications
You must be signed in to change notification settings - Fork 591
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
Do not create string objects from consumerTag, exchange and routingKey, or get them from a string cache. #1231
Comments
How much difference would this make to even a very basic (tutorial 1-2 style) consumer? |
I don't undertand your question. We have 1 consumer in this process. |
Feel free to submit a pull request implementing your suggestion. |
Which one do you prefer?
|
In the framework? dotnet/runtime#28368 |
No for some reason I thought string interning worked differently. I'd like to get feedback from the "usual gang", aka @bollhals, @bording, @danielmarbach and @stebet if they have time. I'm a bit surprised nobody has reported this issue in their use of this library. |
@zgabi my point is that caching is always harder than it sounds. What would be the gains (in terms of memory footprint and allocations) to a realistic consumer (application) like yours, or even a basic tutorial-style one. Previously all efficiency improvements in this client were driven by profiler results. Sure, |
Because they are just some small allocations. 2-3 small strings / message ("exchange" is empty string for me, which is not allocated) We are sending a lot of messages (~200/ sec), because it is a requirement for us to send everything between the server and the clients through RabbitMQ... even video streams. I reporteed this issue, since it is the top 1 in the memory profiler. |
Yes, caching is harder than simply passing spans to the user. For exmaple:
So probably this is slower than just simply creating a new string. So I'd prefer passing span (or memory) of byte to the user.... similar to the data. |
@zgabi we can do this for 7.0. Option 2 above is more in the spirit of RabbitMQ and clients but I can see how many might vote for option 1. |
I agree with @michaelklishin that this should be 7.0 only if there is an API change. |
Yeah the strings always were a bit of a problem, but we've focused on everything else so far.
Regarding the options Caching, I've actually 1.5 years ago started experimenting with option 3 back then (see commit here) and even saw gains in overall performance if the string I think was > 16 chars. But it's been a while, so one would had to check again. In principal the API is a complex topic and depends which usecase we want to support.
=> So for me the caching option looks like the best option if the API can somewhat be simple enough for the common use case. |
What about distinguish the consumerTag from the another 2 strings. As you wrote consumerTags are problematic, they are stored in dictionary and hashset. Multiple handlers receve it. I've investigated a little bit. This is measure from the current main branch: (Btw: you can see that everything other disappeared from the list which was in v 6.4 in my first comment) This measure is when I change the exchange and routingKey to Changes: zgabi@a6ac5c4 I tried to make similar time periods for the measure, but check the ratio between the number of the objects. In the latest main code there are almost the same number ofd strings as WPF EventKeys. In the modified code about half of them. I could change the ROM to another struct with an overridden ToString. It won't affect the memory allocations, and also not really changes the performance. So the user can get the string... but he has the opportunity to convert to |
I haven't looked in depth into the code base for a while... I was wondering if we have already looked at places where we can benefit from the low allocation APIs like |
@danielmarbach |
yeah I'm aware of that. My point was not about the specific problem but more about making sure where we use string and concatenation we do it efficiently before we go into complex string caching. |
As far as I saw there are no string concatenation issues in the code. After the connection is established 100% of the new strings in RabbitMQ client are cunsumerTags, excanges and routingKeys. The only string concatientaion in this library is in the ToString method of the ShutdownEventArgs.. which is (according to the method comment's) only for debugging: rabbitmq-dotnet-client/projects/RabbitMQ.Client/client/api/ShutdownEventArgs.cs Lines 97 to 108 in 6b1a06b
|
Created a test commit for removing the string allocation. Commit: Memory profiler result after both commits: So basically the consulerTag remains string everywhere, but (only) in the HandleBasicDeliver it is not allocated when consumer exists) |
FYI Ben Adams wrote once an intern pool https://github.com/benaadams/Ben.StringIntern. Yet it seems by the first look your approach is elegant and low effort. Need to look closer though a bit next week |
One thing you need to take care of if you store it in a dictionary or cache it, is that the backing storage is a rented array, hence it will be returned and potentially overwritten, which changes the content of your ROM I'll try to look into the options and provide a bit more context. Btw, we already have this CachedString class here, the intend when introducing it was cases like this. |
Yes, I create a new byte[] in the AddConsumer method. No rented array is used after it is returned to the pool. |
If I'm not mistaken then you return the array right after you call dispatch. Whereas for de body array we pass it on to the dispatcher. |
Oh, so you mean the excahnge and routingKey. Yes. I've created new commits, fixed some other things which was commented by @danielmarbach (See comments here: zgabi@4b67b23) New commits: |
You should make your commits on a separate branch and open a draft PR. It makes it much easier to see what you're doing that way. |
Ok, created a draft PR (#1233) |
I've had a look at it and I think it's very important to be consistent in the API. Meaning if we expose e.g. ExchangeName as ReadOnlyMemory, then every API that takes a Exchange name, should have the possibility to work with ReadOnlyMemory. Otherwise it's very confusing for the customer to get something different from what the user provided elsewhere. On the topic of ReadOnlyMemory vs caching:
|
Yes, all the methods which has routingKey / exchange should accept Instead of accepting (and providing in the Handle* methods) In this case it will be enough to have only 1 parameter combinataion for for example the BasicPublish method
And the users can call it with strings (implicit cast): And the users who are interested in the performance will "cache" the CustomString stuct. |
@zgabi in the protocol, the type used by those fields is called "short strings". It can be a decent type name to use in the context of this client. |
The default up until now was and still is the string case, so we should "try" not to make this case worse, but rather provide a even better alternative. (The CachedString overload was just recently added and can probably easily be modified if needed) Nesting structs in structs will have it's own challenges and inefficiencies, but if wanted it can be managed. But I'd hold off implicit conversions to this type in general, and from this type if they allocate. (E.g. No implicit converstion to string, if it means allocating a string. then I rather have an explicit ConvertToString or similar) |
I created a new commit which is using CachedStrings for every exhange and routingKey (sometimes the parameter name is "name" or "source", but it contains exchange or routingKey) This branch and commit does not contain the counsumerTag change as requested: All public method has both string and CachedString overloads. (Except the HandleBasicDeliver, which is a callback, it makes no sense to add and call both) Internal is only CachedString (when it is enough) Should I create a new PR? |
Any news on this issue? |
FTR, the PR was submitted as #1233. |
@zgabi as you can see, in #1233 I have brought your code in-line with Could you please provide precise instructions, including code if necessary, for me to see the benefit of this PR? Assume I have never done memory profiling (because it has been so long I don't remember the details). Thank you. |
@zgabi @bollhals @michaelklishin Hi everyone, now that I've done some work on #1233 and have gotten more familiar with the rationale behind this discussion and associated PR, I have the following to ask -
|
In our real application the most allocated objects are Strings. Most of them are allocated in RabbitMQ Client:
They usually contains the same value.
Please consider using
ReadOnlySpan<char>
orReadOnlySpan<byte>
with a pooled array in the background similar to the data.Alternatively get the strings from a cache table (maybe separate table for each "string type" (consumerTag, exchange...)), since most of them are the same.
By the way in the screenshot the 3rd, 4th, 5th and 6th allocations also belongs to RabbitMQ Client, but maybe some of them are already solved in the main branch. (I'm using the latest 6.x package)
The 2nd one is WFP, I have a PR for that.
The text was updated successfully, but these errors were encountered: