Repository navigation
Description
When trimming a Blazor WASM app, the last usage of Reflection.Emit (after fixing #38678) is coming from System.Text.Json.Serialization.JsonSerializerOptions:
runtime/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.cs
Lines 423 to 438 in d89772a
| internal MemberAccessor MemberAccessorStrategy | |
| { | |
| get | |
| { | |
| if (_memberAccessorStrategy == null) | |
| { | |
| #if NETFRAMEWORK || NETCOREAPP | |
| _memberAccessorStrategy = new ReflectionEmitMemberAccessor(); | |
| #else | |
| _memberAccessorStrategy = new ReflectionMemberAccessor(); | |
| #endif | |
| } | |
| return _memberAccessorStrategy; | |
| } | |
| } |
However, on Mono WASM, RuntimeFeature.IsDynamicCodeCompiled is always false, so using Reflection.Emit is probably a waste, and it brings in a decent amount of code. In my investigations I find it removing ~50KB of IL if we trim this usage of Reflection.Emit.
We should change this code to something more like:
internal MemberAccessor MemberAccessorStrategy
{
get
{
if (_memberAccessorStrategy == null)
{
#if NETFRAMEWORK || NETCOREAPP
if (RuntimeFeature.IsDynamicCodeCompiled)
{
_memberAccessorStrategy = new ReflectionEmitMemberAccessor();
}
else
{
_memberAccessorStrategy = new ReflectionMemberAccessor();
}
#else
_memberAccessorStrategy = new ReflectionMemberAccessor();
#endif
}
return _memberAccessorStrategy;
}
}With changing the code to the above, on a default template Blazor WASM app, I am seeing size savings of:
| Build | Size |
|---|---|
| master | 3,366,912 bytes |
| #38729 | 3,039,232 bytes |
| #38729 + this change | 2,990,080 bytes |
So almost a 50 KB savings by allowing the removing all usages of System.Reflection.Emit.
Activity
steveharter commentedon Jul 6, 2020
FWIW we previously used
RuntimeFeature.IsDynamicCodeSupportedbut that API requires netstandard 2.1, and we only had support for 2.0 in STJ.dll and due to the switch to netcoreapp (instead of netstandard) it was decided not worth the hit of a larger package just for this.But using an ifdef branch for netcoreapp that checks RuntimeFeature should work great.
eerhardt commentedon Jul 6, 2020
I ran the attached JSON perf application with and without this change.
Program (1).zip
5.0.0-preview.8.20354.5
Proposed change
So it is ~15-20% slower to use the Reflection based strategy after everything is warmed up. (Note this is explicitly not testing the time it takes to emit IL during warmup.)
eerhardt commentedon Jul 30, 2020
Since the performance tradeoff of taking this change isn't necessarily a net-win, we won't be making this change in 5.0. Moving to 6.0.
WIP - Add Json feature switch to remove Ref.Emit code.
eerhardt commentedon May 11, 2021
With the advent of the JSON source generator, we can solve the ~15-20% slower to use Reflection based strategy issue above by telling places that need fast JSON Serialization to use the source generator.
With that in mind, I created a prototype of introducing a feature switch in System.Text.Json to use the
ReflectionMemberAccessorin order to allow for trimming the Ref.Emit code. See the prototype here:59c60c6
This allows for the following size savings (.br compressed):
Before: 2,644,974 bytes
After: 2,628,106 bytes (with new feature switch set)
So roughly 16.5KB .br compressed size savings.
This could be an alternative approach to allow for the Ref.Emit code to be trimmed in a default Blazor WASM app, even if not all the places that use JSON Serialization use the source generator.
eerhardt commentedon May 17, 2021
Now that the
microsoft-net-sdk-blazorwebassembly-aotworkload is functional, I was able to retest trimming out the Reflection.Emit code along with re-linking thedotnet.wasmassembly (which also trims the native Reflection.Emit code).Using the rough numbers of summing all the *.br files in the publish directory, here are the results I am seeing:
So paired with the re-linking feature, if we can trim away the Reflection.Emit code, it would trim 25 KB compressed. And if you aren’t re-linking we’d save the estimated ~18 KBs, as above.
However, it still comes at a tradeoff for throughput. I re-ran the above
Program (1).zipapplication using the latest .NET 6 bits with this feature switch, and I'm seeing the following results:Ref.Emit + re-linking:
Trim Ref.Emit + re-linking:
As you can see, the performance of Json serialization has gotten drastically better from my original numbers (I may have been on a different machine as well). However, the difference between using Ref.Emit vs. Reflection has gotten wider. For "serialize" - ~48% worse, and for deserialize - ~73% worse.
For ahead-of-time (AOT) compilation, here are the differences:
Size difference, the only file that changes when using AOT is the
dotnet.wasmfile.So a ~62 KB .br compressed size savings.
The throughput ratio between Ref.Emit vs Reflection is about the same on AOT as it is on non-AOT:
Ref.Emit + AOT:
Trim Ref.Emit + AOT:
6 remaining items
eiriktsarpalis commentedon Sep 2, 2022
Appears to have been fixed by #54027.
eerhardt commentedon Sep 2, 2022
There are 2 different switches here:
#54027 allows JsonSerializer work at all when you can't IL Emit code. This issue is to track the 2nd case, when you can IL Emit code, but it get interpreted.
dotnet-policy-service commentedon Jul 10, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
Select STJ member accessor based on IsDynamicCodeCompiled (#130503)
Select STJ member accessor based on IsDynamicCodeCompiled (#130503)
Use Reflection.Emit accessors when dynamic code is supported (#131452)