Open colejohnson66 opened 1 year ago
Tagging @333fred to advise/triage.
We could certainly make this smarter, but it's not going to be trivial due to how we do (or don't do, in this case) constant folding when the entire string isn't a constant value.
We'd review a pr that implements this (likely would be a change in how the local rewriter lowers these), but are unlikely to prioritize it ourselves.
Oh, and one more detail before I forget: AppendFormatted calls can have other important side-effects, like CallerExpression attributes, or being used for logging (where the fact that this is a placeholder is important). We can only optimize this for regular strings because of this.
If allowing those CallerX
attributes is so important, why isn't the constant+nameof one lowered to a single AppendLiteral
call instead of AppendLiteral("abc ") / AppendX("Test")
? Basically, which is more "correct"? Should nameof
expressions always be a seperate call to AppendX
(in which the second line is correct)? Or should they always be treated as a string constant and merged (in which the first line is correct)? If the former, is AppendFormatted
correct, or should that be AppendLiteral
?
Because my example above shows both things happening, but the choice is dependent on using a property (or possibly other things); the nameof
expression is treated as a literal for the first call (what would be AppendLiteral
) and merges it with the string prior, but it's treated as an interpolated value for the second (AppendFormatted
).
If allowing those CallerX attributes is so important, why isn't the constant+nameof one lowered to a single AppendLiteral call instead of AppendLiteral("abc ") / AppendX("Test")?
We didn't feel that these were important for AppendLiteral, and didn't spec it this way. AppendLiteral is specified as being looked up once for the entire string, and using that one result for every literal component. AppendFormatted is not specified this way, as every interpolation hole must be individually looked up.
By "lookup", I assume you mean name resolution? In which case, that's fine. But that doesn't explain why nameof(Test)
is lowered as a constant in the $"abc {nameof(Test)}"
case, but lowered to a "hole" in the $"abc {nameof(Test)}{Property}"
case.
My comment was pointing out this discrepancy and asking "which is ideal?" Because, if we need to take CallerX
attributes into account, then the "hole" in the $"abc {nameof(Test)}"
case should be lowered to an AppendFormatted
call, but it's not.
These questions are all spelled out in the interpolated strings draft spec. When the string is a constant, we always prefer overloads that take strings. When the string is not a constant (has holes that are not constants), we prefer overloads that take interpolated string handlers. For converting an interpolated string to a string, the compiler is free to optimize as it chooses, including just emitting a string constant, using Concat/Format, or using the DefaultInterpolatedStringHandler. For converting an interpolated string to an observable handler type, the compiler is not free to make optimizations that would affect which Append methods is called.
Not only nameof
but also all string constants:
I've approached only built-in handlers (DefaultInterpolatedStringHandler
and so on) for the time being.
Version Used: sharplab.io;
main (24 Jan 2023)
Steps to Reproduce:
nameof
expression.nameof
expression is now lowered toAppendFormatted
(notAppendLiteral
)Source:
Expected Behavior:
Actual Behavior:
Note how the
nameof
expression was combined with the literal string next to it in the first, but not in the second. In addition, the second one lowers thenameof
into anAppendFormatted
call instead of anAppendLiteral
.Performance
Running the above benchmark (with BenchmarkDotNet) on 7.0.323.6910 (x64 RyuJIT AVX2) shows a slight performance decrease for the actual compiler output compared to what I would expect:
In fact, performance is horrible compared to plain concatenation (lowered to
string.Concat
).