Skip to content

Help with improving JIT for Range GetEnumerator #40770

@YairHalberstadt

Description

@YairHalberstadt

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

Metadata

Metadata

Assignees

Labels

area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMItenet-performancePerformance related issue

Type

No type
No fields configured for issues without a type.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions