Skip to content

Lower interpolation into a call to concat#16556

Merged
abonie merged 25 commits into
dotnet:mainfrom
abonie:str-interp-to-concat
Feb 22, 2024
Merged

Lower interpolation into a call to concat#16556
abonie merged 25 commits into
dotnet:mainfrom
abonie:str-interp-to-concat

Conversation

@abonie

@abonie abonie commented Jan 19, 2024

Copy link
Copy Markdown
Member

Description

Optimization that lowers string interpolation into a call to concat iff there are at most 4 string parts and all fill expressions are strings.

Fixes #16247

Benchmarks

Run a benchmark like in this gist with and without the feature flag set.

Results without the flag (no optimization):


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22621.3155/22H2/2022Update/SunValley2)
11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.300-preview.24101.10
  [Host]     : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI DEBUG
  DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


Method Mean Error StdDev Gen0 Allocated
SingleVariable 70.49 ns 1.214 ns 1.136 ns 0.0107 136 B
JustLiterals 145.61 ns 1.443 ns 1.350 ns 0.0215 272 B
Variables 144.05 ns 0.959 ns 0.897 ns 0.0215 272 B
Function 116.73 ns 1.557 ns 1.381 ns 0.0191 240 B
LongString 363.35 ns 2.838 ns 2.654 ns 0.4330 5432 B

Results with the flag set (with optimization):


BenchmarkDotNet v0.13.12, Windows 11 (10.0.22621.3155/22H2/2022Update/SunValley2)
11th Gen Intel Core i7-11850H 2.50GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.300-preview.24101.10
  [Host]     : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI DEBUG
  DefaultJob : .NET 8.0.2 (8.0.224.6711), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI


Method Mean Error StdDev Median Gen0 Allocated
SingleVariable 0.0004 ns 0.0013 ns 0.0011 ns 0.0000 ns - -
JustLiterals 12.0562 ns 0.0719 ns 0.0673 ns 12.0500 ns 0.0032 40 B
Variables 11.9894 ns 0.0810 ns 0.0718 ns 12.0101 ns 0.0032 40 B
Function 12.2912 ns 0.1201 ns 0.1124 ns 12.3161 ns 0.0032 40 B
LongString 206.0906 ns 2.4311 ns 2.0301 ns 206.5404 ns 0.4158 5216 B

Checklist

  • Test cases added
  • Performance benchmarks added in case of performance changes
  • Release notes entry updated

@github-actions

github-actions Bot commented Jan 19, 2024

Copy link
Copy Markdown
Contributor

❗ Release notes required


✅ Found changes and release notes in following paths:

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/8.0.300.md
LanguageFeatures.fsi docs/release-notes/.Language/preview.md

Comment thread src/Compiler/Checking/CheckExpressions.fs Outdated
@abonie abonie force-pushed the str-interp-to-concat branch 3 times, most recently from a44ceed to e9e4f4b Compare February 5, 2024 19:28
@abonie abonie force-pushed the str-interp-to-concat branch 3 times, most recently from 79260a2 to fdea8a5 Compare February 14, 2024 16:45
Sanity check that lowering to concat does not break these simple cases
Initial attempt with many TODOs, also not sure whether it should be done
in checking, but it seems that later we would have to again parse the
string (since CheckExpressions is going from AST version of an
interpolated string to a sprintf call basically)
Cannot really optimize this way if width and other flags are specified.
Typed interpolated expressions should be possible to support, but
skipping them for now (TODO).
E.g. $"{x}{y}" has 5 string parts, including 3 empty strings
There were false positives before
@abonie abonie force-pushed the str-interp-to-concat branch from fdea8a5 to a60c558 Compare February 15, 2024 16:18
@abonie

abonie commented Feb 15, 2024

Copy link
Copy Markdown
Member Author

Also, if anyone is curious about same benchmarks but with server GC:
No optimization:

Method Mean Error StdDev Gen0 Allocated
SingleVariable 78.08 ns 1.021 ns 0.905 ns 0.0005 136 B
JustLiterals 157.44 ns 2.675 ns 2.502 ns 0.0010 272 B
Variables 151.16 ns 1.498 ns 1.401 ns 0.0010 272 B
Function 125.50 ns 1.239 ns 1.099 ns 0.0010 240 B
LongString 641.34 ns 2.861 ns 2.536 ns 0.0219 5432 B

With optimization:

Method Mean Error StdDev Median Gen0 Allocated
SingleVariable 0.0064 ns 0.0103 ns 0.0096 ns 0.0000 ns - -
JustLiterals 14.8223 ns 0.1345 ns 0.1258 ns 14.8525 ns 0.0001 40 B
Variables 14.1441 ns 0.1240 ns 0.1099 ns 14.1558 ns 0.0001 40 B
Function 14.2070 ns 0.1159 ns 0.1028 ns 14.2344 ns 0.0001 40 B
LongString 508.0675 ns 6.9382 ns 6.1506 ns 507.0075 ns 0.0210 5216 B

@abonie abonie changed the title [WIP] Lower interpolation into a call to concat Lower interpolation into a call to concat Feb 15, 2024
@abonie abonie marked this pull request as ready for review February 16, 2024 16:18
@abonie abonie requested a review from a team as a code owner February 16, 2024 16:18
@abonie abonie requested a review from T-Gro February 16, 2024 18:32
@psfinaki

Copy link
Copy Markdown
Contributor

