Skip to content

Commit

Permalink
Remove some allocation at "hello world" startup (#44469)
Browse files Browse the repository at this point in the history
* Specialize `EqualityComparer<string>.Default`

Removes ~80 allocations at startup.

* Avoid loading Encoding.Unicode just for CodePage

* Use fixed instead of GCHandle.Alloc in EventSource.Initialize

* Remove lock / lock object from EncodingProvider.AddProvider

* Remove lock object from AppDomain.cs

* Lazily allocate EventSource's m_createEventLock

It's only used on an error path.  We don't need to allocate it for each EventSource that's created.

* Avoid unnecessary CultureInfo access in derived TextWriters

SyncTextWriter already overrides FormatProvider, in which case the t.FormatProvider passed to the base will never be used, so this call is incurring a virtual dispatch for no benefit.  And NullTextWriter needn't access InvariantCulture and force it into existence if it isn't yet, as the formatting should never actually be used, and if it is, its FormatProvider override can supply the culture.

* Avoid allocating AssemblyLoadContext's dictionary if no direct interaction with ALC

AssemblyLoadContext.OnProcessExit gets called by EventSource, which in turn forces s_allContexts into existence in order to lock on it in order to enumerate all active contexts, and if there's been no interaction with AssemblyLoadContext, there won't be any to enumerate.  So delay allocate the object.

* Address PR feedback

* Call EventListener.DisposeOnShutdown from AppContext.OnProcessExit

Avoids the need to register with AppContext.ProcessExit, avoiding an EventHandler allocation, and avoids the need in the common case to fire AppContext.ProcessExit, which in turn avoids allocating an AppDomain and EventArgs if they weren't otherwise created, plus it avoids the delegate invocation.

* Update src/libraries/System.Private.CoreLib/src/System/Diagnostics/Tracing/EventSource.cs
  • Loading branch information
stephentoub committed Nov 11, 2020
1 parent e8bba0b commit aa04371
Show file tree
Hide file tree
Showing 11 changed files with 137 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,28 +120,33 @@ internal static object CreateDefaultEqualityComparer(Type type)
object? result = null;
var runtimeType = (RuntimeType)type;

// Specialize for byte so Array.IndexOf is faster.
if (type == typeof(byte))
{
// Specialize for byte so Array.IndexOf is faster.
result = new ByteEqualityComparer();
}
// If T implements IEquatable<T> return a GenericEqualityComparer<T>
else if (type == typeof(string))
{
// Specialize for string, as EqualityComparer<string>.Default is on the startup path
result = new GenericEqualityComparer<string>();
}
else if (type.IsAssignableTo(typeof(IEquatable<>).MakeGenericType(type)))
{
result = CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<int>), runtimeType);
// If T implements IEquatable<T> return a GenericEqualityComparer<T>
result = CreateInstanceForAnotherGenericParameter((RuntimeType)typeof(GenericEqualityComparer<string>), runtimeType);
}
// Nullable does not implement IEquatable<T?> directly because that would add an extra interface call per comparison.
// Instead, it relies on EqualityComparer<T?>.Default to specialize for nullables and do the lifted comparisons if T implements IEquatable.
else if (type.IsGenericType)
{
// Nullable does not implement IEquatable<T?> directly because that would add an extra interface call per comparison.
// Instead, it relies on EqualityComparer<T?>.Default to specialize for nullables and do the lifted comparisons if T implements IEquatable.
if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
result = TryCreateNullableEqualityComparer(runtimeType);
}
}
// The equality comparer for enums is specialized to avoid boxing.
else if (type.IsEnum)
{
// The equality comparer for enums is specialized to avoid boxing.
result = TryCreateEnumEqualityComparer(runtimeType);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ internal static extern unsafe int EventSetInformation(
long registrationHandle,
EVENT_INFO_CLASS informationClass,
void* eventInformation,
int informationLength);
uint informationLength);
}
}
14 changes: 10 additions & 4 deletions src/libraries/Common/src/System/Text/EncodingHelper.Windows.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Text;
using System.Diagnostics;

