Skip to content
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 #1233

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

zgabi
Copy link

@zgabi zgabi commented Jun 25, 2022

Proposed Changes

See issue #1231

Types of Changes

What types of changes does your code introduce to this project?

  • Bug fix (non-breaking change which fixes issue #NNNN)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause an observable behavior change in existing systems)
  • Documentation improvements (corrections, new content, etc)
  • Cosmetic change (whitespace, formatting, etc)

Checklist

  • I have read the CONTRIBUTING.md document
  • I have signed the CA (see https://cla.pivotal.io/sign/rabbitmq)
  • All tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)
  • Any dependent changes have been merged and published in related repositories

@bollhals
Copy link
Contributor

I'd prefer to split off at least the confirmTag into it's own PR. (as it contains a lot of special code to handle things, simpler to review track errors)

The other two are so similar that they can remain in this one.

As mentioned in the issue, I think we should look into also providing new API that take ROM for the exchangeName/RoutingKey

@zgabi
Copy link
Author

zgabi commented Jun 27, 2022

@bollhals This is just a draft PR, which was created for the request of @bollhals
Of cource once your team decides how to implement it, (if it helps to you) I could make another (separated) PRs.
The comsumerTag and other chages are already separated to 2 commits.

michaelklishin
michaelklishin previously approved these changes Aug 1, 2022
@michaelklishin
Copy link
Member

I do not have a strong opinion on whether this should be two separate PRs. However,

As mentioned in the issue, I think we should look into also providing new API that take ROM for the exchangeName/RoutingKey

would be nice and can be included into this PR or a separate one.

lukebakken
lukebakken previously approved these changes Sep 22, 2022
Copy link
Contributor

@lukebakken lukebakken left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a reasonable way to reduce memory use.

@Gsantomaggio
Copy link
Member

I improved similarly to the .NET stream client here, avoiding tons of allocations.

We need to test the PR deeply, but it makes sense to me.

The problem here is the breaking changes.

@zgabi
Copy link
Author

zgabi commented May 18, 2023

@Gsantomaggio We are using this PR in our application since almost a year ago without any problem... Today I only updated it to the latest code. (nothing else changed in this PR)

@lukebakken
Copy link
Contributor

@zgabi thank you! We appreciate you keeping this PR up-to-date. As you can see, we're making progress on version 7.0.

@zgabi
Copy link
Author

zgabi commented May 18, 2023

@lukebakken Could you please run the build again, there was a problem in the APIApproval file

@lukebakken
Copy link
Contributor

Well, it's going to take some time to resolve conflicts with the current state of main.

@zgabi - if you have time to start resolving conflicts, I'd appreciate it.

@bollhals
Copy link
Contributor

IMHO, having classes wrapping string / bytes is only useful if

@bollhals there is another benefit - I have had RabbitMQ support cases that boil down to the wrong string passed as the wrong parameter to a library call. Having dedicated types makes that kind of mistake much less likely.

Sure, that's what I ment by stating to make it much clearer what to pass.
But the tradeoff is the allocation of the objects and the indirection through another object.

It depends in the end what the goal is:

  • Increasing performance? => I have a feeling that these additional objects & indirection made it slower for most real world usecases. (Perf tests needed to confirm)
  • Increasing usability? => Fine, this surely helps, but then the PR Title should be changed, as the goal of not allocating string objects becomes a side thing.

@lukebakken
Copy link
Contributor

I'll run some performance tests. I don't think I have access to memory profiling tools to see how this PR affects allocations ... @zgabi ?

`AutorecoveringChannel` `CloseAsync` should just call `_innerChannel.CloseAsync`
@lukebakken
Copy link
Contributor

@zgabi @bollhals I have force-reset the stringallocations branch to 2320480, which is just before I went crazy with this PR. I saved my work on another branch.

@paulomorgado
Copy link
Contributor

  • Increasing performance? => I have a feeling that these additional objects & indirection made it slower for most real world usecases. (Perf tests needed to confirm)

Sometimes, minimal local extra work/complexity avoids extra processing, allocations, GC work. It really needs to be measured.

@lukebakken
Copy link
Contributor

lukebakken commented May 22, 2024

Here is what I see when comparing allocations between the stringallocations branch and the current main (as of 1600PDT 2024-05-22), using the MassPublish program to send 64 batches of 1024 messages per batch.

Running from the command line, each branch appears to take about the same amount of time. Memory usage appears to be about the same.

@zgabi @bollhals @paulomorgado thoughts? Right now I don't really see a reason to include this PR in version 7 and would happily move it to version 8 (where taking allocations into account will be a first-class priority).

stringallocations branch

image

main branch

image

@paulomorgado
Copy link
Contributor

paulomorgado commented May 23, 2024

@lukebakken, I haven't looked at the stringallocations branch in detail, but I have some generic considerations.

If the stringallocations branch has 98,479 strings and the main branch has 265,205 strings, where did those strings go?
What are those strings? Can those strings be pooled/cached?

One of the benefits of System.Text.Json is working with UTF-8 instead of UTF-16 (the encoding of System.String. System.Char is 2 bytes long). And to truly benefit from that, there are APIs working on byte alongside char.

I find very worrying the existence of System.ArrayEnumerator instances in the heap. This is an indication that they are being boxed. Coupling this with System.Linq.Enumerable (a known performance killer), I'd say LONQ to Objects is being used and shouldn't.

What is System.Collections.Generic.LinkedList<System.UInt64> being used for?

@lukebakken
Copy link
Contributor

Hi @paulomorgado

If the stringallocations branch has 98,479 strings and the main branch has 265,205 strings, where did those strings go? What are those strings? Can those strings be pooled/cached?

That's the point of this PR, to reduce string allocations when this library handles basic.deliver from the AMQP server. Exchange name and routing key used to be converted to strings, and this PR keeps them as ReadOnlyMemory<byte>

I find very worrying the existence of System.ArrayEnumerator instances in the heap. This is an indication that they are being boxed. Coupling this with System.Linq.Enumerable (a known performance killer), I'd say LONQ to Objects is being used and shouldn't.

That's easy to fix. Visual Studio "helpfully" suggested adding a .Cast<> in a couple foreach loops. I will open a new PR.

What is System.Collections.Generic.LinkedList<System.UInt64> being used for?

C:\Users\lbakken\development\rabbitmq\rabbitmq-dotnet-client_main [main ≡]
> git grep LinkedList
projects/RabbitMQ.Client/client/impl/ChannelBase.cs:63:        private readonly LinkedList<ulong> _pendingDeliveryTags = new LinkedList<ulong>();

lukebakken added a commit that referenced this pull request May 23, 2024
lukebakken added a commit that referenced this pull request May 23, 2024
@paulomorgado
Copy link
Contributor

Hi @lukebakken

If the stringallocations branch has 98,479 strings and the main branch has 265,205 strings, where did those strings go? What are those strings? Can those strings be pooled/cached?

That's the point of this PR, to reduce string allocations when this library handles basic.deliver from the AMQP server. Exchange name and routing key used to be converted to strings, and this PR keeps them as ReadOnlyMemory<byte>

As you pointed out, the only change is the reduced number of string instances and not overall memory or CPU.

So, what's the benefit here?

Is it the tests that are not taking advantage of this?

C:\Users\lbakken\development\rabbitmq\rabbitmq-dotnet-client_main [main ≡]
> git grep LinkedList
projects/RabbitMQ.Client/client/impl/ChannelBase.cs:63:        private readonly LinkedList<ulong> _pendingDeliveryTags = new LinkedList<ulong>();

So, it's kind of a queue, but you need to remove items from both ends and, sometimes, anywhere. Is that it?

lukebakken added a commit that referenced this pull request May 24, 2024
@lukebakken
Copy link
Contributor

@zgabi - if you have time, please read @paulomorgado's observations:

As you pointed out, the only change is the reduced number of string instances and not overall memory or CPU. So, what's the benefit here? Is it the tests that are not taking advantage of this?

I am running the MassPublish program using the ".NET Object Allocation Tracking" performance monitor, and, as @paulomorgado notes, there does not seem to be a reduction in memory use.

At this point I'm getting the feeling like I'm testing this incorrectly, perhaps?

@zgabi
Copy link
Author

zgabi commented May 28, 2024

Reducing the allocations reduces the GC overhead only.
You wrote that the running time and memory is almost the same... but is the CPU usage also the same? I think it should be bigger (because of multiple threads) when there are a lot of allocations.

@paulomorgado
Copy link
Contributor

I'm assuming the same test conditions, otherwise the tests are not comparable.

@lukebakken
Copy link
Contributor

Hi everyone -

In the interest of shipping v7 of this library this month, I am leaning towards moving this PR to version 8. I am going to release RC1 of version 7 by the end-of-day tomorrow, June 4th, 2024, so if someone has a very strong opinion about why this PR should be in version 7, comment here.

@lukebakken lukebakken added the next-gen-todo If a rewrite happens, address this issue. label Jun 3, 2024
@lukebakken lukebakken modified the milestones: 7.0.0, 8.0.0 Jun 3, 2024
@paulomorgado
Copy link
Contributor

I haven't had the time to look into it properly.

If this still allows the user to use string and can be improved in a 7.x version, I'd keep it in 7.0.0.

@lukebakken
Copy link
Contributor

If this still allows the user to use string and can be improved in a 7.x version, I'd keep it in 7.0.0.

There would still have to be work in the API to keep it consistent. Given the time constraint and what I've seen in testing it doesn't seem like the benefit is worth further delay of v7. I still have quite a few issues to get through in the milestone (https://github.com/rabbitmq/rabbitmq-dotnet-client/milestone/53)

@paulomorgado
Copy link
Contributor

paulomorgado commented Jun 3, 2024

Shiping is also a feature, right?

If there are doubts, it should be postponed.

If it is just additive for the API point of view, it doesn't need to be a new major version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
next-gen-todo If a rewrite happens, address this issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants