Skip to content

avoid some string alloc by using AppendJoin#4971

Merged
thomhurst merged 4 commits intothomhurst:mainfrom
SimonCropp:avoid-some-string-alloc-by-using-AppendJoin
Mar 3, 2026
Merged

avoid some string alloc by using AppendJoin#4971
thomhurst merged 4 commits intothomhurst:mainfrom
SimonCropp:avoid-some-string-alloc-by-using-AppendJoin

Conversation

@SimonCropp
Copy link
Contributor

No description provided.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review

Summary

This PR replaces patterns with to avoid intermediate string allocations. The optimization is correct and well-understood.


Positive Observations

  • The core intent is sound. string.Join allocates an intermediate string before Append writes it; AppendJoin writes tokens directly into the builder.
  • The new CodeWriter.AppendJoin<T> implementation correctly preserves the existing indentation/newline behaviour (_isNewLine guard) before delegating to the underlying StringBuilder.AppendJoin.
  • The interface doc-comment follows the existing convention.

Issue: Redundant .ToArray() calls in AssertionMethodGenerator.cs

The PR's goal is reducing allocations, but the AssertionMethodGenerator.cs changes leave a redundant intermediate array in place:

// current (after this PR)
var paramList = parameters.Select(p => $"_{p.Name}").ToArray();
sourceBuilder.AppendJoin(", ", paramList);

StringBuilder.AppendJoin has an IEnumerable<T> overload, so the ToArray() call creates an array that is iterated once and immediately discarded. The allocation this PR is trying to save is partially offset by that array allocation.

Suggested fix – drop the intermediate variable entirely:

sourceBuilder.AppendJoin(", ", parameters.Select(p => $"_{p.Name}"));

This applies to all four call-sites changed in AssertionMethodGenerator.cs (lines 783, 832, 857, 866 in the post-merge file).


Minor: Adding a method to a public interface is a breaking change

ICodeWriter is declared public, so any external code that implements it will fail to compile after this change. If external implementors are a concern, a default interface method would be safer:

ICodeWriter AppendJoin<T>(string separator, IEnumerable<T> values)
    => Append(string.Join(separator, values)); // default keeps existing behaviour

If ICodeWriter is intentionally internal-only (despite the public modifier), this can be ignored.


Scope note

The source-generator call sites (compile-time, cold path) will see negligible real-world benefit from this change. The most meaningful win is AspireFixture.cs, which runs at test execution time. That said, consistency across the codebase is valuable and the change does no harm.


Overall: good direction. The main ask is removing the now-redundant .ToArray() calls to actually realise the full allocation saving the PR aims for.

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review

Summary

This PR replaces sb.Append(string.Join(...)) patterns with sb.AppendJoin(...) to avoid intermediate string allocations. The optimization is correct and well-understood.


Positive Observations

  • The core intent is sound. string.Join allocates an intermediate string before Append writes it; AppendJoin writes tokens directly into the builder.
  • The new CodeWriter.AppendJoin<T> implementation correctly preserves the existing indentation/newline behaviour (_isNewLine guard) before delegating to the underlying StringBuilder.AppendJoin.
  • The interface doc-comment follows the existing convention.

Issue: Redundant .ToArray() calls in AssertionMethodGenerator.cs

The PR's goal is reducing allocations, but the AssertionMethodGenerator.cs changes leave a redundant intermediate array in place:

// current (after this PR)
var paramList = parameters.Select(p => $"_{p.Name}").ToArray();
sourceBuilder.AppendJoin(", ", paramList);

StringBuilder.AppendJoin has an IEnumerable<T> overload, so the .ToArray() call creates an array that is iterated once and immediately discarded. The allocation this PR is trying to save is partially offset by that array allocation.

Suggested fix – drop the intermediate variable entirely:

sourceBuilder.AppendJoin(", ", parameters.Select(p => $"_{p.Name}"));

This applies to all four call-sites changed in AssertionMethodGenerator.cs.


Minor: Adding a method to a public interface is a breaking change

ICodeWriter is declared public, so any external code that implements it will fail to compile after this change. If external implementors are a concern, a default interface method would be safer:

ICodeWriter AppendJoin<T>(string separator, IEnumerable<T> values)
    => Append(string.Join(separator, values)); // default keeps existing behaviour

If ICodeWriter is intentionally internal-only (despite the public modifier), this can be ignored.


Scope note

The source-generator call sites (compile-time, cold path) will see negligible real-world benefit from this change. The most meaningful win is AspireFixture.cs, which runs at test execution time. That said, consistency across the codebase is valuable and the change does no harm.


Overall: good direction. The main ask is removing the now-redundant .ToArray() calls to actually realise the full allocation saving the PR aims for.

@thomhurst thomhurst merged commit 9e952b2 into thomhurst:main Mar 3, 2026
9 of 11 checks passed
@claude claude bot mentioned this pull request Mar 3, 2026
1 task
@SimonCropp SimonCropp deleted the avoid-some-string-alloc-by-using-AppendJoin branch March 4, 2026 01:56
intellitect-bot pushed a commit to IntelliTect/EssentialCSharp.Web that referenced this pull request Mar 4, 2026
Updated [TUnit](https://github.com/thomhurst/TUnit) from 1.18.9 to
1.18.21.

<details>
<summary>Release notes</summary>

_Sourced from [TUnit's
releases](https://github.com/thomhurst/TUnit/releases)._

## 1.18.21

<!-- Release notes generated using configuration in .github/release.yml
at v1.18.21 -->

## What's Changed
### Other Changes
* avoid some string alloc by using AppendJoin by @​SimonCropp in
thomhurst/TUnit#4971
* respect attribute namespace during migration by @​SimonCropp in
thomhurst/TUnit#5066
* fix: parent data source initialization spans under session span by
@​thomhurst in thomhurst/TUnit#5071
### Dependencies
* chore(deps): update tunit to 1.18.9 by @​thomhurst in
thomhurst/TUnit#5060
* chore(deps): update dependency polyfill to 9.14.0 by @​thomhurst in
thomhurst/TUnit#5063
* chore(deps): update dependency
microsoft.testing.extensions.codecoverage to 18.5.2 by @​thomhurst in
thomhurst/TUnit#5065
* chore(deps): update dependency polyfill to 9.14.0 by @​thomhurst in
thomhurst/TUnit#5062
* chore(deps): update actions/github-script action to v8 by @​thomhurst
in thomhurst/TUnit#5053
* chore(deps): update dependency polyfill to 9.15.0 by @​thomhurst in
thomhurst/TUnit#5067
* chore(deps): update dependency polyfill to 9.15.0 by @​thomhurst in
thomhurst/TUnit#5068


**Full Changelog**:
thomhurst/TUnit@v1.18.9...v1.18.21

Commits viewable in [compare
view](thomhurst/TUnit@v1.18.9...v1.18.21).
</details>

[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=TUnit&package-manager=nuget&previous-version=1.18.9&new-version=1.18.21)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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