namespace System.Text
{
// If we find issues with this or if more libraries need this behavior we will revisit the solution.
internal static partial class EncodingHelper
{
/// <summary>Hardcoded Encoding.UTF8.CodePage to avoid accessing Encoding.Unicode and forcing it into existence unnecessarily.</summary>
private const int Utf8CodePage = 65001;

#if DEBUG
static EncodingHelper() => Debug.Assert(Utf8CodePage == Encoding.UTF8.CodePage);
#endif

// Since only a minimum set of encodings are available by default,
// Console encoding might not be available and require provider registering.
// To avoid encoding exception in Console APIs we fallback to OSEncoding.
Expand All @@ -27,12 +33,12 @@ internal static Encoding GetSupportedConsoleEncoding(int codepage)
{
int defaultEncCodePage = Encoding.GetEncoding(0).CodePage;

if ((defaultEncCodePage == codepage) || defaultEncCodePage != Encoding.UTF8.CodePage)
if (defaultEncCodePage == codepage || defaultEncCodePage != Utf8CodePage)
{
return Encoding.GetEncoding(codepage);
}

if (codepage != Encoding.UTF8.CodePage)
if (codepage != Utf8CodePage)
{
return new OSEncoding(codepage);
}
Expand Down
17 changes: 12 additions & 5 deletions src/libraries/System.Console/src/System/ConsolePal.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ namespace System
// Provides Windows-based support for System.Console.
internal static class ConsolePal
{
/// <summary>Hardcoded Encoding.Unicode.CodePage to avoid accessing Encoding.Unicode and forcing it into existence unnecessarily.</summary>
private const int UnicodeCodePage = 1200;

#if DEBUG
static ConsolePal() => Debug.Assert(UnicodeCodePage == Encoding.Unicode.CodePage);
#endif

private static IntPtr InvalidHandleValue => new IntPtr(-1);

/// <summary>Ensures that the console has been initialized for use.</summary>
Expand All @@ -29,19 +36,19 @@ public static Stream OpenStandardInput() =>
GetStandardFile(
Interop.Kernel32.HandleTypes.STD_INPUT_HANDLE,
FileAccess.Read,
useFileAPIs: Console.InputEncoding.CodePage != Encoding.Unicode.CodePage || Console.IsInputRedirected);
useFileAPIs: Console.InputEncoding.CodePage != UnicodeCodePage || Console.IsInputRedirected);

public static Stream OpenStandardOutput() =>
GetStandardFile(
Interop.Kernel32.HandleTypes.STD_OUTPUT_HANDLE,
FileAccess.Write,
useFileAPIs: Console.OutputEncoding.CodePage != Encoding.Unicode.CodePage || Console.IsOutputRedirected);
useFileAPIs: Console.OutputEncoding.CodePage != UnicodeCodePage || Console.IsOutputRedirected);

public static Stream OpenStandardError() =>
GetStandardFile(
Interop.Kernel32.HandleTypes.STD_ERROR_HANDLE,
FileAccess.Write,
useFileAPIs: Console.OutputEncoding.CodePage != Encoding.Unicode.CodePage || Console.IsErrorRedirected);
useFileAPIs: Console.OutputEncoding.CodePage != UnicodeCodePage || Console.IsErrorRedirected);

private static IntPtr InputHandle =>
Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_INPUT_HANDLE);
Expand Down Expand Up @@ -99,7 +106,7 @@ public static Encoding InputEncoding

