I'm using extension GetEnumerator (coming in C# 9) to implement foreach on Range. See https://github.com/YairHalberstadt/RangeForeach.
I'm using sharplab to check the jit output.
I want everybody to be able to rewrite their for loops to foreach loops without having to think about any potential performance impact. Therefore ideally we'd want M1 and M2 in:
public static class Program {
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void M1() {
foreach (var i in 1..10)
Console.WriteLine(i);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void M2() {
for (int i = 1; i<10; i++)
Console.WriteLine(i);
}
}
To have identical asm.
My current implementation is:
using System;
using System.Runtime.CompilerServices;
public static class Program {
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void M1() {
foreach (var i in 1..10)
Console.WriteLine(i);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static void M2() {
for (int i = 1; i<10; i++)
Console.WriteLine(i);
}
}
public static class RangeExtensions
{
public struct RangeEnumerator
{
public RangeEnumerator(int start, int end) => (Current, _end) = (start - 1, end);
public int Current { get; private set; }
private int _end;
public bool MoveNext() => ++Current < _end;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static RangeEnumerator GetEnumerator(this Range range)
{
if (range.Start.IsFromEnd || range.End.IsFromEnd)
ThrowIsFromEnd();
if (range.Start.Value > range.End.Value)
ThrowStartIsGreaterThanEnd();
return new RangeEnumerator(range.Start.Value, range.End.Value);
static void ThrowIsFromEnd() => throw new ArgumentException("range start and end must not be from end");
static void ThrowStartIsGreaterThanEnd() => throw new ArgumentException("start is greater than end");
}
}
Jit is:
Program.M1()
L0000: push ebp
L0001: mov ebp, esp
L0003: push edi
L0004: push esi
L0005: mov ecx, 1
L000a: mov eax, 0xa
L000f: cmp ecx, eax
L0011: jg short L0030
L0013: mov esi, 1
L0018: mov edi, 0xa
L001d: dec esi
L001e: jmp short L0027
L0020: mov ecx, esi
L0022: call System.Console.WriteLine(Int32)
L0027: inc esi
L0028: cmp esi, edi
L002a: jl short L0020
L002c: pop esi
L002d: pop edi
L002e: pop ebp
L002f: ret
L0030: call dword ptr [0x2622c9fc]
L0036: int3
Program.M2()
L0000: push ebp
L0001: mov ebp, esp
L0003: push esi
L0004: mov esi, 1
L0009: mov ecx, esi
L000b: call System.Console.WriteLine(Int32)
L0010: inc esi
L0011: cmp esi, 0xa
L0014: jl short L0009
L0016: pop esi
L0017: pop ebp
L0018: ret
To be honest I'm pretty impressed by how much the JIT is managing to do here, but it's still approximately twice as long.
The main thing that isn't getting optimized away is the if (range.Start.Value > range.End.Value) check.
Any ideas if I can optimize this further, or if the JIT might be able to do better here?
category:cq
theme:optimization
skill-level:expert
cost:large
I'm using extension GetEnumerator (coming in C# 9) to implement foreach on Range. See https://github.com/YairHalberstadt/RangeForeach.
I'm using sharplab to check the jit output.
I want everybody to be able to rewrite their for loops to foreach loops without having to think about any potential performance impact. Therefore ideally we'd want
M1andM2in:To have identical asm.
My current implementation is:
Jit is:
To be honest I'm pretty impressed by how much the JIT is managing to do here, but it's still approximately twice as long.
The main thing that isn't getting optimized away is the
if (range.Start.Value > range.End.Value)check.Any ideas if I can optimize this further, or if the JIT might be able to do better here?
category:cq
theme:optimization
skill-level:expert
cost:large