Привет! Хочу рассказать об интересном опыте, как я писал енумератор для типа Range, который был бы таким же дешевым, как цикл for. У System.Range, как известно, очень красивый синтаксис создания: Поэтому, как эксперимент, я решил абузить его для цикла foreach, как замена for: (выводит 12345) Для foreach Розлин требует метод GetEnumerator, который возвращал бы что-то, у чего есть bool MoveNext() и T Current. Этот метод можно добавить как метод расширения: Вообще-то for это куда больше, чем пройтись по последовательности. Но так как очень часто мы видим то это стоит своего кейса использования for. Сравнивать мы будем с циклом for у которого в качестве итерации стоит i += inc: По производительности он идентичен версии с i++: Method Iterations Mean Error StdDev i++ 10000 3.380 us 0.0534 us 0.0446 us i += inc 10000 3.385 us 0.0575 us 0.0685 us Код метода с for-ом: (мы выгрузили все проперти в локальные переменные чтобы исключить чтение из памяти при итерации) Кодген for-а: Мы будем смотреть только на код одной итерации цикла, начальный оверхед и окружающий мусор нас не интересует. Конечно, сначала стоит попробовать вариант из BCL: Enumerable.Range, исходный код которого можно поизучать здесь. Так выглядит наш бенчмарк: Этот код Розлином раскрывается как Изучать ассемблерный кодген смысла нет, достаточно взглянуть на бенчмарк: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.448 us 0.0689 us 0.0896 us ForeachWithEnumerableRange 10000 43.946 us 0.8685 us 1.2456 us Второй по простоте способ - написать метод-генератор, и вызывать его енумератор: Принципиально нового мы ничего не получаем, наш енумератор это все еще огромный референс тип с кучей полей. Бенчмарки сообщают, что с yield-ом даже хуже, чем с Enumerable.Range: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.548 us 0.0661 us 0.1320 us ForeachWithEnumerableRange 10000 51.663 us 0.9103 us 1.2460 us ForeachWithYieldReturn 10000 56.580 us 0.3561 us 0.2780 us Теперь будем писать собственный енумератор. Чтобы оно работало с foreach-ем, нужно, чтобы был метод bool MoveNext() и свойство T Current (в нашем случае int Current). Чтобы Розлин увидел, что по System.Range можно форычиться, нужно сделать метод расширения: Код бенчмарка выглядит так: И раскрывается Розлином так: На этот раз нет референс типов, а так как RangeEnumerator не имплементит IDisposable, то и нет try-catch блоков вокруг. Взглянем на бенчмарк: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.477 us 0.0690 us 0.0989 us ForeachWithEnumerableRange 10000 50.501 us 0.9984 us 1.2627 us ForeachWithYieldReturn 10000 53.782 us 1.0583 us 0.9900 us ForeachWithRangeEnumerator 10000 18.061 us 0.1731 us 0.1619 us 18 микросекунд на 10000 операций. Намного лучше, чем аналоги до этого, но сильно хуже, чем for. Время взглянуть на кодген! Одна итерация цикла выглядит так: Да, у нас референс-типов нет, но все данные-то все равно в памяти! Хоть и на стеке.  Что за магия? Дело в том, что чтобы получить наш енумератор таким же быстрым, как for, следует поместить его поля в регистры. Но компилятор сам этого не сделал, слишком много локальных переменных в методе было, и он не положил нужные. Давайте облегчим задачу. Из эксперимента 3: (я заменил foreach на точно то же самое с постфиксом Raw, только написанное ручками, для наглядности. См. эксперимент 3, там показано, как Розлин раскрывает foreach). Чтобы уменьшить количество локальных переменных, спрячем цикл в локальную функцию: И побенчим: Method Iterations Mean Error StdDev ForWithCustomIncrement 10000 3.498 us 0.0658 us 0.1153 us ForeachWithEnumerableRange 10000 43.313 us 0.8207 us 1.0079 us ForeachWithYieldReturn 10000 56.963 us 1.0638 us 1.0448 us ForeachWithRangeEnumerator 10000 17.931 us 0.2047 us 0.1915 us ForeachWithRangeEnumeratorRaw 10000 17.932 us 0.1486 us 0.1390 us ForeachWithRangeEnumeratorRawWithLocal 10000 3.501 us 0.0678 us 0.0807 us ForeachWithRangeEnumeratorRawWithLocal - это как раз вариант с локальной функцией. И... он почти в шесть раз быстрее, чем заинлайненный! Как же так вышло-то? Смотрим кодген: В нашем методе локальная функция не заинлайнилась: И это нам на руку. Смотрим эту локальную функцию: Отмеченные четыре строчки, это и есть наша победа, так как этот код идентичен итерации for-а. Первые три строчки Выгружают все, что нужно, в регистры, и поэтому так быстро работает наш метод. Во-первых, хоть и желаемое получилось, в реальной жизни проще написать for, чем проверять, догадался ли компилятор положить ваш енумератор в регистры. Во-вторых, это очень показательный пример, что инлайнинг может сделать не только не лучше, но и сильно хуже (в данном случае он увеличил время работы с 3.5 до 18 us на 10000 итераций). Спасибо за внимание! UPD: поведение совершенно разное на винде и линуксе. Например, разбирая WithLocal метод-бенчмарк, у @DistortNeo получился совсем иной от винды кодген. Вот здесь я сделал гист с кодгеном метода WithLocal для винды и линукса. В случае последнего, джит заменил call на jmp, а локальные переменные локальной функции разместил на стеке. Весь код, который я писал, размещен в репозитории на github. Бенчмарки Код енумератора Тесты, если есть подозрение на неправильный код BenchmarkDotNet - утилита/библиотека для бенчинга Бенчилось на винде Open source contributor