Reduce string allocations#1639
Conversation
|
This PR introduces some readbility issues in places. Replacing case statements with large if/else blocks is not really desireable. In other cases if the code is going to be changed it should go further and make use of new built in methods. For example, changing: to: would be better as: |
At the moment, C#/.NET is not able to use I'll try to come up with something that makes this more concise.
OOPS! Missed those ones. I'll do another pass. |
|
I reverted back to I have a little devil screaming in the back of my mind to create |
| { | ||
| // Attempt to execute the current command. | ||
| switch (command.ToLower()) | ||
| switch (command) |
There was a problem hiding this comment.
The case statement could be kept clean and readable by just changing the switch to: switch(command.ToLowerInvariant())
| if (args?.Length > 0) | ||
| { | ||
| switch(args[0].ToLower()) | ||
| if (string.Equals(args[0], FFMPEG_VP8_CODEC, StringComparison.OrdinalIgnoreCase) || |
There was a problem hiding this comment.
The original swtich could be left in place, and still get the benefit of the suggestion, by doing:
switch (args[0].ToLowerInvariant())
{
case FFMPEG_VP8_CODEC:
case FFMPEG_VP9_CODEC:
...
There was a problem hiding this comment.
args[0].ToLowerInvariant() this may unnecessarily allocate a string,
| _logger.LogInformation("ICE Candidate: " + message); | ||
|
|
||
| if (string.IsNullOrWhiteSpace(message) || message.Trim().ToLower() == SDP.END_ICE_CANDIDATES_ATTRIBUTE) | ||
| if (string.IsNullOrWhiteSpace(message) || message.AsSpan().Trim().Equals(SDP.END_ICE_CANDIDATES_ATTRIBUTE, StringComparison.OrdinalIgnoreCase)) |
There was a problem hiding this comment.
I think an extension method for this is justified (as per your main comment) given the frequency of use and the fact it's a bit of a mouthful.
There was a problem hiding this comment.
I tried to optimize strings as much as possible in this PR.
I'm sure more pattern will be uncovered.
I'd like propose other optimizations but, as you mention, these can become large PRs
| { | ||
| string trimmedExtension = extension.Trim().ToLower(); | ||
| switch (trimmedExtension) | ||
| var trimmedExtension = extension.Trim(); |
There was a problem hiding this comment.
The case could be kept here as well by using the ToLowerInvariant() in the original swtitch.
|
I agree with the improvements in this PR but it's big and touches some very fundamental classes. Can it be split up even further. The string formatting on all the demo apps and tests are low risk and could be put in a separate PR. The changes to the core SIP classes are the biggest risk and they should be separated into their own PR. |
|
I'm creating a PR for the changes under What is your plan after that? |
Do you mean as far as merging? I don't see any issues merging the changes in the test and examples directories. If one of the PR's was solely for string formatting and spacing that would aslo be straight forward to verify and merge. The bigger changes, particulatly in the |
|
Use ordinal comparisons and span-based parsing to avoid temporary string generation across SIP, RTSP, SDP, RTP, and WebRTC paths. Replace allocation-heavy Trim, Substring, Split, regex, and string concatenation patterns where span slicing, destination-span split buffers, or StringBuilder preserve behavior with fewer intermediate strings. Convert staged string.Format and fixed concatenation sites to interpolation, and replace loop-based string concatenation with StringBuilder where strings are built incrementally. Avoid temporary string creation in SIP custom header prefix checks. Change SIPHeader and RTSPHeader ToString implementations to override object.ToString() so interpolating header instances preserves protocol serialization behavior. Replace span-trim length checks with string.IsNullOrWhiteSpace where only null/blank detection is needed. Keep SDP media status parsing close to the original switch structure by using guarded cases with ordinal ignore-case comparisons. Use discard patterns for guarded switch cases where the matched value is not used. Replace the long SIP/SDP test fixture with a raw string literal and normalize it with ReplaceLineEndings(CRLF) so the protocol payload stays stable across LF and CRLF checkouts. Set the unit test project to C# 14 and import Polyfills so the call also compiles for net462. Convert long SIP and SDP test payloads from CRLF-heavy interpolated strings to raw string literals normalized with ReplaceLineEndings. Share the repeated integration INVITE payload through a helper and update test projects to C# 14 so raw strings compile across target frameworks.
eecb312 to
00cc653
Compare
Summary
This PR reduces temporary string generation across SIP, RTSP, SDP, RTP, WebRTC, and related example/test code paths. The changes replace allocation-heavy parsing and comparison patterns with ordinal string comparisons,
ReadOnlySpan<char>slicing, destination-span split buffers, and more efficient serialization patterns.The intent is to preserve existing behavior while reducing avoidable allocations in protocol parsing and message formatting hot paths.
Part of: #1434
Why
Protocol parsing in this library runs frequently and often on network-facing hot paths. Several existing patterns created unnecessary intermediate strings or arrays while parsing or comparing protocol text:
ToUpper()/ToLower()before comparisonTrim()andSubstring()for short-lived parser slicesstring.Split(...)where only a few fields are needed+=string concatenation while serializing headers and SDP blocksThese patterns increase GC pressure and make hot-path parsing more expensive than necessary. They can also obscure the intended comparison semantics, especially for protocol tokens where ordinal comparisons are the correct behavior.
What changed
String comparisons
Replaced case-normalizing comparisons such as:
value.ToUpper() == ...value.ToLower() == ...value.ToUpperInvariant().StartsWith(...)value.ToLowerInvariant().Contains(...)with explicit ordinal comparisons, primarily:
StringComparison.OrdinalStringComparison.OrdinalIgnoreCaseThis avoids temporary strings and makes protocol comparison semantics explicit.
Span-based parsing
Reworked many parser paths to operate on
ReadOnlySpan<char>instead of repeatedly materializing strings.This includes parsing in:
The parser logic now generally slices spans and only converts back to
stringat API/storage boundaries where a string is actually required.Split allocation reduction
Replaced several
string.Split(...)usages with span-based splitting.For fixed-shape parsing, the code now uses destination-span split buffers such as:
This preserves the original “exactly N parts” behavior without allocating a string array or intermediate string segments.
Where appropriate, split options are used to preserve intended trimming/removal behavior without extra
Trim()calls, such as SDP line splitting with remove-empty and trim semantics.Trim and substring reduction
Replaced parser-local
Trim()andSubstring()usage with span slicing and span trimming.This avoids creating temporary strings for values that are only needed for comparison, branching, or short-lived parsing decisions.
Strings are still materialized when they are stored in public object properties, dictionaries, collections, or returned through existing APIs.
Regex removal in fixed-format hot paths
Removed regex usage from several parser paths where the format is fixed and can be parsed directly.
Notable examples include:
These paths now use direct span scanning/slicing instead of regex match objects and replacement strings.
Serialization allocation reduction
Reworked repeated string concatenation in message serialization paths.
Changes include:
StringBuilderfor repeated line/block construction+concatenation where interpolation is clearer and lets the compiler choose the appropriate loweringThis primarily affects SIP, RTSP, and SDP serialization code.
Compatibility support
Added Polyfill support and updated
LangVersionto14.0so newer APIs and compiler features can be used consistently across the library’s target frameworks.This makes span/range/split-based parsing available in cross-targeted code while keeping the implementation aligned with modern C# syntax and compiler behavior.
How
The implementation follows a few consistent patterns:
Span<Range>/stackallocsplit buffers for small fixed-field parsing.StringBuilderfor repeated serialization and interpolated strings for fixed-format output.Validation
Validated with:
src\SIPSorcery\SIPSorcery.csprojSome broader test runs may still be affected by unrelated networking/environment issues.