MEASUREMENT
bench/goiter/runs/mechanics.txt
The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.
- Cited in
- /en/go/iteration/range-over-func
The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.
Record of the run
This measurement has no recorded run — only the script.
Script
175 linesgo1.24.7 linux/amd64 | цикл по функции изнутри
ни одного измерения времени: проверяется порядок, а не скорость
1. ЦИКЛ ПО ФУНКЦИИ — ЭТО ВЫЗОВ ЭТОЙ ФУНКЦИИ
------------------------------------------------------------------
for v := range talking { ... }
итератор: начал работу
итератор: зову yield(0)
тело цикла: получил 0
итератор: yield вернул true
итератор: зову yield(1)
тело цикла: получил 1
итератор: yield вернул true
итератор: зову yield(2)
тело цикла: получил 2
итератор: yield вернул true
итератор: значения кончились, возвращаюсь сам
Печать итератора и печать тела чередуются: тело выполнилось
МЕЖДУ двумя строками итератора. Так может быть только в
одном случае — если тело вызвал сам итератор.
Стек вызовов из тела двух вложенных циклов по функциям
(внешний идёт по one, внутренний по other):
0 main.frames
1 main.block1.one.block1-range2-range4
2 main.other
3 main.block1-range2
4 main.one
5 main.block1
6 main.main
7 runtime.main
8 runtime.goexit
Читать снизу вверх. Кадры чередуются: итератор, тело,
итератор, тело. У тела есть собственное имя, и придумал его
компилятор — суффикс -rangeN и означает «тело N-го цикла по
функции в этой функции».
2. BREAK — ЭТО RETURN FALSE, CONTINUE — ЭТО RETURN TRUE
------------------------------------------------------------------
тело доработало до конца шаг 0: yield вернул true
тело доработало до конца шаг 1: yield вернул true
тело доработало до конца шаг 2: yield вернул true
тело сделало continue шаг 0: yield вернул true
тело сделало continue шаг 1: yield вернул true
тело сделало continue шаг 2: yield вернул true
тело сделало break шаг 0: yield вернул false
Итератор не знает слов break и continue: он видит только
булев результат yield. Всё управление циклом проходит
через это единственное значение.
3. RETURN ИЗ ТЕЛА НЕ ВЫХОДИТ НЕМЕДЛЕННО
------------------------------------------------------------------
Вызываем функцию, у которой внутри цикла стоит return:
итератор: открыл ресурс
тело: получил 0
тело: получил 1
тело: получил 2
тело: делаю return
итератор: yield вернул false, возвращаюсь
итератор: ЗАКРЫЛ ресурс (defer)
функция вернула: "значение из тела цикла"
Порядок строк — главное здесь. После слова «делаю return»
управление НЕ ушло из функции: сначала yield вернул false,
итератор доработал и отработал свой defer, и только потом
вернулась вызывающая функция.
Иначе и быть не может: тело — это функция, из неё нельзя
вернуться за вызывающего. return в теле означает «останови
цикл и запомни, что после него надо выйти»; сам выход
происходит уже после того, как итератор вернул управление.
Практическое следствие: уборка в defer у итератора
выполняется при ЛЮБОМ выходе из тела, включая return и
панику. Файл будет закрыт.
4. ПАНИКА ПРИХОДИТ К ИТЕРАТОРУ НЕ ТАК, КАК RETURN
------------------------------------------------------------------
Тот же опыт, что в блоке 3, с двумя отличиями: у итератора
есть строка ПОСЛЕ yield, а тело вместо return паникует.
итератор: открыл ресурс
тело: паникую
итератор: ЗАКРЫЛ ресурс (defer)
вызывающий: поймал панику: паника тела
Строки «ПОСЛЕ yield» в выводе нет: при панике yield не
возвращает управление вовсе — паника разматывает кадр
итератора насквозь. Из кода итератора выполняются только
его defer.
Отсюда правило: уборку класть в defer, а не в строку после
yield. Строка после yield при панике тела не выполнится,
defer выполнится и там и там.
5. ЧЕТЫРЕ ОШИБКИ ИТЕРАТОРА, КОТОРЫЕ ЛОВИТ РАНТАЙМ
------------------------------------------------------------------
yield после break: runtime error: range function continued iteration after function for loop body returned false
yield после паники: runtime error: range function continued iteration after loop body panic
yield после цикла: runtime error: range function continued iteration after whole loop exit
итератор съел панику: runtime error: range function recovered a loop body panic and did not resume panicking
Все четыре — не ошибки компиляции, а проверки во время
работы: цикл держит при себе переменную состояния и сверяет
с ней каждый вход в тело. Поэтому итератор, который
сохранил yield и зовёт его позже или из другой горутины,
падает предсказуемо, а не портит данные молча.
Четвёртая — самая неочевидная. Итератор с общим
defer recover() выглядит как разумная защита, но паника из
ТЕЛА цикла проходит через кадр итератора, и такой recover
съедает чужую панику. Рантайм это замечает.
6. ВЛОЖЕННЫЕ ЦИКЛЫ И BREAK С МЕТКОЙ
------------------------------------------------------------------
Два цикла по функциям, break с меткой из внутреннего:
тело: A1
тело: A2
тело: A3
внутренний итератор: вернулся
тело: B1
тело: B2
тело: break Loop
внутренний итератор: yield вернул false
внутренний итератор: вернулся
внешний итератор: yield вернул false
внешний итератор: вернулся
Оба итератора получили false и оба доработали — сначала
внутренний, потом внешний. Выход через два уровня не
прыжок: это две остановки подряд, каждая по своему yield.
Уборка обоих итераторов при этом выполняется.
7. ITER.SEQ2 И СТАНДАРТНАЯ БИБЛИОТЕКА
------------------------------------------------------------------
Два типа, и оба — просто имена для функций:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
slices.Sorted(maps.Keys(m)): [груша слива яблоко]
slices.Sorted(maps.Values(m)): [1 2 3]
Метод All() коллекции возвращает iter.Seq2, и по нему
ходят обычным for ... range с двумя переменными:
груша 3
слива 2
яблоко 1
slices.Collect собирает последовательность обратно в срез:
slices.Collect((set{m}).All() по ключам) -> [груша слива яблоко]
slices.Collect(slices.Values([]int{4,1,3})) -> [4 1 3]
slices.All даёт пары «индекс, значение» для среза:
0 a
1 b
2 c
Ничего нового в язык для этого не добавляли: iter.Seq —
это имя для типа функции, а не новая сущность. Поэтому
свой итератор ничем не отличается от библиотечного и
работает с теми же slices.Collect и slices.Sorted.