From 3d3e1d465cbac74577394eb6162a7c6ce2d20e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Thu, 14 Aug 2025 08:11:36 +0200 Subject: [PATCH 1/3] Drop size of hello world to 745 kB (With `StackTraceSupport=false`, `OptimizationPreference=Size`, `UseSystemResourceKeys=true`) I don't know if we want it in this form, but it is tempting. The thing that makes the difference is that we are no longer boxing enums and we can get rid of the super expensive enum stringification logic. Not boxing enums also means that #118640 can fully kick in because enums are the last remaining place that looks at custom attributes (looking for `FlagsAttribute`). --- .../StartupCode/StartupCodeHelpers.Extensions.cs | 2 +- .../src/Internal/Runtime/MethodTable.Runtime.cs | 2 +- .../System.Private.CoreLib/src/System/GC.NativeAot.cs | 7 ++++++- .../src/System/Threading/Thread.NativeAot.Windows.cs | 9 +++++++-- .../src/System/Globalization/CultureData.cs | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs index 6bc5ac4a9f5f29..a2d91a0772cd6a 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs @@ -81,7 +81,7 @@ private static int Shutdown() [SupportedOSPlatform("windows")] private static void InitializeApartmentState(ApartmentState state) { - Thread.CurrentThread.SetApartmentState(state); + Thread.CurrentThread.SetApartmentStateUnchecked(state, throwOnError: true); } #endif } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/MethodTable.Runtime.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/MethodTable.Runtime.cs index 899ac448d5f9d8..c0c196713aa846 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/MethodTable.Runtime.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/MethodTable.Runtime.cs @@ -37,7 +37,7 @@ internal bool IsEnum // Generic type definitions that return true for IsPrimitive are type definitions of generic enums. // Otherwise check the base type. - return IsPrimitive && (IsGenericTypeDefinition || NonArrayBaseType == MethodTable.Of()); + return IsPrimitive && (IsGenericTypeDefinition || NonArrayBaseType != MethodTable.Of()); } } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/GC.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/GC.NativeAot.cs index 6f6aec5df683aa..3f14becf5ee7cb 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/GC.NativeAot.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/GC.NativeAot.cs @@ -764,7 +764,7 @@ public static long GetTotalAllocatedBytes(bool precise = false) /// Gets garbage collection memory information. /// An object that contains information about the garbage collector's memory usage. - public static GCMemoryInfo GetGCMemoryInfo() => GetGCMemoryInfo(GCKind.Any); + public static GCMemoryInfo GetGCMemoryInfo() => GetGCMemoryInfoUnchecked(GCKind.Any); /// Gets garbage collection memory information. /// The kind of collection for which to retrieve memory information. @@ -780,6 +780,11 @@ public static GCMemoryInfo GetGCMemoryInfo(GCKind kind) GCKind.Background)); } + return GetGCMemoryInfoUnchecked(kind); + } + + private static GCMemoryInfo GetGCMemoryInfoUnchecked(GCKind kind) + { var data = new GCMemoryInfoData(); RuntimeImports.RhGetMemoryInfo(ref data.GetRawData(), kind); return new GCMemoryInfo(data); diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 12a2273eddf605..0b4e72067616a9 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -246,7 +246,7 @@ public ApartmentState GetApartmentState() } } - private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) + internal bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) { ApartmentState retState; @@ -289,7 +289,12 @@ private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) { if (throwOnError) { - string msg = SR.Format(SR.Thread_ApartmentState_ChangeFailed, retState); + string msg = SR.Format(SR.Thread_ApartmentState_ChangeFailed, retState switch + { + ApartmentState.MTA => "MTA", + ApartmentState.STA => "STA", + _ => "Unknown" + }); throw new InvalidOperationException(msg); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index e52739208f392c..4057d5e84a4990 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -626,7 +626,7 @@ private static CultureData CreateCultureWithInvariantData() invariant._iFirstWeekOfYear = 0; // first week of year // all available calendar type(s). The first one is the default calendar - invariant._waCalendars = [CalendarId.GREGORIAN]; + invariant._waCalendars = (CalendarId[])(object)new ushort[] { (ushort)CalendarId.GREGORIAN }; if (!GlobalizationMode.InvariantNoLoad) { From 2abb25d8e408acfb8dd9b2f58d459b4c97161e6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Strehovsk=C3=BD?= Date: Fri, 15 Aug 2025 06:02:22 +0200 Subject: [PATCH 2/3] Review feedback --- .../StartupCodeHelpers.Extensions.cs | 2 +- .../Threading/Thread.NativeAot.Windows.cs | 4 +- .../src/System/Globalization/CultureData.cs | 58 ++++++++++++++----- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs index a2d91a0772cd6a..6bc5ac4a9f5f29 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/StartupCode/StartupCodeHelpers.Extensions.cs @@ -81,7 +81,7 @@ private static int Shutdown() [SupportedOSPlatform("windows")] private static void InitializeApartmentState(ApartmentState state) { - Thread.CurrentThread.SetApartmentStateUnchecked(state, throwOnError: true); + Thread.CurrentThread.SetApartmentState(state); } #endif } diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs index 0b4e72067616a9..f01fe0f86f38f7 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.Windows.cs @@ -246,7 +246,7 @@ public ApartmentState GetApartmentState() } } - internal bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) + private bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError) { ApartmentState retState; @@ -289,6 +289,8 @@ internal bool SetApartmentStateUnchecked(ApartmentState state, bool throwOnError { if (throwOnError) { + // NOTE: We do the enum stringification manually to avoid introducing a dependency + // on enum stringification in small apps. We set apartment state in the startup path. string msg = SR.Format(SR.Thread_ApartmentState_ChangeFailed, retState switch { ApartmentState.MTA => "MTA", diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 4057d5e84a4990..41356f71618c35 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -543,7 +543,34 @@ internal static CultureInfo[] GetCultures(CultureTypes types) return GlobalizationMode.UseNls ? NlsEnumCultures(types) : IcuEnumCultures(types); } + private static CalendarId[] GetInvariantCalendarIdArray() => [CalendarId.GREGORIAN]; + private static CalendarData[] GetInvariantCalendarDataArray() + { + var calendars = new CalendarData[CalendarData.MAX_CALENDARS]; + calendars[0] = CalendarData.Invariant; + return calendars; + } + private static CultureData CreateCultureWithInvariantData() + { + CultureData invariant = CreateCultureWithInvariantDataWithoutCalendars(); + + // all available calendar type(s). The first one is the default calendar + invariant._waCalendars = GetInvariantCalendarIdArray(); + + if (!GlobalizationMode.InvariantNoLoad) + { + invariant._calendars = GetInvariantCalendarDataArray(); + } + + return invariant; + } + + // Calendar information is expensive size-wise, especially for AOT. + // CalendarData.Invariant is special cased in _waCalendars and _calendars + // accessors and gets initialized lazily. All other uses should use + // CreateCultureWithInvariantData above that populates calendars too. + private static CultureData CreateCultureWithInvariantDataWithoutCalendars() { // Make a new culturedata CultureData invariant = new CultureData(); @@ -625,16 +652,6 @@ private static CultureData CreateCultureWithInvariantData() invariant._iFirstDayOfWeek = 0; // first day of week invariant._iFirstWeekOfYear = 0; // first week of year - // all available calendar type(s). The first one is the default calendar - invariant._waCalendars = (CalendarId[])(object)new ushort[] { (ushort)CalendarId.GREGORIAN }; - - if (!GlobalizationMode.InvariantNoLoad) - { - // Store for specific data about each calendar - invariant._calendars = new CalendarData[CalendarData.MAX_CALENDARS]; - invariant._calendars[0] = CalendarData.Invariant; - } - // Text information invariant._iReadingLayout = 0; @@ -658,7 +675,7 @@ private static CultureData CreateCultureWithInvariantData() /// Build our invariant information /// We need an invariant instance, which we build hard-coded /// - internal static CultureData Invariant => field ??= CreateCultureWithInvariantData(); + internal static CultureData Invariant => field ??= CreateCultureWithInvariantDataWithoutCalendars(); // Cache of cultures we've already looked up private static volatile Dictionary? s_cachedCultures; @@ -1647,7 +1664,13 @@ internal CalendarId[] CalendarIds { get { - if (_waCalendars == null && !GlobalizationMode.Invariant) + if (_waCalendars == null && this == Invariant) + { + // We do this lazily as opposed in CreateCultureWithInvariantData to avoid introducing + // enum arrays into apps that otherwise wouldn't have them. This helps with size in AOT. + _waCalendars = GetInvariantCalendarIdArray(); + } + else if (_waCalendars == null && !GlobalizationMode.Invariant) { // We pass in an array of ints, and native side fills it up with count calendars. // We then have to copy that list to a new array of the right size. @@ -1660,8 +1683,8 @@ internal CalendarId[] CalendarIds // See if we had a calendar to add. if (count == 0) { - // Failed for some reason, just grab Gregorian from Invariant - _waCalendars = Invariant._waCalendars!; + // Failed for some reason, just use Gregorian + _waCalendars = GetInvariantCalendarIdArray(); } else { @@ -1727,6 +1750,13 @@ internal CalendarData GetCalendar(CalendarId calendarId) // arrays are 0 based, calendarIds are 1 based int calendarIndex = (int)calendarId - 1; + if (_calendars == null && this == Invariant) + { + // We do this lazily as opposed in CreateCultureWithInvariantData to avoid introducing + // invariant calendar data into apps that don't even need calendar. + _calendars = GetInvariantCalendarDataArray(); + } + // Have to have calendars _calendars ??= new CalendarData[CalendarData.MAX_CALENDARS]; From fd64912a53a612287fba55e8198cee167241d081 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Fri, 15 Aug 2025 01:26:53 -0700 Subject: [PATCH 3/3] Cleanup --- .../src/System/Globalization/CultureData.cs | 150 ++++++++---------- 1 file changed, 66 insertions(+), 84 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs index 41356f71618c35..9a870579f94be7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CultureData.cs @@ -543,34 +543,7 @@ internal static CultureInfo[] GetCultures(CultureTypes types) return GlobalizationMode.UseNls ? NlsEnumCultures(types) : IcuEnumCultures(types); } - private static CalendarId[] GetInvariantCalendarIdArray() => [CalendarId.GREGORIAN]; - private static CalendarData[] GetInvariantCalendarDataArray() - { - var calendars = new CalendarData[CalendarData.MAX_CALENDARS]; - calendars[0] = CalendarData.Invariant; - return calendars; - } - private static CultureData CreateCultureWithInvariantData() - { - CultureData invariant = CreateCultureWithInvariantDataWithoutCalendars(); - - // all available calendar type(s). The first one is the default calendar - invariant._waCalendars = GetInvariantCalendarIdArray(); - - if (!GlobalizationMode.InvariantNoLoad) - { - invariant._calendars = GetInvariantCalendarDataArray(); - } - - return invariant; - } - - // Calendar information is expensive size-wise, especially for AOT. - // CalendarData.Invariant is special cased in _waCalendars and _calendars - // accessors and gets initialized lazily. All other uses should use - // CreateCultureWithInvariantData above that populates calendars too. - private static CultureData CreateCultureWithInvariantDataWithoutCalendars() { // Make a new culturedata CultureData invariant = new CultureData(); @@ -652,6 +625,12 @@ private static CultureData CreateCultureWithInvariantDataWithoutCalendars() invariant._iFirstDayOfWeek = 0; // first day of week invariant._iFirstWeekOfYear = 0; // first week of year + // Calendar information is expensive size-wise, especially for AOT. + // CalendarData.Invariant is special cased in _waCalendars and _calendars + // accessors and gets initialized lazily. + // invariant._waCalendars + // invariant._calendars + // Text information invariant._iReadingLayout = 0; @@ -675,7 +654,7 @@ private static CultureData CreateCultureWithInvariantDataWithoutCalendars() /// Build our invariant information /// We need an invariant instance, which we build hard-coded /// - internal static CultureData Invariant => field ??= CreateCultureWithInvariantDataWithoutCalendars(); + internal static CultureData Invariant => field ??= CreateCultureWithInvariantData(); // Cache of cultures we've already looked up private static volatile Dictionary? s_cachedCultures; @@ -1664,67 +1643,68 @@ internal CalendarId[] CalendarIds { get { - if (_waCalendars == null && this == Invariant) - { - // We do this lazily as opposed in CreateCultureWithInvariantData to avoid introducing - // enum arrays into apps that otherwise wouldn't have them. This helps with size in AOT. - _waCalendars = GetInvariantCalendarIdArray(); - } - else if (_waCalendars == null && !GlobalizationMode.Invariant) + if (_waCalendars == null) { - // We pass in an array of ints, and native side fills it up with count calendars. - // We then have to copy that list to a new array of the right size. - // Default calendar should be first - CalendarId[] calendars = new CalendarId[23]; - Debug.Assert(_sWindowsName != null, "[CultureData.CalendarIds] Expected _sWindowsName to be populated by already"); - - int count = CalendarData.GetCalendarsCore(_sWindowsName, _bUseOverrides, calendars); - - // See if we had a calendar to add. - if (count == 0) + if (GlobalizationMode.Invariant || IsInvariantCulture) { - // Failed for some reason, just use Gregorian - _waCalendars = GetInvariantCalendarIdArray(); + // We do this lazily as opposed in CreateCultureWithInvariantData to avoid introducing + // enum arrays into apps that otherwise wouldn't have them. This helps with size in AOT. + _waCalendars = [CalendarId.GREGORIAN]; } else { - // The OS may not return calendar 4 for zh-TW, but we've always allowed it. - // TODO: Is this hack necessary long-term? - if (_sWindowsName == "zh-TW") - { - bool found = false; + // We pass in an array of ints, and native side fills it up with count calendars. + // We then have to copy that list to a new array of the right size. + // Default calendar should be first + CalendarId[] calendars = new CalendarId[23]; + Debug.Assert(_sWindowsName != null, "[CultureData.CalendarIds] Expected _sWindowsName to be populated by already"); + + int count = CalendarData.GetCalendarsCore(_sWindowsName, _bUseOverrides, calendars); - // Do we need to insert calendar 4? - for (int i = 0; i < count; i++) + // See if we had a calendar to add. + if (count == 0) + { + // Failed for some reason, just use Gregorian + _waCalendars = Invariant.CalendarIds; + } + else + { + // The OS may not return calendar 4 for zh-TW, but we've always allowed it. + // TODO: Is this hack necessary long-term? + if (_sWindowsName == "zh-TW") { - // Stop if we found calendar four - if (calendars[i] == CalendarId.TAIWAN) + bool found = false; + + // Do we need to insert calendar 4? + for (int i = 0; i < count; i++) { - found = true; - break; + // Stop if we found calendar four + if (calendars[i] == CalendarId.TAIWAN) + { + found = true; + break; + } } - } - // If not found then insert it - if (!found) - { - // Insert it as the 2nd calendar - count++; - // Copy them from the 2nd position to the end, -1 for skipping 1st, -1 for one being added. - Array.Copy(calendars, 1, calendars, 2, 23 - 1 - 1); - calendars[1] = CalendarId.TAIWAN; + // If not found then insert it + if (!found) + { + // Insert it as the 2nd calendar + count++; + // Copy them from the 2nd position to the end, -1 for skipping 1st, -1 for one being added. + Array.Copy(calendars, 1, calendars, 2, 23 - 1 - 1); + calendars[1] = CalendarId.TAIWAN; + } } - } - // It worked, remember the list - CalendarId[] temp = new CalendarId[count]; - Array.Copy(calendars, temp, count); - - _waCalendars = temp; + // It worked, remember the list + CalendarId[] temp = new CalendarId[count]; + Array.Copy(calendars, temp, count); + _waCalendars = temp; + } } } - - return _waCalendars!; + return _waCalendars; } } @@ -1750,13 +1730,6 @@ internal CalendarData GetCalendar(CalendarId calendarId) // arrays are 0 based, calendarIds are 1 based int calendarIndex = (int)calendarId - 1; - if (_calendars == null && this == Invariant) - { - // We do this lazily as opposed in CreateCultureWithInvariantData to avoid introducing - // invariant calendar data into apps that don't even need calendar. - _calendars = GetInvariantCalendarDataArray(); - } - // Have to have calendars _calendars ??= new CalendarData[CalendarData.MAX_CALENDARS]; @@ -1767,8 +1740,17 @@ internal CalendarData GetCalendar(CalendarId calendarId) // Make sure that calendar has data if (calendarData == null) { - Debug.Assert(_sWindowsName != null, "[CultureData.GetCalendar] Expected _sWindowsName to be populated by already"); - calendarData = new CalendarData(_sWindowsName, calendarId, _bUseOverrides); + if (calendarIndex == 0 && IsInvariantCulture) + { + // We do this lazily as opposed in CreateCultureWithInvariantData to avoid introducing + // invariant calendar data into apps that don't even need calendar. + calendarData = CalendarData.Invariant; + } + else + { + Debug.Assert(_sWindowsName != null, "[CultureData.GetCalendar] Expected _sWindowsName to be populated by already"); + calendarData = new CalendarData(_sWindowsName, calendarId, _bUseOverrides); + } _calendars[calendarIndex] = calendarData; }