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

Replace String concat with StringBuilder in Cluster Nodes #994

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

Conversation

msft-paddy14
Copy link
Contributor

@msft-paddy14 msft-paddy14 commented Feb 4, 2025

StackExchange.Redis does periodic CLUSTER NODES and for large clusters a lot of allocations can happen with such calls.
We recently saw an issue where lots of allocation increased our .NET committed memory and also GC activity when new clients would be deployed/control plane would probe nodes where cluster size was ~100 nodes.

image

Using StringBuilder in an adhoc BDN test shows we can get significant improvements using this

| TestMultiStringConcat | None | 125.13 us | 1.894 us | 2.395 us | 93.5059 | 4.3945 | 1567640 B |
| TestMultiStringConcatBuilder | None | 41.59 us | 0.627 us | 0.586 us | 9.5215 | 0.9155 | 160152 B |

@msft-paddy14 msft-paddy14 requested a review from vazois February 4, 2025 18:56
@badrishc
Copy link
Contributor

badrishc commented Feb 4, 2025

Ideally, there would be no need to create strings at all, or use stringbuilder, if this is a very common operation. We could directly write to a session-local scratch buffer that is reusable for that session, and write to it a sequence of the usual style of writing:

while (!RespWriteUtils.TryWriteXxx(...))
   ...

Here is a rough (non-working) prototype of this for CLUSTER NODE: https://github.com/microsoft/garnet/compare/badrishc/cluster-node?expand=1

However, the StringBuilder approach is reasonable and simple to begin with, if you are seeing it is sufficient for your use case.

@msft-paddy14
Copy link
Contributor Author

Ideally, there would be no need to create strings at all, or use stringbuilder, if this is a very common operation. We could directly write to a session-local scratch buffer that is reusable for that session, and write to it a sequence of the usual style of writing:

while (!RespWriteUtils.TryWriteXxx(...))
   ...

Here is a rough (non-working) prototype of this for CLUSTER NODE: https://github.com/microsoft/garnet/compare/badrishc/cluster-node?expand=1

However, the StringBuilder approach is reasonable and simple to begin with, if you are seeing it is sufficient for your use case.

Your approach seems more performant. However, since it's relatively more code and needs few interface changes, maybe string builder is okay to start with for simplicity and more gains compared to main code. We can optimize further with scratchbuffer if needed?

@badrishc
Copy link
Contributor

badrishc commented Feb 5, 2025

Ideally, there would be no need to create strings at all, or use stringbuilder, if this is a very common operation. We could directly write to a session-local scratch buffer that is reusable for that session, and write to it a sequence of the usual style of writing:

while (!RespWriteUtils.TryWriteXxx(...))
   ...

Here is a rough (non-working) prototype of this for CLUSTER NODE: https://github.com/microsoft/garnet/compare/badrishc/cluster-node?expand=1
However, the StringBuilder approach is reasonable and simple to begin with, if you are seeing it is sufficient for your use case.

Your approach seems more performant. However, since it's relatively more code and needs few interface changes, maybe string builder is okay to start with for simplicity and more gains compared to main code. We can optimize further with scratchbuffer if needed?

Yeah this is at least better than main.

return nodeInfoStringBuilder
.Append(workers[workerId].Nodeid).Append(" ")
.Append(workers[workerId].Address).Append(":").Append(workers[workerId].Port)
.Append("@").Append(workers[workerId].Port + 10000).Append(",").Append(workers[workerId].hostname).Append(" ")
Copy link
Contributor

Choose a reason for hiding this comment

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

use .Append(char c) for all single characters such as ' ', ':', etc.

.Append(info.pong).Append(" ")
.Append(workers[workerId].ConfigEpoch).Append(" ")
.Append(info.connected || workerId == 1 ? "connected" : "disconnected")
.Append(GetSlotRange(workerId))
Copy link
Contributor

@badrishc badrishc Feb 5, 2025

Choose a reason for hiding this comment

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

Pass the current StringBuilder to GetSlotRange and GetSpecialStates so we are not creating new string builders or intermediate strings.

Copy link
Contributor

Choose a reason for hiding this comment

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

i.e. we can call GetSlotRange(workerId, nodeInfoStringBuilder) here.

for (ushort i = 1; i <= NumWorkers; i++)
{
var info = default(ConnectionInfo);
_ = clusterProvider?.clusterManager?.GetConnectionInfo(workers[i].Nodeid, out info);
nodes += GetNodeInfo(i, info);
Copy link
Contributor

@badrishc badrishc Feb 5, 2025

Choose a reason for hiding this comment

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

Send this StringBuilder to private void GetNodeInfo(ushort workerId, ConnectionInfo info, StringBuilder sb) so it can append to it.

And implement the public string GetNodeInfo(ushort workerId, ConnectionInfo info) using the common method, as something like:

var sb = new StringBuilder()
GetNodeInfo(..., sb);
return sb.ToString();

@badrishc badrishc self-assigned this Feb 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants