core
core copied to clipboard
What's new in .NET 7 Preview 6 [WIP]
What's new in .NET 7 Preview 6
This issue is for teams to highlight work for the community that will release .NET 7 Preview 6
To add content, use a new conversation entry. The entry should include the team name and feature title as the first line as shown in the template below.
## Team Name: Feature title
[link to the tracking issue or epic item for the work]
Tell the story of the feature and anything the community should pay particular attention
to be successful using the feature.
Preview 1: https://github.com/dotnet/core/issues/7106 Preview 2: https://github.com/dotnet/core/issues/7107 Preview 3: https://github.com/dotnet/core/issues/7108 Preview 4: https://github.com/dotnet/core/issues/7378 Preview 5: https://github.com/dotnet/core/issues/7441 Preview 6: https://github.com/dotnet/core/issues/7454 Preview 7: https://github.com/dotnet/core/issues/7455
Type Converters
Exposing type converters for the newly added primitive types DateOnly, TimeOnly, Int128, UInt128, and Half
https://github.com/dotnet/runtime/issues/68743
namespace System.ComponentModel
{
public class DateOnlyConverter : System.ComponentModel.TypeConverter
{
public DateOnlyConverter() { }
}
public class TimeOnlyConverter : System.ComponentModel.TypeConverter
{
public TimeOnlyConverter() { }
}
public class Int128Converter : System.ComponentModel.BaseNumberConverter
{
public Int128Converter() { }
}
public class UInt128Converter : System.ComponentModel.BaseNumberConverter
{
public UInt128Converter() { }
}
public class HalfConverter : System.ComponentModel.BaseNumberConverter
{
public HalfConverter() { }
}
}
Usage Example
TypeConverter dateOnlyConverter = TypeDescriptor.GetConverter(typeof(DateOnly));
// produce DateOnly value of DateOnly(1940, 10, 9)
DateOnly? date = dateOnlyConverter.ConvertFromString("1940-10-09") as DateOnly?;
TypeConverter timeOnlyConverter = TypeDescriptor.GetConverter(typeof(TimeOnly));
// produce TimeOnly value of TimeOnly(20, 30, 50)
TimeOnly? time = timeOnlyConverter.ConvertFromString("20:30:50") as TimeOnly?;
TypeConverter halfConverter = TypeDescriptor.GetConverter(typeof(Half));
// produce Half value of -1.2
Half? half = halfConverter.ConvertFromString(((Half)(-1.2)).ToString()) as Half?;
TypeConverter Int128Converter = TypeDescriptor.GetConverter(typeof(Int128));
// produce Int128 value of Int128.MaxValue which equal 170141183460469231731687303715884105727
Int128? int128 = Int128Converter.ConvertFromString("170141183460469231731687303715884105727") as Int128?;
TypeConverter UInt128Converter = TypeDescriptor.GetConverter(typeof(UInt128));
// produce UInt128 value of UInt128.MaxValue which equal 340282366920938463463374607431768211455
UInt128? uint128 = UInt128Converter.ConvertFromString("340282366920938463463374607431768211455") as UInt128?;
JSON contract customization
In certain situations developers serializing or deserializing JSON find that they don't want to or cannot change types because they i.e. come from external library or it would greatly pollute the code but need to make some changes which influence serialization like removing property, changing how numbers get serialized, how object is created etc. They frequently are forced to either write wrappers or custom converters which are not only a hassle but also make serialization slower.
JSON contract customization allows user for more control over what and how types get serialized or deserialized.
Opting into customization
There are two basic ways developers can "plug" into the customization, they both end up assigning JsonSerializerOptions.TypeInfoResolver and require assigning resolver:
- developer can use
DefaultJsonTypeInfoResolverand add their modifier, all modifiers will be called serially. Simply:
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers =
{
(JsonTypeInfo jsonTypeInfo) =>
{
// your modifications here, i.e.:
if (jsonTypeInfo.Type == typeof(int))
{
jsonTypeInfo.NumberHandling = JsonNumberHandling.AllowReadingFromString;
}
}
}
}
};
Point point = JsonSerializer.Deserialize<Point>(@"{""X"":""12"",""Y"":""3""}", options);
Console.WriteLine($"({point.X},{point.Y})"); // (12,3)
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}
- writing own custom resolver by implementing
System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver- when type is not handled code should returnnull-IJsonTypeInfoResolvercan be combined with others into effective resolver which will return first non-null answer. For exampleJsonTypeInfoResolver.Combine(new MyResolver(), new DefaultJsonTypeInfoResolver())
Customizations
IJsonTypeInfoResolver job is to provide JsonTypeInfo for any Type serializer requests - this will only happen once per type per options.
JsonTypeInfo.Kind will determine what knobs developer can change and is determined based on converter which is determined based on converters provided to options. For example JsonTypeInfoKind.Object means Properties can be added/modified while JsonTypeInfoKind.None means none of the knobs is guaranteed to be used - that can happen when type has custom converter.
JsonTypeInfo is either created by DefaultJsonTypeInfoResolver with pre-populated knobs coming from i.e. custom attributes or can be created from scratch by user: JsonTypeInfo.CreateJsonTypeInfo - creating from scratch means user will also need to set JsonTypeInfo.CreateObject.
Customizing properties
Properties are only relevant when JsonTypeInfo.Kind == JsonTypeInfoKind.Object and in case of DefaultJsonTypeInfoResolver will be pre-populated.
They can be modified or created by using JsonTypeInfo.CreateJsonPropertyInfo and added to the list of properties, i.e. say you got a class from separate library which has weirdly designed APIs which you can't change:
class MyClass
{
private string _name = string.Empty;
public string LastName { get; set; }
public string GetName() => _name;
public void SetName(string name)
{
_name = name;
}
}
Before this feature existed you'd need to wrap your type hierarchy or create your own custom converter for that type. Now you can simply fix it:
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { ModifyTypeInfo }
}
};
MyClass obj = new()
{
LastName = "Doe"
};
obj.SetName("John");
string serialized = JsonSerializer.Serialize(obj, options); // {"LastName":"Doe","Name":"John"}
static void ModifyTypeInfo(JsonTypeInfo ti)
{
if (ti.Type != typeof(MyClass))
return;
JsonPropertyInfo property = ti.CreateJsonPropertyInfo(typeof(string), "Name");
property.Get = (obj) =>
{
MyClass myClass = (MyClass)obj;
return myClass.GetName();
};
property.Set = (obj, val) =>
{
MyClass myClass = (MyClass)obj;
string value = (string)val;
myClass.SetName(value);
};
ti.Properties.Add(property);
}
Conditional serialization of properties
In some usage scenarios it's required that some default values don't get serialized. I.e. you don't want 0 to show up in JSON for certain properties. It was possible to get that scenario to work before by using JsonIgnoreAttribute with JsonIgnoreCondition.WhenWritingDefault.
The problem occurs when your default value is not 0 and it's something different, i.e. -1 or it depends on some external setting.
Now it's possible to set your own predicate ShouldSerialize with any condition you'd like. I.e. say you have string property and you'd want N/A to not show up in JSON:
// string property you'd like to customize
JsonPropertyInfo property = ...;
property.ShouldSerialize = (obj, val) =>
{
// in this specific example we don't use parent but it's available if needed
MyClass parentObj = (MyClass)obj;
string value = (string)val;
return value != "N/A";
};
Sample: Ignoring properties with specific name or type
var modifier = new IgnorePropertiesWithNameOrType();
modifier.IgnorePropertyWithType(typeof(SecretHolder));
modifier.IgnorePropertyWithName("IrrelevantDetail");
JsonSerializerOptions options = new()
{
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
{
Modifiers = { modifier.ModifyTypeInfo }
}
};
ExampleClass obj = new()
{
Name = "Test",
Secret = new SecretHolder() { Value = "MySecret" },
IrrelevantDetail = 15,
};
string output = JsonSerializer.Serialize(obj, options); // {"Name":"Test"}
class ExampleClass
{
public string Name { get; set; }
public SecretHolder Secret { get; set; }
public int IrrelevantDetail { get; set; }
}
class SecretHolder
{
public string Value { get; set; }
}
class IgnorePropertiesWithNameOrType
{
private List<Type> _ignoredTypes = new List<Type>();
private List<string> _ignoredNames = new List<string>();
public void IgnorePropertyWithType(Type type)
{
_ignoredTypes.Add(type);
}
public void IgnorePropertyWithName(string name)
{
_ignoredNames.Add(name);
}
public void ModifyTypeInfo(JsonTypeInfo ti)
{
JsonPropertyInfo[] props = ti.Properties.Where((pi) => !_ignoredTypes.Contains(pi.PropertyType) && !_ignoredNames.Contains(pi.Name)).ToArray();
ti.Properties.Clear();
foreach (var pi in props)
{
ti.Properties.Add(pi);
}
}
}
System.Formats.Tar API updates
In Preview 4, the System.Formats.Tar assembly was introduced. It offers APIs for manipulating TAR archives.
In Preview 6, some changes were made to cover a few special cases:
Global Extended Attributes specialized class
The initial design was assuming that only PAX TAR archives could contain a single Global Extended Attributes (GEA) entry in the first position, but it was discovered that TAR archives can contain multiple GEA entries, which can affect all subsequent entries until encountering a new GEA entry or the end of the archive.
It was also discovered that GEA entries should not be expected only in archives containing PAX entries exclusively: they can show up in archives that intermix entries of different formats. So a new class was added to describe a GEA entry:
+ public sealed partial class PaxGlobalExtendedAttributesTarEntry : PosixTarEntry
+ {
+ public PaxGlobalExtendedAttributesTarEntry(IEnumerable<KeyValuePair<string, string>> globalExtendedAttributes) { }
+ public IReadOnlyDictionary<string, string> GlobalExtendedAttributes { get { throw null; } }
+ }
Entry format, not archive format
Since it was also discovered that entries of different formats can be intermixed in a single TAR archive, the TarFormat enum was renamed to TarEntryFormat:
-public enum TarFormat
+public enum TarEntryFormat
{
...
}
And a new property was added to TarEntry to expose the entry's format:
public abstract partial class TarEntry
{
...
+ public TarEntryFormat Format { get { throw null; } }
...
}
Writing and reading changes
The Format property was removed from TarReader because no archive is expected to have all its entries in a single format.
Since GEA entries are now being described with their own specialized class, and multiple entries of this type can be found in a single archive, the dictionary property from the TarReader was also removed:
public sealed partial class TarReader : IDisposable
{
...
- public TarFormat Format { get { throw null; } }
- public IReadOnlyDictionary<string, string>? GlobalExtendedAttributes { get { throw null; } }
...
}
The addition of the specialized GEA class also affected TarWriter:
- The constructor that used to take the dictionary for a single first-position GEA entry was removed.
- A new constructor that takes only the stream and the
leaveOpenboolean was added. - The constructor that takes the
TarFormatwas kept, but the enum was renamed, and a default value was set toPax. The method's documentation was changed to explain that the specified format parameter only applies to theTarWriter.WriteEntrymethod that adds an entry from a file.
public sealed partial class TarWriter : IDisposable
{
...
- public TarWriter(Stream archiveStream, IEnumerable<KeyValuePair<string, string>>? globalExtendedAttributes = null, bool leaveOpen = false) { }
+ public TarWriter(Stream archiveStream, bool leaveOpen = false) { }
- public TarWriter(Stream archiveStream, TarFormat archiveFormat, bool leaveOpen = false) { }
+ public TarWriter(Stream archiveStream, TarEntryFormat format = TarEntryFormat.Pax, bool leaveOpen = false) { }
public void WriteEntry(string fileName, string? entryName) { }
...
}
CodeGen
Community PRs (Many thanks to JIT community contributors!)
- @am11 added a glossary for {M}IBC in https://github.com/dotnet/runtime/pull/68111.
- @aromaa contributed https://github.com/dotnet/runtime/pull/70655. In some cases, inlining can reveal that a pinned local refers to a stack location; this change detects that case and removes the pin, as it’s unnecessary and blocks some optimizations.
- @huoyaoyuan removed ternary workaround in order to remove redundant code when inlining string.IsNullOrEmpty in https://github.com/dotnet/runtime/pull/63095.
- @singleaccretion made 41 PR contributions during Preview 6 ( https://github.com/dotnet/runtime/pulls?q=is%3Apr+is%3Aclosed+label%3Aarea-CodeGen-coreclr+closed%3A2022-05-24..2022-06-22+author%3Asingleaccretion). A lot of this work is streamlining the JITs IR and improving how the JIT represents struct values, in particular around calls and call arguments. -- Enabling TYP_STRUCT LCL_VAR/LCL_FLD call args on Windows x64 ( https://github.com/dotnet/runtime/pull/70777 ) and Windows x86 (https://github.com/dotnet/runtime/pull/70779 ) showing much improved code-quality in many cases when structs are passed as arguments -- Deleting field sequences from LCL_FLD and VNF_PtrToArrElem https://github.com/dotnet/runtime/pull/68986 which simplified the JITs IR and resulted in great code-quality and throughput improvements. This greatly improved codegen for some cases of struct copies.
- @skiFoD made an optimization for removing unnecessary range checks in https://github.com/dotnet/runtime/pull/70222 (for example, x <= 255 for a byte x) and transforming "~x + 1" to "-x" in https://github.com/dotnet/runtime/pull/69600.
- @Wraith2 improved the JIT to remove a lot of unnecessary unconditional jumps in https://github.com/dotnet/runtime/pull/69041 and added blsmsk xarch instruction for the pattern XOR(x, x - 1) in https://github.com/dotnet/runtime/pull/66561.
Dynamic PGO
- https://github.com/dotnet/runtime/pull/68703 adds support for guarded devirtualization for delegate calls. When dynamic PGO is enabled this allows the JIT to specialize and inline delegate calls when it determines this might be profitable. This can greatly increase performance as demonstrated by the following microbenchmark where dynamic PGO is now roughly 5x faster (was roughly 2.5x before) than no PGO.
For now only delegates bound to instance methods are supported. We expect that support for static methods will come in early previews for .NET 8.

(note to editors: the screenshot is taken from the link above, please convert to appropriate code blocks/tables in the final blog post)
- We started to implement hot and cold splitting and https://github.com/dotnet/runtime/pull/69763 is the first part of it.
- Hot/Cold splitting on ARM64 has been implemented in the JIT (PR). This work largely consisted of generating long pseudo-instructions for branching between hot/cold sections, and loading constants from the data section.
- We've also added support for hot/cold splitting of functions with exception handling (PR). Without PGO data, our heuristic moves all exception handling funclets to the cold section, and copies “finally” blocks to the hot section; we’re operating under the assumption that exceptions occur rarely, but finally blocks are executed regardless of the presence of exceptions.
- When running various SuperPMI collections, the JIT split ~14% of functions on the low end (no PGO data), and ~26% of functions on the high end (with PGO data). See more metrics here.
Arm64
- https://github.com/dotnet/runtime/pull/70600 enabled LSE atomics in Windows Arm64. It improves lock related operation performance by up to 78%.

- https://github.com/dotnet/runtime/pull/70749 enabled addressing mode for gc type on Arm64 to gain up to 45% of performance.
- https://github.com/dotnet/runtime/pull/71044 aligned arm64 data section for 16 byte SIMD16.
- https://github.com/dotnet/runtime/pull/70599 optimizes i % 2 and gives up to 17% throughput improvement.
Loop Optimizations
- Loop cloning driven by type tests: https://github.com/dotnet/runtime/pull/70377 enables loop cloning based on loop invariant type tests such as those added by GDV. This effectively allows the fast path loop to hoist the type check out of the loop, leading to improved performance. For example:

- Started hoisting invariants out of multi-level nested loop in https://github.com/dotnet/runtime/pull/68061.

General Optimizations
- PR https://github.com/dotnet/runtime/pull/68874 improved handling of vector constants in the JIT including support for value numbering, constant propagation, and other optimizations already available to other constants.
.NET 7 GA is available. Closing these pre-release issues.