Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace OPTIMIZE_FOR_SIZE with feature switch #111743

Merged
merged 10 commits into from
Jan 30, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,12 @@ public static int SlowRuntimeTimeoutModifier
public static bool IsStartingProcessesSupported => !IsiOS && !IstvOS;

public static bool IsSpeedOptimized => !IsSizeOptimized;
public static bool IsSizeOptimized => IsBrowser || IsWasi || IsAndroid || IsAppleMobile;
public static bool IsSizeOptimized => s_linqIsSizeOptimized.Value;
MichalStrehovsky marked this conversation as resolved.
Show resolved Hide resolved
private static readonly Lazy<bool> s_linqIsSizeOptimized = new Lazy<bool>(IsLinqSizeOptimized);
private static bool IsLinqSizeOptimized()
{
return (bool)typeof(Enumerable).GetMethod("get_IsSizeOptimized", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, Array.Empty<object>());
MichalStrehovsky marked this conversation as resolved.
Show resolved Hide resolved
}

public static bool IsBrowserDomSupported => IsEnvironmentVariableTrue("IsBrowserDomSupported");
public static bool IsBrowserDomSupportedOrNotBrowser => IsNotBrowser || IsBrowserDomSupported;
Expand Down
13 changes: 3 additions & 10 deletions src/libraries/System.Linq/src/System.Linq.csproj
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppCurrent)-browser;$(NetCoreAppCurrent)-wasi;$(NetCoreAppCurrent)-android;$(NetCoreAppCurrent)-ios;$(NetCoreAppCurrent)-tvos</TargetFrameworks>
<TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
MichalStrehovsky marked this conversation as resolved.
Show resolved Hide resolved
<UseCompilerGeneratedDocXmlFile>false</UseCompilerGeneratedDocXmlFile>
</PropertyGroup>

<!-- DesignTimeBuild requires all the TargetFramework Derived Properties to not be present in the first property group. -->
<PropertyGroup>
<TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
<OptimizeForSize Condition="'$(TargetPlatformIdentifier)' == 'browser' or '$(TargetPlatformIdentifier)' == 'android' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos'">true</OptimizeForSize>
Copy link
Member

@pavelsavara pavelsavara Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptimizeForSize was set true for some targets but it's not anymore.
WasmFeatures.props set the default for wasm workload, but default runtime pack also needs to be optimized for size - when people don't use workload in Blazor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OptimizeForSize was set true for some targets but it's not anymore.

Yes, that is the purpose of this PR. To only have one build of this library (#111743 (comment))

WasmFeatures.props set the default for wasm workload, but default runtime pack also needs to be optimized for size - when people don't use workload in Blazor.

Could you please point me to which .targets we need to update? Is it the Microsoft.NET.Sdk.BlazorWebAssembly.6_0.targets linked from #111743 (comment)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(To be clear, the OptimizeForSize property controls whether we build System.Linq assembly in a not-fully-compatible manner (see the tests I'm touching where it behaves differently) that has better size characteristics. This PR deletes that build and changes it to always build the compatible Linq implementation. It introduces a feature switch that allows switching to the not-fully-compatible mode. The default for the feature switch is disabled. It can be enabled where the tradeoff is worth it (I have PRs out enabling it in places, linked above). The advantage is that now if a customer runs into the incompatibility, they can just set a property and unblock themselves.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Folded the BlazorWebAssembly.targets change into dotnet/sdk#46375.

WasmFeatures.props change is already part of the PR here.

<DefineConstants Condition="'$(OptimizeForSize)' == 'true'">$(DefineConstants);OPTIMIZE_FOR_SIZE</DefineConstants>
</PropertyGroup>

<ItemGroup Condition="'$(OptimizeForSize)' == true">
<ItemGroup>
<Compile Include="System\Linq\Skip.SizeOpt.cs" />
<Compile Include="System\Linq\Take.SizeOpt.cs" />
</ItemGroup>

<ItemGroup Condition="'$(OptimizeForSize)' != true">
<ItemGroup>
MichalStrehovsky marked this conversation as resolved.
Show resolved Hide resolved
<Compile Include="System\Linq\AppendPrepend.SpeedOpt.cs" />
<Compile Include="System\Linq\Cast.SpeedOpt.cs" />
<Compile Include="System\Linq\Concat.SpeedOpt.cs" />
Expand Down
4 changes: 1 addition & 3 deletions src/libraries/System.Linq/src/System/Linq/AnyAll.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public static bool Any<TSource>(this IEnumerable<TSource> source)
return gc.Count != 0;
}