public static void SetConsoleInputEncoding(Encoding enc)
{
if (enc.CodePage != Encoding.Unicode.CodePage)
if (enc.CodePage != UnicodeCodePage)
{
if (!Interop.Kernel32.SetConsoleCP(enc.CodePage))
throw Win32Marshal.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
Expand All @@ -113,7 +120,7 @@ public static Encoding OutputEncoding

public static void SetConsoleOutputEncoding(Encoding enc)
{
if (enc.CodePage != Encoding.Unicode.CodePage)
if (enc.CodePage != UnicodeCodePage)
{
if (!Interop.Kernel32.SetConsoleOutputCP(enc.CodePage))
throw Win32Marshal.GetExceptionForWin32Error(Marshal.GetLastWin32Error());
Expand Down
10 changes: 7 additions & 3 deletions src/libraries/System.Private.CoreLib/src/System/AppContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Diagnostics.Tracing;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Runtime.Loader;
Expand Down Expand Up @@ -64,14 +64,18 @@ public static void SetData(string name, object? data)
#pragma warning disable CS0067 // events raised by the VM
public static event UnhandledExceptionEventHandler? UnhandledException;

public static event System.EventHandler<FirstChanceExceptionEventArgs>? FirstChanceException;
public static event EventHandler<FirstChanceExceptionEventArgs>? FirstChanceException;
#pragma warning restore CS0067

public static event System.EventHandler? ProcessExit;
public static event EventHandler? ProcessExit;

internal static void OnProcessExit()
{
AssemblyLoadContext.OnProcessExit();
if (EventSource.IsSupported)
{
EventListener.DisposeOnShutdown();
}

ProcessExit?.Invoke(AppDomain.CurrentDomain, EventArgs.Empty);
}
Expand Down
11 changes: 3 additions & 8 deletions src/libraries/System.Private.CoreLib/src/System/AppDomain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ namespace System
public sealed partial class AppDomain : MarshalByRefObject
{
private static readonly AppDomain s_domain = new AppDomain();
private readonly object _forLock = new object();
private IPrincipal? _defaultPrincipal;
private PrincipalPolicy _principalPolicy = PrincipalPolicy.NoPrincipal;
private Func<IPrincipal>? s_getWindowsPrincipal;
Expand Down Expand Up @@ -272,14 +271,10 @@ public void SetThreadPrincipal(IPrincipal principal)
throw new ArgumentNullException(nameof(principal));
}

lock (_forLock)
// Set the principal while checking it has not been set previously.
if (Interlocked.CompareExchange(ref _defaultPrincipal, principal, null) is not null)
{
// Check that principal has not been set previously.
if (_defaultPrincipal != null)
{
throw new SystemException(SR.AppDomain_Policy_PrincipalTwice);
}
_defaultPrincipal = principal;
throw new SystemException(SR.AppDomain_Policy_PrincipalTwice);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ private void EventUnregister(long registrationHandle) =>

internal unsafe int SetInformation(
Interop.Advapi32.EVENT_INFO_CLASS eventInfoClass,
IntPtr data,
void* data,
uint dataSize)
{
int status = Interop.Errors.ERROR_NOT_SUPPORTED;
Expand All @@ -1247,8 +1247,8 @@ internal unsafe int SetInformation(
status = Interop.Advapi32.EventSetInformation(
m_regHandle,
eventInfoClass,
(void*)data,
(int)dataSize);
data,
dataSize);
}
catch (TypeLoadException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public partial class EventSource : IDisposable
#pragma warning restore CA1823
#endif //FEATURE_EVENTSOURCE_XPLAT

private static bool IsSupported { get; } = InitializeIsSupported();
internal static bool IsSupported { get; } = InitializeIsSupported();

private static bool InitializeIsSupported() =>
AppContext.TryGetSwitch("System.Diagnostics.Tracing.EventSource.IsSupported", out bool isSupported) ? isSupported : true;
Expand Down Expand Up @@ -1508,17 +1508,13 @@ private unsafe void Initialize(Guid eventSourceGuid, string eventSourceName, str
if (this.Name != "System.Diagnostics.Eventing.FrameworkEventSource" || Environment.IsWindows8OrAbove)
#endif
{
int setInformationResult;
System.Runtime.InteropServices.GCHandle metadataHandle =
System.Runtime.InteropServices.GCHandle.Alloc(this.providerMetadata, System.Runtime.InteropServices.GCHandleType.Pinned);
IntPtr providerMetadata = metadataHandle.AddrOfPinnedObject();

setInformationResult = m_etwProvider.SetInformation(
Interop.Advapi32.EVENT_INFO_CLASS.SetTraits,
providerMetadata,
(uint)this.providerMetadata.Length);

metadataHandle.Free();
fixed (byte* providerMetadata = this.providerMetadata)
{
m_etwProvider.SetInformation(
Interop.Advapi32.EVENT_INFO_CLASS.SetTraits,
providerMetadata,
(uint)this.providerMetadata.Length);
}
}
#endif // TARGET_WINDOWS
#endif // FEATURE_MANAGED_ETW
Expand Down Expand Up @@ -2241,6 +2237,11 @@ private unsafe void WriteEventString(string msgString)
{
if (m_writeEventStringEventHandle == IntPtr.Zero)
{
if (m_createEventLock is null)
{
Interlocked.CompareExchange(ref m_createEventLock, new object(), null);
}

lock (m_createEventLock)
{
if (m_writeEventStringEventHandle == IntPtr.Zero)
Expand Down Expand Up @@ -3771,7 +3772,7 @@ private bool SelfDescribingEvents
private volatile OverideEventProvider m_etwProvider = null!; // This hooks up ETW commands to our 'OnEventCommand' callback
#endif
#if FEATURE_PERFTRACING
private object m_createEventLock = new object();
private object? m_createEventLock;
private IntPtr m_writeEventStringEventHandle = IntPtr.Zero;
private volatile OverideEventProvider m_eventPipeProvider = null!;
#endif
Expand Down Expand Up @@ -4116,18 +4117,17 @@ internal static void AddEventSource(EventSource newEventSource)
{
lock (EventListenersLock)
{
s_EventSources ??= new List<WeakReference<EventSource>>(2);
Debug.Assert(s_EventSources != null);

#if ES_BUILD_STANDALONE
// netcoreapp build calls DisposeOnShutdown directly from AppContext.OnProcessExit
if (!s_EventSourceShutdownRegistered)
{
s_EventSourceShutdownRegistered = true;
#if ES_BUILD_STANDALONE
AppDomain.CurrentDomain.ProcessExit += DisposeOnShutdown;
AppDomain.CurrentDomain.DomainUnload += DisposeOnShutdown;
#else
AppContext.ProcessExit += DisposeOnShutdown;
#endif
}
#endif

// Periodically search the list for existing entries to reuse, this avoids
// unbounded memory use if we keep recycling eventSources (an unlikely thing).
Expand Down Expand Up @@ -4184,8 +4184,14 @@ internal static void AddEventSource(EventSource newEventSource)
// such callbacks on process shutdown or appdomain so that unmanaged code will never
// do this. This is what this callback is for.
// See bug 724140 for more
#if ES_BUILD_STANDALONE
private static void DisposeOnShutdown(object? sender, EventArgs e)
#else
internal static void DisposeOnShutdown()
#endif
{
Debug.Assert(EventSource.IsSupported);

lock (EventListenersLock)
{
Debug.Assert(s_EventSources != null);
Expand Down Expand Up @@ -4420,10 +4426,12 @@ private void CallBackForExistingEventSources(bool addToListenersList, EventHandl
private static bool s_ConnectingEventSourcesAndListener;
#endif

#if ES_BUILD_STANDALONE
/// <summary>
/// Used to register AD/Process shutdown callbacks.
/// </summary>
private static bool s_EventSourceShutdownRegistered;
#endif
#endregion
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -703,10 +703,12 @@ public virtual Task FlushAsync()

private sealed class NullTextWriter : TextWriter
{
internal NullTextWriter() : base(CultureInfo.InvariantCulture)
internal NullTextWriter()
{
}

public override IFormatProvider FormatProvider => CultureInfo.InvariantCulture;

public override Encoding Encoding => Encoding.Unicode;

public override void Write(char[] buffer, int index, int count)
Expand Down Expand Up @@ -748,7 +750,7 @@ internal sealed class SyncTextWriter : TextWriter, IDisposable
{
private readonly TextWriter _out;

internal SyncTextWriter(TextWriter t) : base(t.FormatProvider)
internal SyncTextWriter(TextWriter t)
{
_out = t;
}
Expand Down
Loading

0 comments on commit aa04371

Please sign in to comment.