I am not really familiar with it, but on a quick glance, it is something else entirely - it (makes and) optimizes a call to System.String.Concat (while this change will make it so that there is a call to Concat in the first place)

Well yeah I just noticed that it also deals with <5 parts so I wonder if your PR doesn't somehow supersede that thing.

@abonie

abonie commented Feb 20, 2024

Copy link
Copy Markdown
Member Author

Well yeah I just noticed that it also deals with <5 parts so I wonder if your PR doesn't somehow supersede that thing.

It doesn't supersede it for sure. MakeOptimizedSystemStringConcatCall deals with situations where there was a call to Concat in the source code AFAICT. And the reason for the 5 parts thing is that there are optimized overloads for Concat for 2-4 arguments. That's why in my PR if there are more than 4 parts, we don't even want to do Concat, it would be slower.

@psfinaki

Copy link
Copy Markdown
Contributor

Alright then, thanks for the explanation there :)

Comment thread src/Compiler/Checking/CheckExpressions.fs Outdated
Comment thread src/Compiler/Checking/CheckExpressions.fs Outdated
@psfinaki

Copy link
Copy Markdown
Contributor

/run fantomas

  Co-authored-by: psfinaki <5451366+psfinaki@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Comment thread src/Compiler/Checking/CheckExpressions.fs
@abonie

abonie commented Feb 22, 2024

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 2 pipeline(s).

@abonie abonie enabled auto-merge (squash) February 22, 2024 16:02
@abonie abonie merged commit 2de1f68 into dotnet:main Feb 22, 2024
charlesroddie added a commit to charlesroddie/fsharp that referenced this pull request Jun 18, 2026
A string-typed interpolated string is lowered to System.String.Concat of its
parts rather than the reflection-based printf engine: a string-typed hole is
passed through directly, any other plain hole is converted with `string x`, an
aligned/formatted hole with `String.Format(InvariantCulture, ...)`, and a
printf-specifier hole with `sprintf`. This removes the reflection dependency on
the common path, so these interpolations become trim- and NativeAOT-compatible.

This generalizes and replaces the language-version-gated String.Concat
optimization (dotnet#16556), which only handled all-string holes: the lowering now
applies to every string-typed interpolation, ungated. The reflection path is
used only for PrintfFormat/FormattableString-typed interpolation.

The syntax tree now carries each hole's formatting explicitly, so a printf
specifier no longer leaks into an adjacent literal and alignment is no longer a
fake tuple:

    type SynInterpolatedStringPart =
        | String of value: string * range: range
        | FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting

    type SynInterpolationFormatting =
        | DotNet of alignment: SynExpr option * format: Ident option
        | Printf of specifier: string * range: range

Behavioural change: plain `{x}` holes now render with invariant culture (the F#
`string` operator) rather than the current thread culture, matching `string`.

Adds a NativeAOT regression test under tests/AheadOfTime/NativeAOT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
charlesroddie added a commit to charlesroddie/fsharp that referenced this pull request Jun 18, 2026
A string-typed interpolated string is lowered to System.String.Concat of its
parts rather than the reflection-based printf engine: a string-typed hole is
passed through directly, any other plain hole is converted with `string x`, an
aligned/formatted hole with `String.Format(InvariantCulture, ...)`, and a
printf-specifier hole with `sprintf`. This removes the reflection dependency on
the common path, so these interpolations become trim- and NativeAOT-compatible.

This generalizes and replaces the language-version-gated String.Concat
optimization (dotnet#16556), which only handled all-string holes: the lowering now
applies to every string-typed interpolation, ungated. The reflection path is
used only for PrintfFormat/FormattableString-typed interpolation.

The syntax tree now carries each hole's formatting explicitly, so a printf
specifier no longer leaks into an adjacent literal and alignment is no longer a
fake tuple:

    type SynInterpolatedStringPart =
        | String of value: string * range: range
        | FillExpr of fillExpr: SynExpr * formatting: SynInterpolationFormatting

    type SynInterpolationFormatting =
        | DotNet of alignment: SynExpr option * format: Ident option
        | Printf of specifier: string * range: range

Behavioural change: plain `{x}` holes now render with invariant culture (the F#
`string` operator) rather than the current thread culture, matching `string`.

Adds a NativeAOT regression test under tests/AheadOfTime/NativeAOT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
charlesroddie added a commit to charlesroddie/fsharp that referenced this pull request Jun 20, 2026
Rewrite TcInterpolatedStringViaConcat to type-check each interpolation
part in place and convert it to a string expression, then String.Concat
them. This removes the parallel 'holeIsString' bool list and the
flat-fillExprs/dense-parts interleave entirely: 'build' now walks a
single list (the parts), threading only tpenv.

- Plain '{x}' holes are built directly in the typed tree: a string is
  passed through raw (matching dotnet#16556's lean IL), anything else is
  converted via the 'string' operator, emitted through a new
  string_operator_info intrinsic + mkCallStringOperator helper.
- Aligned/formatted and printf holes are checked from a small synthesized
  String.Format/sprintf expression, so name resolution still does the BCL
  work.
- The function-value warning is re-homed per-hole.

Known follow-ups: ill-typed formatted holes currently report their error
twice (the formatted arm type-checks the hole once for the warning and
again inside String.Format); the warning wants to move to its own pass
over hole types, which also removes that double check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

Optimization for string interpolation - simple scenarios: unfolding constants and lowering to concatenation

5 participants