#if !OPTIMIZE_FOR_SIZE
if (source is Iterator<TSource> iterator)
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
{
int count = iterator.GetCount(onlyIfCheap: true);
if (count >= 0)
Expand All @@ -32,7 +31,6 @@ public static bool Any<TSource>(this IEnumerable<TSource> source)
iterator.TryGetFirst(out bool found);
return found;
}
#endif

if (source is ICollection ngc)
{
Expand Down
8 changes: 2 additions & 6 deletions src/libraries/System.Linq/src/System/Linq/Count.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@ public static int Count<TSource>(this IEnumerable<TSource> source)
return collectionoft.Count;
}

#if !OPTIMIZE_FOR_SIZE
if (source is Iterator<TSource> iterator)
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
{
return iterator.GetCount(onlyIfCheap: false);
}
#endif

if (source is ICollection collection)
{
Expand Down Expand Up @@ -115,8 +113,7 @@ public static bool TryGetNonEnumeratedCount<TSource>(this IEnumerable<TSource> s
return true;
}

#if !OPTIMIZE_FOR_SIZE
if (source is Iterator<TSource> iterator)
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
{
int c = iterator.GetCount(onlyIfCheap: true);
if (c >= 0)
Expand All @@ -125,7 +122,6 @@ public static bool TryGetNonEnumeratedCount<TSource>(this IEnumerable<TSource> s
return true;
}
}
#endif

if (source is ICollection collection)
{
Expand Down
8 changes: 2 additions & 6 deletions src/libraries/System.Linq/src/System/Linq/ElementAt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int i

bool found;
TSource? element =
#if !OPTIMIZE_FOR_SIZE
source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
#endif
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
TryGetElementAtNonIterator(source, index, out found);

if (!found)
Expand Down Expand Up @@ -123,9 +121,7 @@ public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, Index
}

return
#if !OPTIMIZE_FOR_SIZE
source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
#endif
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetElementAt(index, out found) :
TryGetElementAtNonIterator(source, index, out found);
}

Expand Down
4 changes: 4 additions & 0 deletions src/libraries/System.Linq/src/System/Linq/Enumerable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Linq
{
public static partial class Enumerable
{
[FeatureSwitchDefinition("System.Linq.Enumerable.IsSizeOptimized")]
internal static bool IsSizeOptimized { get; } = AppContext.TryGetSwitch("System.Linq.Enumerable.IsSizeOptimized", out bool isEnabled) ? isEnabled : false;

public static IEnumerable<TSource> AsEnumerable<TSource>(this IEnumerable<TSource> source) => source;

/// <summary>Returns an empty <see cref="IEnumerable{TResult}"/>.</summary>
Expand Down
4 changes: 1 addition & 3 deletions src/libraries/System.Linq/src/System/Linq/First.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source,
}

return
#if !OPTIMIZE_FOR_SIZE
source is Iterator<TSource> iterator ? iterator.TryGetFirst(out found) :
#endif
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetFirst(out found) :
TryGetFirstNonIterator(source, out found);
}

Expand Down
8 changes: 3 additions & 5 deletions src/libraries/System.Linq/src/System/Linq/Iterator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,9 @@ public Iterator<TSource> GetEnumerator()
/// <typeparam name="TResult">The type of the mapped items.</typeparam>
/// <param name="selector">The selector used to map each item.</param>
public virtual IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) =>
#if OPTIMIZE_FOR_SIZE
new IEnumerableSelectIterator<TSource, TResult>(this, selector);
#else
new IteratorSelectIterator<TSource, TResult>(this, selector);
#endif
!IsSizeOptimized
? new IteratorSelectIterator<TSource, TResult>(this, selector)
: new IEnumerableSelectIterator<TSource, TResult>(this, selector);


