Skip to content

Commit

Permalink
Merge 624a85b into b61b836
Browse files Browse the repository at this point in the history
  • Loading branch information
neon-sunset committed Dec 23, 2022
2 parents b61b836 + 624a85b commit c47bab7
Show file tree
Hide file tree
Showing 29 changed files with 1,966 additions and 380 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/dotnet-releaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ jobs:
dotnet-version: |
3.1.x
6.0.x
7.0.x
- name: Build, Test, Pack, Publish
shell: bash
run: |
Expand All @@ -41,6 +42,7 @@ jobs:
dotnet-version: |
3.1.x
6.0.x
7.0.x
- name: Build, Test, Pack, Publish
shell: pwsh
run: |
Expand Down
100 changes: 66 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# RangeExtensions
[![CI/CD](https://github.com/neon-sunset/RangeExtensions/actions/workflows/dotnet-releaser.yml/badge.svg)](https://github.com/neon-sunset/RangeExtensions/actions/workflows/dotnet-releaser.yml) [![nuget](https://badgen.net/nuget/v/RangeExtensions/latest)](https://www.nuget.org/packages/RangeExtensions/) [![Coverage Status](https://coveralls.io/repos/github/neon-sunset/RangeExtensions/badge.svg)](https://coveralls.io/github/neon-sunset/RangeExtensions)

This package allows you to use `0..100` in `foreach` expressions and implements `RangeEnumerable` that supports a variety of LINQ-like operations as well as `ICollection<int>` and `IEnumerable<int>`.
- Correctness is verified against standard `IEnumerable<int>` and `Enumerable.Range` behavior;
- Performance is hand tuned to produce efficient native code with no allocations as long as `RangeEnumerable` isn't boxed (same applies to enumerator). However, even when boxed it is still faster than `Enumerable.Range`.
This package enables the usage of `System.Range` in `foreach` expressions and provides extensions to integrate it with LINQ as a faster replacement to `Enumerable.Range`.

- Correctness is verified against `IEnumerable<int>` and `Enumerable.Range` behavior;
- Implementation tries its best to make abstractions either zero-cost or reasonably close to that. For critical paths, performance is tuned to be allocation-free and on par with regular `for` loops

## Features
### Range enumeration
Expand All @@ -28,54 +29,85 @@ foreach (var i in 100..0)
}
```

### Select and Where
```cs
var floats = (0..100).Select(i => (float)i);
var odd = (0..100).Where(i => i % 2 != 0);

var randomNumbers = (0..1000)
.Select(_ => Random.Shared.Next())
.ToArray();
```

### Collecting to array or list
```cs
var numbers = (0..100).ToArray();
```

### `IEnumerable<int>` (some methods have bespoke implementations for performance)
### Aggregate
```cs
var digits = (0..10)
.Aggregate(new StringBuilder(), (sb, i) => sb.Append(i))
.ToString();

Assert.Equal("0123456789", digits);
```

### Other LINQ specializations
```cs
var enumerable = (..100).AsEnumerable();

var sum = enumerable.Sum();
var count = enumerable.Count();
var count = enumerable.Count;
var average = enumerable.Average();
var firstTen = enumerable.Take(10);
var reversed = enumerable.Reverse();
// and others.
// and others
```

## Performance
In short: 10x fast vs `Enumerable.Range()` and as fast as a plain `for` loop (there's small fixed overhead to check range correctness).
Tl;Dr: In .NET 7, `foreach (var i in 0..Length)` has the same performance as `for` loop. In other scenarios, `RangeExtensions` is 2-10x faster than `Enumerable.Range` (DynamicPGO helps the latter quite a bit)
``` ini

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-rc.1.22363.32
[Host] : .NET 7.0.0 (7.0.22.36203), X64 RyuJIT
ShortRun : .NET 7.0.0 (7.0.22.36203), X64 RyuJIT
BenchmarkDotNet=v0.13.2, OS=macOS 13.1 (22C65) [Darwin 22.2.0]
Apple M1 Pro, 1 CPU, 8 logical and 8 physical cores
.NET SDK=8.0.100-alpha.1.22620.11
[Host] : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD
DefaultJob : .NET 7.0.1 (7.0.122.56804), Arm64 RyuJIT AdvSIMD

Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
WarmupCount=3

DOTNET_TieredPGO=1
DOTNET_ReadyToRun=0
```
| Method | Length | Mean | Error | StdDev | Ratio | RatioSD | Code Size | Gen 0 | Allocated |
|------------------ |--------- |-----------------:|---------------:|--------------:|------:|--------:|----------:|-------:|----------:|
| **For** | **100** | **23.94 ns** | **0.556 ns** | **0.030 ns** | **1.00** | **0.00** | **20 B** | **-** | **-** |
| Range | 100 | 24.91 ns | 0.138 ns | 0.008 ns | 1.04 | 0.00 | 65 B | - | - |
| RangeReverse | 100 | 27.40 ns | 0.245 ns | 0.013 ns | 1.14 | 0.00 | 65 B | - | - |
| Enumerable.Range | 100 | 269.46 ns | 52.032 ns | 2.852 ns | 11.25 | 0.13 | 322 B | 0.0024 | 40 B |
|Range.AsEnumerable | 100 | 24.92 ns | 0.522 ns | 0.029 ns | 1.04 | 0.00 | 67 B | - | - |
| | | | | | | | | | |
| **For** | **10000** | **2,085.24 ns** | **300.295 ns** | **16.460 ns** | **1.00** | **0.00** | **20 B** | **-** | **-** |
| Range | 10000 | 2,085.39 ns | 308.278 ns | 16.898 ns | 1.00 | 0.00 | 65 B | - | - |
| RangeReverse | 10000 | 2,078.58 ns | 81.149 ns | 4.448 ns | 1.00 | 0.01 | 65 B | - | - |
| Enumerable.Range | 10000 | 27,364.70 ns | 616.148 ns | 33.773 ns | 13.12 | 0.11 | 322 B | - | 40 B |
|Range.AsEnumerable | 10000 | 2,104.25 ns | 464.044 ns | 25.436 ns | 1.01 | 0.01 | 67 B | - | - |
| | | | | | | | | | |
| **For** | **10000000** | **2,086,119.92 ns** | **289,496.016 ns** | **15,868.253 ns** | **1.00** | **0.00** | **20 B** | **-** | **-** |
| Range | 10000000 | 2,086,358.07 ns | 335,673.174 ns | 18,399.379 ns | 1.00 | 0.02 | 65 B | - | - |
| RangeReverse | 10000000 | 2,083,810.55 ns | 342,667.388 ns | 18,782.756 ns | 1.00 | 0.01 | 65 B | - | - |
| Enumerable.Range | 10000000 | 27,263,256.25 ns | 396,121.214 ns | 21,712.740 ns | 13.07 | 0.09 | 322 B | - | - |
|Range.AsEnumerable | 10000000 | 2,075,666.41 ns | 45,777.672 ns | 2,509.229 ns | 1.00 | 0.01 | 67 B | - | - |

| Method | Length | Mean | Error | Ratio | Allocated |
|---------------------- |------- |----------------:|--------------:|------:|----------:|
| For | 1 | 0.0000 ns | 0.0000 ns | ? | - |
| **Range** | 1 | 0.6531 ns | 0.0051 ns | ? | - |
| EnumerableRange | 1 | 8.1135 ns | 0.0198 ns | ? | 40 B |
| **RangeSelect** | 1 | 1.1588 ns | 0.0048 ns | ? | - |
| EnumerableSelect | 1 | 40.7948 ns | 0.7697 ns | ? | 88 B |
| **RangeSelectTwice** | 1 | 19.4165 ns | 0.0480 ns | ? | 96 B |
| EnumerableSelectTwice | 1 | 43.6399 ns | 0.0908 ns | ? | 232 B |
| **RangeWhere** | 1 | 1.3954 ns | 0.0036 ns | ? | - |
| EnumerableWhere | 1 | 26.1945 ns | 0.0534 ns | ? | 96 B |
| | | | | | |
| For | 100 | 36.1897 ns | 0.0618 ns | 1.00 | - |
| **Range** | 100 | 36.9244 ns | 2.0789 ns | 1.00 | - |
| EnumerableRange | 100 | 211.4774 ns | 0.6430 ns | 5.85 | 40 B |
| **RangeSelect** | 100 | 42.6852 ns | 0.1689 ns | 1.18 | - |
| EnumerableSelect | 100 | 235.7174 ns | 0.5161 ns | 6.51 | 88 B |
| **RangeSelectTwice** | 100 | 110.1340 ns | 0.1667 ns | 3.04 | 96 B |
| EnumerableSelectTwice | 100 | 298.2976 ns | 2.7831 ns | 8.22 | 232 B |
| **RangeWhere** | 100 | 56.1455 ns | 0.1701 ns | 1.55 | - |
| EnumerableWhere | 100 | 249.1264 ns | 1.5890 ns | 6.89 | 96 B |
| | | | | | |
| For | 100000 | 31,167.5682 ns | 37.3266 ns | 1.00 | - |
| **Range** | 100000 | 31,173.8688 ns | 13.0666 ns | 1.00 | - |
| EnumerableRange | 100000 | 212,925.2827 ns | 156.8182 ns | 6.83 | 40 B |
| **RangeSelect** | 100000 | 50,086.3342 ns | 39.0657 ns | 1.61 | - |
| EnumerableSelect | 100000 | 204,113.5813 ns | 100.0221 ns | 6.55 | 88 B |
| **RangeSelectTwice** | 100000 | 94,302.1444 ns | 230.6254 ns | 3.02 | 96 B |
| EnumerableSelectTwice | 100000 | 203,946.9247 ns | 908.5243 ns | 6.56 | 232 B |
| **RangeWhere** | 100000 | 47,165.0569 ns | 36.7208 ns | 1.51 | - |
| EnumerableWhere | 100000 | 209,918.1519 ns | 3,298.4418 ns | 6.76 | 96 B |
4 changes: 2 additions & 2 deletions RangeExtensions.Benchmarks/EnumerableExtras.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ namespace RangeExtensions.Benchmarks;
[DisassemblyDiagnoser(maxDepth: 5, exportCombinedDisassemblyReport: true)]
public class EnumerableExtras
{
[Params(1, 10, 100, 10000)]
public int Length;
// [Params(1, 10, 100, 10000)]
public const int Length = 1000;

[Benchmark] public bool RangeAny() => Range(Length).Any();

Expand Down
86 changes: 80 additions & 6 deletions RangeExtensions.Benchmarks/ForEach.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace RangeExtensions.Benchmarks;

// [ShortRunJob(RuntimeMoniker.Net48)]
[ShortRunJob]
[MemoryDiagnoser]
[DisassemblyDiagnoser(maxDepth: 5, exportCombinedDisassemblyReport: true)]
[HideColumns("StdDev", "Gen0", "Alloc Ratio")]
// [DisassemblyDiagnoser(maxDepth: 2, exportCombinedDisassemblyReport: true)]
public class ForEach
{
[Params(10, 100, 100_000)]
[Params(1, 100, 100_000)]
public int Length;

[Benchmark(Baseline = true)]
Expand All @@ -28,7 +26,7 @@ public int For()
public int Range()
{
var ret = 0;
foreach (var i in ..Length)
foreach (var i in 0..Length)
{
ret += i;
}
Expand All @@ -47,4 +45,80 @@ public int EnumerableRange()

return ret;
}

[Benchmark]
public int RangeSelect()
{
var ret = 0;
foreach (var i in (..Length).Select(i => i * 2))
{
ret += i;
}

return ret;
}

[Benchmark]
public int EnumerableSelect()
{
var ret = 0;
foreach (var i in Enumerable.Range(0, Length).Select(i => i * 2))
{
ret += i;
}

return ret;
}

[Benchmark]
public int RangeSelectTwice()
{
var ret = 0;
foreach (var i in (..Length)
.Select(i => i * 2)
.Select(i => i * 2))
{
ret += i;
}

return ret;
}

[Benchmark]
public int EnumerableSelectTwice()
{
var ret = 0;
foreach (var i in Enumerable.Range(0, Length)
.Select(i => i * 2)
.Select(i => i * 2))
{
ret += i;
}

return ret;
}

[Benchmark]
public int RangeWhere()
{
var ret = 0;
foreach (var i in (..Length).Where(i => i % 2 is 0))
{
ret += i;
}

return ret;
}

[Benchmark]
public int EnumerableWhere()
{
var ret = 0;
foreach (var i in Enumerable.Range(0, Length).Where(i => i % 2 is 0))
{
ret += i;
}

return ret;
}
}
3 changes: 1 addition & 2 deletions RangeExtensions.Benchmarks/RangeExtensions.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6.0</TargetFrameworks>
<LangVersion>10</LangVersion>
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
Expand Down
45 changes: 45 additions & 0 deletions RangeExtensions.Benchmarks/SpeedOpt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using BenchmarkDotNet.Attributes;

namespace RangeExtensions.Benchmarks;

[ShortRunJob]
[MemoryDiagnoser]
public class SpeedOpt
{
public const int Length = 1000;

[Benchmark]
public long RangeAggregate() => (0..Length)
.Select(i => (long)i)
.Aggregate(0L, (acc, i) => acc + i);

[Benchmark]
public long EnumerableAggregate() => Enumerable.Range(0, Length)
.Select(i => (long)i)
.Aggregate(0L, (acc, i) => acc + i);

[Benchmark]
public int RangeWhereLast() => (0..Length)
.Where(i => i % 64 is 0)
.Last();

[Benchmark]
public int EnumerableWhereLast() => Enumerable
.Range(0, Length)
.Last(i => i % 64 is 0);

[Benchmark]
public long RangeIndex() => (0..Length)
.Select(i => (long)i)[Length - 16];

[Benchmark]
public long RangeElementAt() => (0..Length)
.Select(i => (long)i)
.ElementAt(Length - 16);

[Benchmark]
public long EnumerableElementAt() => Enumerable
.Range(0, Length)
.Select(i => (long)i)
.ElementAt(Length - 16);
}
16 changes: 14 additions & 2 deletions RangeExtensions.Tests/Data.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,25 @@ public static IEnumerable<object[]> EmptyRanges()
public static IEnumerable<int> Indexes(ICollection<int> collection)
{
yield return 0;
yield return Math.Min(collection.Count, 1);
yield return 1;
yield return 2;
yield return -1;
yield return -2;
yield return collection.Count / 4;
yield return collection.Count / 3;
yield return collection.Count / 2;
yield return Math.Max(collection.Count - 1, 0);
yield return collection.Count - 1;
yield return collection.Count;
yield return collection.Count + 1;
yield return int.MinValue;
yield return int.MaxValue;
}

public static IEnumerable<int> ValidIndexes(ICollection<int> collection) =>
Indexes(collection)
.Where(index => index >= 0 && index < collection.Count);

public static IEnumerable<int> InvalidIndexes(ICollection<int> collection) =>
Indexes(collection)
.Where(index => index < 0 || index >= collection.Count);
}
4 changes: 1 addition & 3 deletions RangeExtensions.Tests/RangeEnumerable.ICollection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;

namespace RangeExtensions.Tests;
namespace RangeExtensions.Tests;

public partial class RangeEnumerableTests
{
Expand Down

0 comments on commit c47bab7

Please sign in to comment.