From df8dd9598205f90cae3c8b8b855c207cf80054ad Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Tue, 13 Jun 2023 23:03:51 -0400 Subject: [PATCH 1/2] Extend Task.FromResult default task optimization to 16 byte unmanaged types For years, async methods / Task.FromResult has cached a `Task` for `default(T)`, however it was only used when the result value was null. Earlier in this release we extended that optimization to also use the default task when the value was 1, 2, 4, or 8 bytes (and not a reference type). This extends that further to also handle types that are 16 bytes, so as to include types like Decimal, Guid, Int128, and DateTimeOffset. --- .../src/System/Threading/Tasks/Task.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs index 22621aa85e26b7..cae33611d5d98c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs @@ -5297,13 +5297,15 @@ public static unsafe Task FromResult(TResult result) } else if (!RuntimeHelpers.IsReferenceOrContainsReferences()) { - // For other value types, we special-case default(TResult) if we can easily compare bit patterns to default/0. + // For other value types, we special-case default(TResult) if we can efficiently compare bit patterns to default/0, + // which means any value type that's 1, 2, 4, 8, or 16 bytes in size and that doesn't contain references. // We don't need to go through the equality operator of the TResult because we cached a task for default(TResult), // so we only need to confirm that this TResult has the same bits as default(TResult). if ((sizeof(TResult) == sizeof(byte) && *(byte*)&result == default(byte)) || (sizeof(TResult) == sizeof(ushort) && *(ushort*)&result == default(ushort)) || (sizeof(TResult) == sizeof(uint) && *(uint*)&result == default) || - (sizeof(TResult) == sizeof(ulong) && *(ulong*)&result == default)) + (sizeof(TResult) == sizeof(ulong) && *(ulong*)&result == default) || + (sizeof(TResult) == sizeof(UInt128) && *(UInt128*)&result == default)) { return Task.s_defaultResultTask; } From d80b4ca1f70a9ce058b799335e272f153f627338 Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Wed, 14 Jun 2023 11:25:09 -0400 Subject: [PATCH 2/2] Fix tests to accomodate additional use of default cached task --- .../AsyncTaskMethodBuilderTests.cs | 2 +- .../tests/Task/TaskRtTests.cs | 95 +++++++------------ 2 files changed, 36 insertions(+), 61 deletions(-) diff --git a/src/libraries/System.Threading.Tasks/tests/System.Runtime.CompilerServices/AsyncTaskMethodBuilderTests.cs b/src/libraries/System.Threading.Tasks/tests/System.Runtime.CompilerServices/AsyncTaskMethodBuilderTests.cs index acbd9bdc600f78..2ef2b23ce935d2 100644 --- a/src/libraries/System.Threading.Tasks/tests/System.Runtime.CompilerServices/AsyncTaskMethodBuilderTests.cs +++ b/src/libraries/System.Threading.Tasks/tests/System.Runtime.CompilerServices/AsyncTaskMethodBuilderTests.cs @@ -377,7 +377,7 @@ public static void TaskMethodBuilderInt32_UsesCompletedCache(int result, bool sh [Fact] public static void TaskMethodBuilderDecimal_DoesntUseCompletedCache() { - TaskMethodBuilderT_UsesCompletedCache(0m, shouldBeCached: false); + TaskMethodBuilderT_UsesCompletedCache(default(decimal), shouldBeCached: true); TaskMethodBuilderT_UsesCompletedCache(0.0m, shouldBeCached: false); TaskMethodBuilderT_UsesCompletedCache(42m, shouldBeCached: false); } diff --git a/src/libraries/System.Threading.Tasks/tests/Task/TaskRtTests.cs b/src/libraries/System.Threading.Tasks/tests/Task/TaskRtTests.cs index b2f69398af4414..f89f6d60d87305 100644 --- a/src/libraries/System.Threading.Tasks/tests/Task/TaskRtTests.cs +++ b/src/libraries/System.Threading.Tasks/tests/Task/TaskRtTests.cs @@ -470,70 +470,45 @@ public static void FromResult_KnownResults_Cached() // Cached - foreach (bool result in new[] { false, true }) - { - Assert.Same(Task.FromResult(result), Task.FromResult(result)); - Assert.Equal(result, Task.FromResult(result).Result); - } + AssertCached(false); + AssertCached(true); for (int i = -1; i <= 8; i++) { - Assert.Same(Task.FromResult(i), Task.FromResult(i)); - Assert.Equal(i, Task.FromResult(i).Result); + AssertCached(i); } - Assert.Same(Task.FromResult('\0'), Task.FromResult('\0')); - Assert.Equal('\0', Task.FromResult('\0').Result); - - Assert.Same(Task.FromResult((byte)0), Task.FromResult((byte)0)); - Assert.Equal(0, Task.FromResult((byte)0).Result); - - Assert.Same(Task.FromResult((ushort)0), Task.FromResult((ushort)0)); - Assert.Equal(0, Task.FromResult((ushort)0).Result); - - Assert.Same(Task.FromResult((uint)0), Task.FromResult((uint)0)); - Assert.Equal(0u, Task.FromResult((uint)0).Result); - - Assert.Same(Task.FromResult((ulong)0), Task.FromResult((ulong)0)); - Assert.Equal(0ul, Task.FromResult((ulong)0).Result); - - Assert.Same(Task.FromResult((sbyte)0), Task.FromResult((sbyte)0)); - Assert.Equal(0, Task.FromResult((sbyte)0).Result); - - Assert.Same(Task.FromResult((short)0), Task.FromResult((short)0)); - Assert.Equal(0, Task.FromResult((short)0).Result); - - Assert.Same(Task.FromResult((long)0), Task.FromResult((long)0)); - Assert.Equal(0, Task.FromResult((long)0).Result); - - Assert.Same(Task.FromResult(IntPtr.Zero), Task.FromResult(IntPtr.Zero)); - Assert.Equal(IntPtr.Zero, Task.FromResult(IntPtr.Zero).Result); - - Assert.Same(Task.FromResult(UIntPtr.Zero), Task.FromResult(UIntPtr.Zero)); - Assert.Equal(UIntPtr.Zero, Task.FromResult(UIntPtr.Zero).Result); - - Assert.Same(Task.FromResult((Half)default), Task.FromResult((Half)default)); - Assert.Equal((Half)default, Task.FromResult((Half)default).Result); - - Assert.Same(Task.FromResult((float)default), Task.FromResult((float)default)); - Assert.Equal((float)default, Task.FromResult((float)default).Result); - - Assert.Same(Task.FromResult((double)default), Task.FromResult((double)default)); - Assert.Equal((double)default, Task.FromResult((double)default).Result); - - Assert.Same(Task.FromResult((TimeSpan)default), Task.FromResult((TimeSpan)default)); - Assert.Equal((TimeSpan)default, Task.FromResult((TimeSpan)default).Result); - - Assert.Same(Task.FromResult((DateTime)default), Task.FromResult((DateTime)default)); - Assert.Equal((DateTime)default, Task.FromResult((DateTime)default).Result); - - Assert.Same(Task.FromResult((object)null), Task.FromResult((object)null)); - Assert.Null(Task.FromResult((object)null).Result); - - Assert.Same(Task.FromResult((string)null), Task.FromResult((string)null)); - Assert.Null(Task.FromResult((string)null).Result); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + AssertCached(); + + static void AssertCached(T value = default) + { + Assert.Same(Task.FromResult(value), Task.FromResult(value)); + Assert.Equal(value, Task.FromResult(value).Result); + } - // Not cached + // Not currently cached foreach (int i in new[] { -2, 9, int.MinValue, int.MaxValue }) { @@ -541,11 +516,11 @@ public static void FromResult_KnownResults_Cached() Assert.Equal(i, Task.FromResult(i).Result); } + // Should never return the same task + Assert.NotSame(Task.FromResult((double)(+0.0)), Task.FromResult((double)(-0.0))); Assert.NotSame(Task.FromResult((float)(+0.0)), Task.FromResult((float)(-0.0))); Assert.NotSame(Task.FromResult((Half)(+0.0)), Task.FromResult((Half)(-0.0))); - - Assert.NotSame(Task.FromResult((decimal)default), Task.FromResult((decimal)default)); } [Fact]