/// <summary>
Expand Down
4 changes: 1 addition & 3 deletions src/libraries/System.Linq/src/System/Linq/Last.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,7 @@ public static TSource LastOrDefault<TSource>(this IEnumerable<TSource> source, F
}

return
#if !OPTIMIZE_FOR_SIZE
source is Iterator<TSource> iterator ? iterator.TryGetLast(out found) :
#endif
!IsSizeOptimized && source is Iterator<TSource> iterator ? iterator.TryGetLast(out found) :
TryGetLastNonIterator(source, out found);
}

Expand Down
2 changes: 1 addition & 1 deletion src/libraries/System.Linq/src/System/Linq/Skip.SizeOpt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace System.Linq
{
public static partial class Enumerable
{
private static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
private static IEnumerable<TSource> SizeOptimizedSkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using IEnumerator<TSource> e = source.GetEnumerator();
while (count > 0 && e.MoveNext()) count--;
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/System.Linq/src/System/Linq/Skip.SpeedOpt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace System.Linq
{
public static partial class Enumerable
{
private static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count) =>
private static IEnumerable<TSource> SpeedOptimizedSkipIterator<TSource>(IEnumerable<TSource> source, int count) =>
source is IList<TSource> sourceList ?
(IEnumerable<TSource>)new IListSkipTakeIterator<TSource>(sourceList, count, int.MaxValue) :
new IEnumerableSkipTakeIterator<TSource>(source, count, -1);
Expand Down
6 changes: 2 additions & 4 deletions src/libraries/System.Linq/src/System/Linq/Skip.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ public static IEnumerable<TSource> Skip<TSource>(this IEnumerable<TSource> sourc

count = 0;
}
#if !OPTIMIZE_FOR_SIZE
else if (source is Iterator<TSource> iterator)
else if (!IsSizeOptimized && source is Iterator<TSource> iterator)
{
return iterator.Skip(count) ?? Empty<TSource>();
}
#endif

return SkipIterator(source, count);
return IsSizeOptimized ? SizeOptimizedSkipIterator(source, count) : SpeedOptimizedSkipIterator(source, count);
}

public static IEnumerable<TSource> SkipWhile<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
Expand Down
4 changes: 2 additions & 2 deletions src/libraries/System.Linq/src/System/Linq/Take.SizeOpt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace System.Linq
{
public static partial class Enumerable
{
private static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
private static IEnumerable<TSource> SizeOptimizedTakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
Debug.Assert(count > 0);

Expand All @@ -19,7 +19,7 @@ private static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> s
}
}

private static IEnumerable<TSource> TakeRangeIterator<TSource>(IEnumerable<TSource> source, int startIndex, int endIndex)
private static IEnumerable<TSource> SizeOptimizedTakeRangeIterator<TSource>(IEnumerable<TSource> source, int startIndex, int endIndex)
{
Debug.Assert(source is not null);
Debug.Assert(startIndex >= 0 && startIndex < endIndex);
Expand Down
4 changes: 2 additions & 2 deletions src/libraries/System.Linq/src/System/Linq/Take.SpeedOpt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace System.Linq
{
public static partial class Enumerable
{
private static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
private static IEnumerable<TSource> SpeedOptimizedTakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
Debug.Assert(source is not null && !IsEmptyArray(source));
Debug.Assert(count > 0);
Expand All @@ -19,7 +19,7 @@ private static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> s
new IEnumerableSkipTakeIterator<TSource>(source, 0, count - 1);
}

private static IEnumerable<TSource> TakeRangeIterator<TSource>(IEnumerable<TSource> source, int startIndex, int endIndex)
private static IEnumerable<TSource> SpeedOptimizedTakeRangeIterator<TSource>(IEnumerable<TSource> source, int startIndex, int endIndex)
{
Debug.Assert(source is not null && !IsEmptyArray(source));
Debug.Assert(startIndex >= 0 && startIndex < endIndex);
Expand Down
23 changes: 16 additions & 7 deletions src/libraries/System.Linq/src/System/Linq/Take.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> sourc
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}

return count <= 0 || IsEmptyArray(source) ?
[] :
TakeIterator(source, count);
if (count <= 0 || IsEmptyArray(source))
{
return [];
}

return IsSizeOptimized ? SizeOptimizedTakeIterator(source, count) : SpeedOptimizedTakeIterator(source, count);
}

/// <summary>Returns a specified range of contiguous elements from a sequence.</summary>
Expand Down Expand Up @@ -60,9 +63,12 @@ public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> sourc
}
else if (!isEndIndexFromEnd)
{
return startIndex >= endIndex ?
[] :
TakeRangeIterator(source, startIndex, endIndex);
if (startIndex >= endIndex)
{
return [];
}

return IsSizeOptimized ? SizeOptimizedTakeRangeIterator(source, startIndex, endIndex) : SpeedOptimizedTakeRangeIterator(source, startIndex, endIndex);
}

return TakeRangeFromEndIterator(source, isStartIndexFromEnd, startIndex, isEndIndexFromEnd, endIndex);
Expand All @@ -88,7 +94,10 @@ private static IEnumerable<TSource> TakeRangeFromEndIterator<TSource>(IEnumerabl

if (startIndex < endIndex)
{
foreach (TSource element in TakeRangeIterator(source, startIndex, endIndex))
IEnumerable<TSource> rangeIterator = IsSizeOptimized
? SizeOptimizedTakeRangeIterator(source, startIndex, endIndex)
: SpeedOptimizedTakeRangeIterator(source, startIndex, endIndex);
foreach (TSource element in rangeIterator)
{
yield return element;
}
Expand Down
8 changes: 2 additions & 6 deletions src/libraries/System.Linq/src/System/Linq/ToCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,10 @@ public static partial class Enumerable
{
public static TSource[] ToArray<TSource>(this IEnumerable<TSource> source)
{
#if !OPTIMIZE_FOR_SIZE
if (source is Iterator<TSource> iterator)
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
{
return iterator.ToArray();
}
#endif

if (source is ICollection<TSource> collection)
{
Expand Down Expand Up @@ -64,12 +62,10 @@ public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}

#if !OPTIMIZE_FOR_SIZE
if (source is Iterator<TSource> iterator)
if (!IsSizeOptimized && source is Iterator<TSource> iterator)
{
return iterator.ToList();
}
#endif

return new List<TSource>(source);
}
Expand Down
7 changes: 3 additions & 4 deletions src/libraries/System.Linq/tests/CountTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,11 @@ public static IEnumerable<object[]> NonEnumeratedCount_SupportedEnumerables()

yield return WrapArgs(0, Enumerable.Empty<string>());

yield return WrapArgs(100, Enumerable.Range(1, 100));
yield return WrapArgs(80, Enumerable.Repeat(1, 80));

if (PlatformDetection.IsSpeedOptimized)
{
yield return WrapArgs(100, Enumerable.Range(1, 100));
yield return WrapArgs(80, Enumerable.Repeat(1, 80));
yield return WrapArgs(50, Enumerable.Range(1, 50).Select(x => x + 1));
yield return WrapArgs(4, new int[] { 1, 2, 3, 4 }.Select(x => x + 1));
yield return WrapArgs(50, Enumerable.Range(1, 50).Select(x => x + 1).Select(x => x - 1));
Expand All @@ -202,8 +203,6 @@ public static IEnumerable<object[]> NonEnumeratedCount_UnsupportedEnumerables()

if (!PlatformDetection.IsSpeedOptimized)
{
yield return WrapArgs(Enumerable.Range(1, 100));
yield return WrapArgs(Enumerable.Repeat(1, 80));
yield return WrapArgs(Enumerable.Range(1, 50).Select(x => x + 1));
yield return WrapArgs(new int[] { 1, 2, 3, 4 }.Select(x => x + 1));
yield return WrapArgs(Enumerable.Range(1, 50).Select(x => x + 1).Select(x => x - 1));
Expand Down
Loading