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

@abonieabonie 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
MethodMeanErrorStdDevGen0Allocated
SingleVariable70.49 ns1.214 ns1.136 ns0.0107136 B
JustLiterals145.61 ns1.443 ns1.350 ns0.0215272 B
Variables144.05 ns0.959 ns0.897 ns0.0215272 B
Function116.73 ns1.557 ns1.381 ns0.0191240 B
LongString363.35 ns2.838 ns2.654 ns0.43305432 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
MethodMeanErrorStdDevMedianGen0Allocated
SingleVariable0.0004 ns0.0013 ns0.0011 ns0.0000 ns--
JustLiterals12.0562 ns0.0719 ns0.0673 ns12.0500 ns0.003240 B
Variables11.9894 ns0.0810 ns0.0718 ns12.0101 ns0.003240 B
Function12.2912 ns0.1201 ns0.1124 ns12.3161 ns0.003240 B
LongString206.0906 ns2.4311 ns2.0301 ns206.5404 ns0.41585216 B

Checklist

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

@github-actions

github-actionsBot commented Jan 19, 2024

Copy link
Copy Markdown
Contributor

❗ Release notes required


✅ Found changes and release notes in following paths:

Change pathRelease notes pathDescription
src/Compilerdocs/release-notes/.FSharp.Compiler.Service/8.0.300.md
LanguageFeatures.fsidocs/release-notes/.Language/preview.md

Comment threadsrc/Compiler/Checking/CheckExpressions.fs Outdated
@abonie
abonieforce-pushed the str-interp-to-concat branch 3 times, most recently from a44ceed to e9e4f4bCompareFebruary 5, 2024 19:28
@abonie
abonieforce-pushed the str-interp-to-concat branch 3 times, most recently from 79260a2 to fdea8a5CompareFebruary 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
abonieforce-pushed the str-interp-to-concat branch from fdea8a5 to a60c558CompareFebruary 15, 2024 16:18
@abonie

Copy link
Copy Markdown
MemberAuthor

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

MethodMeanErrorStdDevGen0Allocated
SingleVariable78.08 ns1.021 ns0.905 ns0.0005136 B
JustLiterals157.44 ns2.675 ns2.502 ns0.0010272 B
Variables151.16 ns1.498 ns1.401 ns0.0010272 B
Function125.50 ns1.239 ns1.099 ns0.0010240 B
LongString641.34 ns2.861 ns2.536 ns0.02195432 B

With optimization:

MethodMeanErrorStdDevMedianGen0Allocated
SingleVariable0.0064 ns0.0103 ns0.0096 ns0.0000 ns--
JustLiterals14.8223 ns0.1345 ns0.1258 ns14.8525 ns0.000140 B
Variables14.1441 ns0.1240 ns0.1099 ns14.1558 ns0.000140 B
Function14.2070 ns0.1159 ns0.1028 ns14.2344 ns0.000140 B
LongString508.0675 ns6.9382 ns6.1506 ns507.0075 ns0.02105216 B

@abonieabonie changed the title [WIP] Lower interpolation into a call to concatLower interpolation into a call to concatFeb 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 ownerFebruary 16, 2024 16:18
@abonie
abonie requested a review from T-GroFebruary 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

Copy link
Copy Markdown
MemberAuthor

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 threadsrc/Compiler/Checking/CheckExpressions.fs Outdated
Comment threadsrc/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 threadsrc/Compiler/Checking/CheckExpressions.fs
@abonie

Copy link
Copy Markdown
MemberAuthor

/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:mainFeb 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 freeto 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

@abonie@psfinaki@vzarytovskii@brianrourkeboll@T-Gro