Deep Engineering

ЗАМЕР

bench/gomap/insert.go

Скрипт, которым получены числа в статье, и запись прогона. Файл читается на сборке из репозитория — это тот самый код, который запускали, а не его копия.

Цитируется в статье
/ru/go/data-structures/maps
Прогон
go1.24.7 linux/amd64, Intel Xeon 2.10GHz
Как запустить
go run bench/gomap/layout.go        # работает и из корня
go run bench/gomap/nankey.go
go run bench/gomap/insert.go
go run bench/gomap/groupindex.go
go run bench/gomap/controlword.go
cd bench/gomap
go test -run '^$' -bench . -benchmem .
./ab.sh

Запись прогона

Замеры для статьи «Карта в Go: Swiss Table, порядок обхода и цена промаха»

Файл Что делает
layout.go восемь наблюдений без единого замера времени: карта как указатель, сколько бывает порядков обхода, nil-карта, почему нельзя взять адрес элемента, что делает delete с памятью, что даёт подсказка размера, порог 128 байт у ключа, какие типы годятся в ключи
cost_test.go цена: поиск по типу ключа, промах против попадания, построение с подсказкой и без, обход, плотные ключи против среза
nankey.go NaN как ключ: сколько записей после N вставок, что возвращает поиск, что делает delete и что — clear. Ни одного замера времени
ab.sh старая реализация карты против новой на одной машине: GOEXPERIMENT=noswissmap
contract.go, contract.sh что карта обещает: ноль вместо ошибки, форма с двумя результатами, nil-карта, и отдельно — что отвергает компилятор на несравнимом ключе
controlword.go управляющее слово: четыре шага, которыми восемь сравнений становятся одним, на настоящих байтах и со сверкой с честным перебором
insert.go путь вставки: как хеш делится на H1 и H2, где применяется каждая половина, пять сценариев вставки со сверкой с настоящей картой
groupindex.go откуда берётся номер группы: таблица как массив групп, h1 & (N − 1) и какие именно биты хеша становятся индексом; что делает с номером рост таблицы; цена пересечения бит метки и номера
growth.go расширение карты наблюдением за памятью, а не по описанию
concurrent.go, race.sh что происходит при одновременном доступе без синхронизации
practice.go ответы к задачам урока — прогоном, а не рассуждением

Каталог — отдельный модуль Go, поэтому замеры запускаются из него:

go run bench/gomap/layout.go        # работает и из корня
go run bench/gomap/nankey.go
go run bench/gomap/insert.go
go run bench/gomap/groupindex.go
go run bench/gomap/controlword.go
cd bench/gomap
go test -run '^$' -bench . -benchmem .
./ab.sh

layout.go помечен //go:build ignore — он package main, а рядом лежит тест пакета gomap, и без метки go test ./... спотыкался бы о два пакета в одном каталоге.

Что здесь важно прочитать правильно

Главное — не наносекунды. Оно в layout.go: порядок обхода не случаен, а провёрнут; delete не возвращает память; подсказка размера уменьшает не карту, а мусор. Это утверждения о поведении, и проверяются они сравнением, а не секундомером.

По времени сопоставляются только строки внутри одного блока cost_test.go. В каждом блоке все строки дают одинаковый результат, отличается лишь способ. Блоки друг с другом не сопоставляются.

ab.sh — единственное место, где сравниваются две сборки, и это законно ровно потому, что отличается один флаг. Компилятор, машина и сам замер те же. Строка cpu: печатается для каждого прогона намеренно: на виртуальной машине частота плавает, и если она в раундах разная, раунд надо выбросить, а не считать разницу.

Что получилось (go1.24.7 linux/amd64, Intel Xeon 2.10GHz)

Устройство

Из internal/abi и internal/runtime/maps:

константа значение что это
SwissMapGroupSlots 8 слотов в группе; на группу один 8-байтовый управляющий блок
maxAvgGroupLoad 7 предел заполнения перед ростом — 7 из 8, то есть 87,5 %
maxTableCapacity 1024 больше этого таблица не растёт целиком, а делится надвое
SwissMapMaxKeyBytes 128 ключ крупнее хранится не в слоте, а по указателю
SwissMapMaxElemBytes 128 то же для значения

unsafe.Sizeof(map[int]int{}) = 8 байт: в переменной лежит один указатель. Для сравнения: срез — 24 байта, строка — 16.

Вставка: где применяется H1, а где H2 (insert.go)

Хеш режется по седьмому биту, и половины не пересекаются:

биты где применяется
h1 = h >> 7 старшие 57 номер стартовой группы (h1 & mask) и семя треугольного пути
h2 = h & 0x7f младшие 7 ложится в управляющий байт слота как метка

Ни один бит не участвует в обеих ролях: совпадение метки ничего не говорит о группе, а номер группы ничего не говорит о метке.

Комментарий в group.go здесь расходится с кодом. Строка про занятый слот

full: 0 h h h h h h h  // h represents the H1 hash bits

называет H1, а код рядом пишет в управляющий байт H2: PutSlot делает g.ctrls().set(i, ctrl(h2(hash))), поиск зовёт matchH2(h2(hash)), и map.go над самой функцией h2 поясняет — «These are used as an occupied control byte» (пер.: «Они используются как управляющий байт занятого слота»). В байте лежит H2; проверено на go1.24.7.

Пять сценариев вставки, все на настоящих хешах FNV-1a и с полной трассой:

сценарий что показывает
в стартовой группе есть место один шаг: свободный слот — конец пути
метка совпала, ключ чужой ложное совпадение стоит одного полного сравнения, поиск продолжается в той же группе
ключ уже есть значение переписывается на месте, нового слота не появляется, len не растёт
стартовая группа занята целиком путь идёт дальше по треугольной последовательности
по дороге удалённый слот путь не останавливается на нём: место запоминается, поиск идёт до свободного слота, и только тогда ключ кладётся в запомненный удалённый

Последняя строка — то, ради чего написана программа. Вставка не может занять первое подвернувшееся место: пока не встретился свободный слот, неизвестно, нет ли этого ключа дальше по пути. Поэтому вставка нового ключа стоит полного поиска промаха, а не «нашли дырку и положили».

Перенос алгоритма проверяется в самой программе: битовые формулы сверяются с честным перебором на 200 000 случайных групп (расхождений 0), треугольный путь — с утверждением исходника, что он обходит каждую группу ровно один раз (проверено для 4, 8, 16, 64 и 1024 групп), а наблюдаемая часть — с настоящей map[string]int по len на семи операциях (расхождений 0).

Хеш-функция взята FNV-1a, а не картина: внутренний хеш карты сеется случайно при её создании и снаружи не воспроизводится. Таблица сокращена до 4 групп по 8 слотов — сокращена только ширина, не правила.

Порядок обхода

Одна и та же карта, 2000 обходов, считаем число РАЗЛИЧНЫХ порядков:

n различных порядков обходов
3 3 2 000
6 6 2 000
8 8 2 000
9 9 2 000
16 16 2 000
896 896 20 000
897 1 174 20 000

До 896 записей — ровно n, а не n!. Все порядки при этом сдвиги одной и той же последовательности: [0 1 2], [1 2 0], [2 0 1].

Граница считается из констант: maxTableCapacity = 1024, коэффициент заполнения 7/8, то есть 896 записей — последняя длина, при которой карта помещается в ОДНУ таблицу. На 897 у итератора появляется второе независимое смещение, по директории таблиц (it.dirOffset), и правило кончается. Замеренная граница совпала с расчётной точно.

Сама последовательность у каждого запуска программы своя: карта засевает хеш при старте. Постоянно другое — что внутри одного запуска порядки отличаются только сдвигом.

Память

Миллион записей map[int]int, живая куча:

момент куча len
до начала 0,1 МБ
миллион записей 36,0 МБ 1 000 000
после delete всех ключей 36,0 МБ 0
после clear(m) 36,0 МБ 0
после m = make(map[int]int) 0,1 МБ 0

Подсказка размера, миллион записей:

выделено всего выделений осталось жить
без подсказки 75 407 864 б 8 188 37 776 744 б
make(map, n) 37 832 960 б 4 101 37 832 752 б
отношение ×1,99 ×2,00 ×1,00

Готовая карта одинакова. Экономятся не байты в ней, а промежуточные таблицы.

Порог размера ключа, 10 000 записей:

ключ выделено выделений на запись
[120]byte 2 228 912 б 34 222,9 б
[128]byte 2 359 984 б 34 236,0 б
[136]byte 1 735 600 б 10 034 173,6 б

За порогом выделений становится по одному на каждый ключ, а байт — меньше: слоты в группе резервируются все восемь сразу, а вынесенный ключ выделяется ровно по размеру.

Цена, N = 100 000

Блок 1 — найти ключ, который есть:

тип ключа ns/op
int64 18,06
[2]int64 24,95
string 28,19

Блок 2 — промах против попадания и форма запроса. Все четыре строки в одном блоке намеренно: брать «попадание» из блока 1 и «попадание с запятой» отсюда было бы сравнением через границу блока.

что ns/op
попадание, v := m[k] 17,83
попадание, v, ok := m[k] 18,43
промах, v := m[k] 28,62
промах, v, ok := m[k] 28,44

Форма с запятой не стоит ничего — на промахе разница даже в другую сторону. А промах дороже попадания в 1,6 раза, и это следствие устройства: поиск останавливается, только найдя группу со свободным слотом.

Блок 3 — построить карту из 100 000 записей:

ns/op B/op allocs/op
make(map[int64]int) 5 423 978 4 729 886 532
make(map[int64]int, N) 2 760 050 2 364 769 258

Блок 4 — обойти 100 000 значений и просуммировать:

ns/op
карта 855 702
срез (та же задача, плотные ключи) 37 434

Блок 5 — прочитать одно значение по плотному целому ключу:

ns/op
карта 15,55
срез 1,12

От прогона к прогону (три прогона по 2 с): блок 1 — 17,81–18,37 / 24,79–24,99 / 27,46–28,25; блок 2 — 17,75–18,11 и 18,40–18,84 у попаданий, 28,57–28,76 и 28,28–28,89 у промахов; блок 3 — 5 420 612–5 506 708 и 2 740 518–2 816 191; блок 4 — 855 535–856 652 и 36 697–37 482. Столбцы B/op и allocs/op не менялись.

Старая реализация против новой

ab.sh, четыре чередующихся раунда, одна машина, строка cpu: во всех раундах совпала:

операция Swiss Table (по умолчанию) старая (noswissmap)
попадание, int64, форма v, ok := 17,95–18,88 35,22–36,58 новая быстрее вдвое
попадание, string 25,59–28,14 51,89–54,70 новая быстрее вдвое
промах, int64 28,96–29,66 20,57–21,71 новая медленнее в 1,4 раза
обход 100 000 записей 847 706–893 851 1 046 614–1 079 682 новая быстрее на ~25 %

Замер один и тот же, компилятор один и тот же, отличается флаг сборки.

Наблюдения layout.go

  • Карта — указатель, поэтому вставка внутри функции видна снаружи; у среза добавление в той же ситуации не видно.
  • Порядок обхода — сдвиг, а не перестановка: различных порядков ровно n — до 896 записей, то есть пока карта помещается в одну таблицу.
  • nil-карта: чтение, len, range и delete работают; паникует только запись.
  • Адрес элемента взять нельзя — при росте таблица перестраивается целиком и записи переезжают.
  • delete и clear не возвращают память — только замена карты.
  • Подсказка размера вдвое уменьшает выделенное по дороге и не меняет размер готовой карты.
  • Порог 128 байт переносит ключ из слота в отдельное выделение.
  • Ключом может быть любой сравнимый тип; у map[any]T сравнимость проверяется в рантайме, и несравнимое значение даёт панику hash of unhashable type.

Что не подтвердилось

Расхожее «карта в Go — это бакеты по 8 элементов с цепочкой переполнения» описывает реализацию до Go 1.24. Она никуда не делась, но включается флагом: GOEXPERIMENT=noswissmap. По умолчанию с 1.24 работает Swiss Table.

И вторая половина расхожего — «новая реализация быстрее». Быстрее не всё: на промахе она медленнее старой в 1,4 раза, и это воспроизводится в каждом раунде. Механизм разный: в старой карте промах смотрит один бакет и кончается там же, если цепочки переполнения нет, а в Swiss Table он обязан идти до группы со свободным слотом. То, что делает попадание быстрым, делает промах длинным. Оговорка в блоге команды Go стоит: «some edge cases do regress compared to Go 1.23». Здесь этот случай назван поимённо и измерен. Оговорка к самому измерению тоже нужна: это одна форма карты (map[int64]int, 100 000 записей, ключи подряд), а цена промаха зависит от заполнения таблицы и от того, как разложены ключи.

NaN как ключ (nankey.go)

Запись, которую нельзя ни найти, ни перезаписать, ни удалить, и которая накапливается при каждой вставке.

Первоисточник №1 — спецификация Go, раздел Comparison operators:

Floating-point types are comparable and ordered. Two floating-point values are compared as defined by the IEEE 754 standard.

(пер.: «Типы с плавающей точкой сравнимы и упорядочены. Два значения с плавающей точкой сравниваются так, как определено стандартом IEEE 754».)

IEEE 754 определяет NaN как значение, не равное ничему, включая само себя. Отсюда всё остальное: карта ищет ключ по равенству, а ключ не равен сам себе.

Первоисточник №2 — спецификация Go, раздел Map types:

The comparison operators == and != must be fully defined for operands of the key type; thus the key type must not be a function, map, or slice.

(пер.: «Операторы сравнения == и != должны быть полностью определены для операндов типа ключа; поэтому тип ключа не должен быть функцией, картой или срезом».)

float64 этому требованию удовлетворяет — операторы определены. Определены, но не рефлексивны, и на это карта не рассчитана. Требование спецификации запрещает срезы и карты, но NaN не запрещает.

Первоисточник №3 — комментарий в рантайме, $(go env GOROOT)/src/runtime/alg.go:93:

// NOTE: Because NaN != NaN, a map can contain any // number of (mostly useless) entries keyed with NaNs. // To avoid long hash chains, we assign a random number // as the hash value for a NaN.

(пер.: «ЗАМЕЧАНИЕ: поскольку NaN != NaN, карта может содержать любое число (по большей части бесполезных) записей с ключами-NaN. Чтобы избежать длинных цепочек хеша, мы назначаем NaN случайное число в качестве значения хеша».)

Это ключевая цитата: накопление дубликатов — не побочный эффект и не дефект, а известное и задокументированное поведение. Рантайм лишь смягчает последствия, раздавая NaN случайный хеш, чтобы они не собирались в одну цепочку. Видно это прямо в f64hash там же: case f != f: return c1 * (c0 ^ h ^ uintptr(rand())).

Первоисточник №4$(go env GOROOT)/src/internal/runtime/maps/table.go:634, про то, что с такой записью можно сделать:

// One exception is keys that don't // compare equal to themselves (e.g., // NaN). These keys cannot be looked // up, so getWithKey will fail even if // the key exists. // // However, we are in luck because such // keys cannot be updated and they // cannot be deleted except with clear.

(пер.: «Исключение — ключи, которые не сравниваются равными сами себе (например, NaN). Такие ключи невозможно найти, поэтому getWithKey потерпит неудачу, даже если ключ существует. Однако нам везёт, потому что такие ключи нельзя обновить и нельзя удалить иначе как через clear».)

Здесь названы сразу три следствия и один выход: не найти, не обновить, не удалить — кроме как clear. Всё четыре проверены прогоном.

Прогон целиком:

=== 0. Исходное свойство ===
  math.NaN() == math.NaN() : false
  nan == nan (одна переменная): false
  math.IsNaN(nan): true

=== 1. N вставок одного и того же NaN ===
  после m[nan] = 1 → len(m) = 1
  после m[nan] = 2 → len(m) = 2
  после m[nan] = 3 → len(m) = 3
  после m[nan] = 4 → len(m) = 4
  после m[nan] = 5 → len(m) = 5

  итог: 5 вставок дали 5 записей
  обычный ключ на его месте дал бы одну: присваивание по
  существующему ключу перезаписывает, но «существующий» —
  это найденный по ==, а найти NaN нельзя.

=== 2. Что возвращает поиск ===
  m[nan]        → значение 0, найдено false
  m[math.NaN()] → значение 0, найдено false
  не помогает даже та же самая переменная: сравнение всё равно
  идёт по ==, а оно ложно.

=== 3. Что делает delete ===
  len до delete: 5
  len после двух delete: 5
  delete не паникует и не сообщает об ошибке — он просто не
  находит, что удалять.

=== 4. Записи на месте, и их видно перебором ===
  запись 1: ключ IsNaN=true, k==k → false, значение 1
  запись 2: ключ IsNaN=true, k==k → false, значение 2
  запись 3: ключ IsNaN=true, k==k → false, значение 3
  запись 4: ключ IsNaN=true, k==k → false, значение 4
  запись 5: ключ IsNaN=true, k==k → false, значение 5
  перебор нашёл 5 записей
  то есть память они занимают, значения хранят, а достучаться
  до них по ключу нельзя. Под нагрузкой, где NaN приходит из
  данных (0.0/0.0, math.Sqrt(-1), разбор "NaN"), карта растёт
  без предела, и ни одна запись из неё не переиспользуется.

=== 5. Единственный штатный способ убрать: clear ===
  len до clear: 5
  len после clear(m): 0
  ровно то, что написано в рантайме: "cannot be deleted
  except with clear".

=== 6. Контроль: карта следует == буквально, и там, где == ===
    ведёт себя нормально, странностей нет ===
  0.0 == -0.0: true
  после m[+0]=1 и m[-0]=2 → len = 1, m[0] = 2
  +0 и -0 равны по IEEE 754, поэтому это ОДНА запись, и вторая
  вставка перезаписала первую. Карта не «сломана» на float —
  она честно следует ==. Ломается ровно то место, где == 
  перестаёт быть рефлексивным, и это только NaN.

=== 7. Как с этим жить ===
  отсекать NaN на входе:
    NaN отвергнут проверкой math.IsNaN
    NaN отвергнут проверкой math.IsNaN
  len(safe) = 2 — ровно столько, сколько годных ключей

Что показал прогон:

действие результат
5 вставок одного и того же nan len(m) = 5
m[nan] 0, false
m[math.NaN()] 0, false
delete(m, nan) дважды len не изменился: 5 → 5
перебор range находит все 5 записей, значения 1…5 целы
clear(m) len = 0

Четыре вещи, которые стоит различать:

  1. Не помогает та же самая переменная. m[nan] с тем же nan, которым клали, всё равно даёт false: сравнение идёт по ==, а оно ложно независимо от того, откуда взялось значение.
  2. delete молчит. Он не паникует и ничего не возвращает — просто не находит, что удалять. Ошибка не проявляется никак.
  3. Записи живые. Перебором они видны, значения целы, память занята. То есть это утечка с полностью рабочим на вид кодом: под нагрузкой, где NaN приходит из данных (0.0/0.0, math.Sqrt(-1), разбор строки "NaN"), карта растёт без предела и ни одна запись не переиспользуется.
  4. clear работает — ровно как обещает комментарий рантайма.

Контроль, который стоит прочитать. Блок 6 прогона показывает +0 и -0: по IEEE 754 они равны, и карта делает из них ОДНУ запись, вторая вставка перезаписывает первую (len = 1, m[0] = 2). То есть карта не «сломана» на float — она честно следует ==. Ломается ровно то место, где == перестаёт быть рефлексивным, и это только NaN.

Лечение — отсекать NaN на входе через math.IsNaN, блок 7.

Источники

Скрипт

796 строк
//go:build ignore

// Вставка в карту Go: что делает H1, а что H2.
//
// ЗАЧЕМ ЭТА ПРОГРАММА. Про Swiss Table обычно пишут, что хеш делится на H1 и
// H2, и на этом останавливаются. Из такой фразы не выводится ничего: непонятно,
// где H1 применяется, почему H2 попадает в управляющий байт, почему вставка
// обязана сначала ИСКАТЬ и что происходит, когда по дороге встретился удалённый
// слот. Здесь путь вставки выполняется по шагам и печатается целиком.
//
// ЧТО ЗДЕСЬ НАСТОЯЩЕЕ. Алгоритм — дословный перенос `table.PutSlot` из
// go1.24.7, `src/internal/runtime/maps/table.go`, вместе с разбором хеша
// (`h1(h) = h >> 7`, `h2(h) = h & 0x7f`, `map.go`), треугольным путём поиска
// (`makeProbeSeq`, `table.go`) и битовыми формулами группы (`group.go`).
// Порядок решений сохранён вплоть до правила «первый удалённый слот
// запоминается, но путь продолжается до свободного».
//
// ЧТО ЗДЕСЬ УСЛОВНО, И ПОЧЕМУ ИНАЧЕ НЕЛЬЗЯ. Хеш-функция взята другая: FNV-1a из
// `hash/fnv`. Карта хеширует своей внутренней функцией со случайным seed,
// который выбирается при создании карты, — снаружи её ни вызвать, ни
// воспроизвести. Взять детерминированную функцию — единственный способ
// напечатать числа, которые у читателя получатся такими же. На разбор хеша это
// не влияет: делится любое 64-битное число одинаково.
//
// Таблица тоже маленькая: 4 группы по 8 слотов вместо настоящей, которая для
// таких размеров ещё и не разбита на несколько таблиц. Сокращена ТОЛЬКО
// ширина, не правила.
//
// ЧЕМ ПОДПЁРТО. Блок 4 проверяет перенос: битовые формулы сверяются с честным
// перебором на всех 128 метках и на случайных группах, а треугольный путь — с
// утверждением исходника, что он обходит каждую группу ровно один раз. Блок 5
// сверяет наблюдаемую часть с настоящей `map[string]int`: то, что видно снаружи
// (len до и после повторной вставки), обязано совпасть.
//
// ЗАПУСК:
//
//	go run bench/gomap/insert.go
package main

import (
	"fmt"
	"hash/fnv"
	"math/bits"
	"math/rand"
	"strings"
)

// ------------------------------------------------------------ из group.go
//
// Дословно из go1.24.7, src/internal/runtime/maps/group.go.
//
//	  empty: 1 0 0 0 0 0 0 0
//	deleted: 1 1 1 1 1 1 1 0
//	   full: 0 h h h h h h h
//
// Про последнюю строку в group.go написано «h represents the H1 hash bits», и
// это единственное место, где сказано «H1». Код рядом говорит обратное:
// `PutSlot` пишет в управляющий байт `ctrl(h2(hash))`, поиск ищет `matchH2`, а
// `map.go` над самой функцией `h2` поясняет: «These are used as an occupied
// control byte». В управляющем байте лежит H2; комментарий в group.go
// разошёлся с кодом.

const (
	ctrlEmpty   = 0b10000000
	ctrlDeleted = 0b11111110

	bitsetLSB = 0x0101010101010101
	bitsetMSB = 0x8080808080808080

	slotsPerGroup = 8 // abi.SwissMapGroupSlots
	maxAvgLoad    = 7 // maxAvgGroupLoad из group.go: 7 занятых слотов на 8
)

func matchH2(g uint64, h uint8) uint64 {
	v := g ^ (bitsetLSB * uint64(h))
	return ((v - bitsetLSB) &^ v) & bitsetMSB
}

func matchEmpty(g uint64) uint64          { return (g &^ (g << 6)) & bitsetMSB }
func matchEmptyOrDeleted(g uint64) uint64 { return g & bitsetMSB }

// first — номер слота по младшему выставленному биту (bitsetFirst из group.go).
func first(mask uint64) int { return bits.TrailingZeros64(mask) >> 3 }

// removeFirst гасит младший выставленный бит (bitset.removeFirst).
func removeFirst(mask uint64) uint64 { return mask & (mask - 1) }

// ------------------------------------------------------------ из map.go

// h1 — старшие 57 бит хеша. Ими выбирается группа и задаётся путь поиска.
func h1(h uint64) uint64 { return h >> 7 }

// h2 — младшие 7 бит хеша. Они ложатся в управляющий байт как метка слота.
func h2(h uint64) uint8 { return uint8(h & 0x7f) }

// ------------------------------------------------------------ из table.go

// probeSeq — треугольный путь по группам, p(i) = (i²+i)/2 + hash (mod mask+1).
type probeSeq struct {
	mask   uint64
	offset uint64
	index  uint64
}

func makeProbeSeq(hash, mask uint64) probeSeq {
	return probeSeq{mask: mask, offset: hash & mask}
}

func (s probeSeq) next() probeSeq {
	s.index++
	s.offset = (s.offset + s.index) & s.mask
	return s
}

// ------------------------------------------------------------ таблица

type slot struct {
	key string
	val int
}

type table struct {
	mask       uint64 // число групп минус один
	ctrl       []uint64
	slots      [][slotsPerGroup]slot
	growthLeft int
	used       int
}

func newTable(groups int) *table {
	t := &table{
		mask:  uint64(groups - 1),
		ctrl:  make([]uint64, groups),
		slots: make([][slotsPerGroup]slot, groups),
	}
	for i := range t.ctrl {
		t.ctrl[i] = bitsetLSB * uint64(ctrlEmpty) // все слоты свободны
	}
	t.growthLeft = groups * slotsPerGroup * maxAvgLoad / slotsPerGroup
	return t
}

// clone — честная копия. Нужна, чтобы каждый сценарий начинался с одного и
// того же состояния. Первая версия делала `snap := *t`, и это было ошибкой:
// в table лежат срезы, у копии структуры массив под ними тот же самый, так что
// запись одного сценария оказывалась видна следующему. В напечатанной трассе
// это выглядело так, будто вставка нового ключа обернулась обновлением
// существующего, — программа сама себе противоречила, и потому ошибку было
// видно.
func (t *table) clone() *table {
	c := &table{
		mask:       t.mask,
		ctrl:       append([]uint64(nil), t.ctrl...),
		slots:      append([][slotsPerGroup]slot(nil), t.slots...),
		growthLeft: t.growthLeft,
		used:       t.used,
	}
	return c
}

func (t *table) ctrlByte(g, i int) uint8 { return uint8(t.ctrl[g] >> (8 * i)) }

func (t *table) setCtrl(g, i int, c uint8) {
	t.ctrl[g] &^= 0xff << (8 * i)
	t.ctrl[g] |= uint64(c) << (8 * i)
}

func hash(key string) uint64 {
	h := fnv.New64a()
	_, _ = h.Write([]byte(key))
	return h.Sum64()
}

// step — одна запись трассы. Печатается и она же уезжает в картинку.
type step struct {
	group    int
	kind     string // probe-match, probe-full, probe-deleted, probe-empty, write, update, grow
	slotIdx  int
	comment  string
	candidat []int // слоты, у которых совпала метка

	// Где путь ДОШЁЛ до свободного слота. Может отличаться от group: если по
	// дороге попался удалённый слот, ключ ложится в него, а не сюда. Без этих
	// двух полей трасса умалчивала самое интересное — что путь побывал в одной
	// группе, а запись случилась в другой.
	endGroup int
	endSlot  int
}

// put повторяет table.PutSlot: сначала ищет ключ, потом место.
func (t *table) put(key string, val int) (trace []step) {
	h := hash(key)
	mark := h2(h)
	seq := makeProbeSeq(h1(h), t.mask)

	firstDeletedGroup, firstDeletedSlot := -1, -1

	for {
		g := int(seq.offset)
		word := t.ctrl[g]

		// 1. Есть ли уже такой ключ? Сначала метка, потом полное сравнение.
		match := matchH2(word, mark)
		var cands []int
		for m := match; m != 0; m = removeFirst(m) {
			cands = append(cands, first(m))
		}
		for m := match; m != 0; m = removeFirst(m) {
			i := first(m)
			if t.slots[g][i].key == key {
				t.slots[g][i].val = val
				trace = append(trace, step{group: g, kind: "update", slotIdx: i, candidat: cands,
					comment: "метка совпала и ключ совпал целиком — это тот же ключ. Значение переписывается на месте: нового слота не появляется, len не растёт."})
				return
			}
		}
		if len(cands) > 0 {
			trace = append(trace, step{group: g, kind: "probe-match", slotIdx: -1, candidat: cands,
				comment: "метка совпала, но ключ при полном сравнении оказался чужим. Семь бит — не ответ, а сужение круга: это ложное совпадение, поиск продолжается в этой же группе."})
		}

		// 2. Ключа здесь нет. Конец ли это пути?
		free := matchEmptyOrDeleted(word)
		if free == 0 {
			trace = append(trace, step{group: g, kind: "probe-full", slotIdx: -1, candidat: cands,
				comment: "в группе нет ни одного свободного или удалённого слота — она занята целиком. Остановиться нельзя: ключ мог осесть дальше по пути."})
			seq = seq.next()
			continue
		}
		i := first(free)
		if t.ctrlByte(g, i) == ctrlDeleted {
			if firstDeletedGroup < 0 {
				firstDeletedGroup, firstDeletedSlot = g, i
			}
			trace = append(trace, step{group: g, kind: "probe-deleted", slotIdx: i, candidat: cands,
				comment: "первый незанятый слот здесь — удалённый. Место запоминается, но путь продолжается: пока не встретился свободный слот, нельзя утверждать, что такого ключа в таблице нет."})
			seq = seq.next()
			continue
		}

		// 3. Свободный слот: путь окончен, ключа в таблице нет.
		endG, endI := g, i
		useG, useI := g, i
		reused := false
		if firstDeletedGroup >= 0 {
			useG, useI = firstDeletedGroup, firstDeletedSlot
			reused = true
			t.growthLeft++ // ниже вычтется обратно: удалённый слот уже был учтён
		}

		if t.growthLeft <= 0 {
			trace = append(trace, step{group: g, kind: "grow", slotIdx: i, candidat: cands,
				comment: "место есть, но запаса роста не осталось: занято 7/8. Таблица пересобирается, и только потом ключ ложится на место."})
			return
		}

		t.slots[useG][useI] = slot{key: key, val: val}
		t.setCtrl(useG, useI, mark)
		t.growthLeft--
		t.used++

		why := "свободный слот — конец пути: будь такой ключ в таблице, он лежал бы не дальше этого места. Ключ и значение ложатся в слот, а в управляющий байт пишется метка — младшие 7 бит хеша."
		if reused {
			why = fmt.Sprintf("свободный слот нашёлся в группе %d — значит, такого ключа в таблице точно нет. Только теперь можно вернуться к запомненному удалённому слоту в группе %d и занять его: он ближе к началу пути, и запас роста на него не тратится.", endG, useG)
		}
		trace = append(trace, step{group: useG, kind: "write", slotIdx: useI, candidat: cands,
			endGroup: endG, endSlot: endI, comment: why})
		return
	}
}

func (t *table) del(key string) bool {
	h := hash(key)
	mark := h2(h)
	seq := makeProbeSeq(h1(h), t.mask)
	for {
		g := int(seq.offset)
		word := t.ctrl[g]
		for m := matchH2(word, mark); m != 0; m = removeFirst(m) {
			i := first(m)
			if t.slots[g][i].key == key {
				t.slots[g][i] = slot{}
				// Правило из map.go: если в группе есть свободный слот, удалённый
				// можно пометить свободным; иначе — только «удалён».
				if matchEmpty(word) != 0 {
					t.setCtrl(g, i, ctrlEmpty)
					t.growthLeft++
				} else {
					t.setCtrl(g, i, ctrlDeleted)
				}
				t.used--
				return true
			}
		}
		if matchEmpty(word) != 0 {
			return false
		}
		seq = seq.next()
	}
}

// ------------------------------------------------------------ печать

func stateRune(c uint8) string {
	switch {
	case c == ctrlEmpty:
		return "  ····"
	case c == ctrlDeleted:
		return "  удал"
	default:
		return fmt.Sprintf("  0x%02x", c)
	}
}

func (t *table) dump(indent string) {
	for g := range t.ctrl {
		var parts []string
		for i := 0; i < slotsPerGroup; i++ {
			parts = append(parts, stateRune(t.ctrlByte(g, i)))
		}
		fmt.Printf("%sгруппа %d %s\n", indent, g, strings.Join(parts, ""))
	}
}

func header(n int, title string) {
	fmt.Printf("\n=== БЛОК %d. %s ===\n\n", n, title)
}

// ------------------------------------------------------------ блок 1

func blockSplit(keys []string) {
	header(1, "КАК ДЕЛИТСЯ ХЕШ")
	fmt.Println("  h1 = h >> 7   (старшие 57 бит)   — выбирает группу и задаёт путь")
	fmt.Println("  h2 = h & 0x7f (младшие  7 бит)   — ложится в управляющий байт")
	fmt.Println()
	fmt.Printf("  %-8s %-20s %-8s %-6s %s\n", "ключ", "хеш", "h2", "группа", "младшие 16 бит хеша")
	for _, k := range keys {
		h := hash(k)
		fmt.Printf("  %-8s 0x%016x 0x%02x   %-6d %s|%s\n",
			k, h, h2(h), h1(h)&3,
			fmt.Sprintf("%09b", (h>>7)&0x1ff), fmt.Sprintf("%07b", h&0x7f))
	}
	fmt.Println()
	fmt.Println("  Столбец справа — это одно и то же число, разрезанное по седьмому биту:")
	fmt.Println("  слева от черты уходит в h1, справа — в h2. Ни один бит не участвует")
	fmt.Println("  в обеих ролях, поэтому совпадение метки ничего не говорит о группе,")
	fmt.Println("  а номер группы ничего не говорит о метке.")
}

// ------------------------------------------------------------ блок 2

func printTrace(title, key string, t *table, trace []step) {
	h := hash(key)
	fmt.Printf("  %s\n", title)
	fmt.Printf("    m[%q] = …\n", key)
	fmt.Printf("    хеш 0x%016x   h1 = 0x%014x   h2 = 0x%02x   старт: группа %d\n",
		h, h1(h), h2(h), h1(h)&t.mask)
	for i, s := range trace {
		fmt.Printf("    шаг %d · группа %d · %s\n", i+1, s.group, s.kind)
		if len(s.candidat) > 0 {
			fmt.Printf("            метка совпала в слотах: %v\n", s.candidat)
		}
		if s.kind == "write" || s.kind == "update" || s.kind == "probe-deleted" {
			fmt.Printf("            слот %d\n", s.slotIdx)
		}
		if s.kind == "write" && s.endGroup != s.group {
			fmt.Printf("            путь дошёл до свободного слота в группе %d, слот %d,\n", s.endGroup, s.endSlot)
			fmt.Printf("            но запись — в группу %d: там ждал удалённый слот\n", s.group)
		}
		fmt.Printf("            %s\n", s.comment)
	}
	fmt.Printf("    len после операции: %d\n\n", t.used)
}


// ------------------------------------------------------------ блок 3
//
// Данные для картинки печатаются здесь же, а не переносятся в неё руками.
// Правило простое: если число видно читателю, оно должно быть в записи
// прогона. Копировать глазами таблицу из восьми слотов на четыре группы —
// верный способ ошибиться в одном байте и не заметить.

func jsonSlots(t *table) string {
	var groups []string
	for g := range t.ctrl {
		var cells []string
		for i := 0; i < slotsPerGroup; i++ {
			c := t.ctrlByte(g, i)
			switch {
			case c == ctrlEmpty:
				cells = append(cells, `{"s":"empty"}`)
			case c == ctrlDeleted:
				cells = append(cells, `{"s":"deleted"}`)
			default:
				cells = append(cells, fmt.Sprintf(`{"s":"used","h2":%d,"k":%q}`, c, t.slots[g][i].key))
			}
		}
		groups = append(groups, "  ["+strings.Join(cells, ",")+"]")
	}
	return "[\n" + strings.Join(groups, ",\n") + "\n ]"
}

func jsonTrace(key string, t *table, trace []step) string {
	h := hash(key)
	var steps []string
	for _, s := range trace {
		steps = append(steps, fmt.Sprintf(
			`  {"group":%d,"kind":%q,"slot":%d,"endGroup":%d,"endSlot":%d,"cands":%s}`,
			s.group, s.kind, s.slotIdx, s.endGroup, s.endSlot, jsonInts(s.candidat)))
	}
	return fmt.Sprintf("{\n \"key\":%q,\n \"hash\":\"0x%016x\",\n \"h1\":\"0x%x\",\n \"h2\":%d,\n \"start\":%d,\n \"lenBefore\":%d,\n \"steps\":[\n%s\n ]\n}",
		key, h, h1(h), h2(h), h1(h)&t.mask, t.used, strings.Join(steps, ",\n"))
}

func jsonInts(xs []int) string {
	if len(xs) == 0 {
		return "[]"
	}
	parts := make([]string, len(xs))
	for i, x := range xs {
		parts[i] = fmt.Sprint(x)
	}
	return "[" + strings.Join(parts, ",") + "]"
}

// ------------------------------------------------------------ блок 4

func blockSelfCheck() {
	header(4, "ПРОВЕРКА ПЕРЕНОСА")

	// 4.1. Битовые формулы против честного перебора.
	rng := rand.New(rand.NewSource(20260902))
	bad := 0
	for n := 0; n < 200000; n++ {
		var b [slotsPerGroup]uint8
		var word uint64
		for i := range b {
			switch rng.Intn(4) {
			case 0:
				b[i] = ctrlEmpty
			case 1:
				b[i] = ctrlDeleted
			default:
				b[i] = uint8(rng.Intn(128))
			}
			word |= uint64(b[i]) << (8 * i)
		}
		h := uint8(rng.Intn(128))
		var wantH2, wantEmpty, wantEOD []int
		for i, c := range b {
			if c == h {
				wantH2 = append(wantH2, i)
			}
			if c == ctrlEmpty {
				wantEmpty = append(wantEmpty, i)
			}
			if c == ctrlEmpty || c == ctrlDeleted {
				wantEOD = append(wantEOD, i)
			}
		}
		gotH2 := setBits(matchH2(word, h))
		if !covers(gotH2, wantH2) || !equal(setBits(matchEmpty(word)), wantEmpty) ||
			!equal(setBits(matchEmptyOrDeleted(word)), wantEOD) {
			bad++
		}
	}
	fmt.Printf("  формулы группы против перебора, 200 000 случайных групп: расхождений %d\n", bad)
	fmt.Println("  (matchH2 сверяется на «не пропустил ни одного настоящего совпадения»:")
	fmt.Println("   лишние кандидаты формуле разрешены и стоят одного сравнения ключа)")

	// 4.2. Треугольный путь обходит каждую группу ровно один раз.
	fmt.Println()
	for _, groups := range []int{4, 8, 16, 64, 1024} {
		mask := uint64(groups - 1)
		ok := true
		for start := uint64(0); start < uint64(groups) && ok; start++ {
			seen := make(map[uint64]bool, groups)
			s := makeProbeSeq(start, mask)
			for i := 0; i < groups; i++ {
				if seen[s.offset] {
					ok = false
					break
				}
				seen[s.offset] = true
				s = s.next()
			}
			if len(seen) != groups {
				ok = false
			}
		}
		fmt.Printf("  путь по %4d группам обходит каждую ровно один раз: %v\n", groups, ok)
	}
}

func setBits(mask uint64) []int {
	var out []int
	for i := 0; i < slotsPerGroup; i++ {
		if mask&(1<<(8*i+7)) != 0 {
			out = append(out, i)
		}
	}
	return out
}

func equal(a, b []int) bool {
	if len(a) != len(b) {
		return false
	}
	for i := range a {
		if a[i] != b[i] {
			return false
		}
	}
	return true
}

func contains(xs []string, x string) bool {
	for _, v := range xs {
		if v == x {
			return true
		}
	}
	return false
}

// covers — все ожидаемые слоты названы (лишние формуле разрешены).
func covers(got, want []int) bool {
	set := map[int]bool{}
	for _, g := range got {
		set[g] = true
	}
	for _, w := range want {
		if !set[w] {
			return false
		}
	}
	return true
}

// ------------------------------------------------------------ блок 5

func blockAgainstRealMap() {
	header(5, "СВЕРКА С НАСТОЯЩЕЙ КАРТОЙ")
	fmt.Println("  Внутренности воспроизведены, поэтому наблюдаемая часть обязана совпасть")
	fmt.Println("  с тем, что делает настоящая map[string]int. Проверяется то, что видно")
	fmt.Println("  снаружи: как len отвечает на вставку нового ключа, на повторную вставку")
	fmt.Println("  того же ключа и на вставку после удаления.")
	fmt.Println()

	real := map[string]int{}
	model := newTable(4)

	type op struct {
		kind string
		key  string
	}
	ops := []op{
		{"put", "alpha"}, {"put", "bravo"}, {"put", "delta"},
		{"put", "alpha"}, // повторная вставка того же ключа
		{"del", "bravo"},
		{"put", "echo"},
		{"put", "bravo"}, // вставка после удаления
	}

	mismatch := 0
	for _, o := range ops {
		switch o.kind {
		case "put":
			real[o.key] = 1
			model.put(o.key, 1)
		case "del":
			delete(real, o.key)
			model.del(o.key)
		}
		mark := "="
		if len(real) != model.used {
			mark = "РАСХОЖДЕНИЕ"
			mismatch++
		}
		fmt.Printf("  %-4s %-7s  len настоящей: %d   len модели: %d   %s\n",
			o.kind, o.key, len(real), model.used, mark)
	}
	fmt.Printf("\n  расхождений: %d\n", mismatch)
}

// ------------------------------------------------------------ подбор ключей

// findKeys подбирает ключи под нужные сценарии на настоящих хешах, а не
// подгоняет числа: перебираются короткие имена вида k0, k1, …, и берутся
// первые, у которых складывается нужная ситуация.
func candidates(n int) []string {
	out := make([]string, 0, n)
	for i := 0; i < n; i++ {
		out = append(out, fmt.Sprintf("k%d", i))
	}
	return out
}

func main() {
	fmt.Println("ВСТАВКА В КАРТУ GO: ЧТО ДЕЛАЕТ H1, А ЧТО H2")
	fmt.Println()
	fmt.Println("Алгоритм — перенос table.PutSlot из go1.24.7. Хеш-функция взята")
	fmt.Println("детерминированная (FNV-1a), потому что внутренний хеш карты сеется")
	fmt.Println("случайно при её создании и снаружи не воспроизводится. Таблица")
	fmt.Println("сокращена до 4 групп по 8 слотов; сокращена только ширина, не правила.")

	blockSplit([]string{"alpha", "bravo", "delta", "echo", "foxtrot"})

	// --- строим таблицу настоящими вставками
	header(2, "ТРАССА ВСТАВКИ")

	t := newTable(4)
	var base []string
	for _, k := range candidates(400) {
		if len(base) == 22 {
			break
		}
		base = append(base, k)
		t.put(k, 1)
	}
	fmt.Printf("  Исходное состояние: вставлено %d ключей (%s …), занято %d из 32 слотов,\n",
		len(base), strings.Join(base[:3], ", "), t.used)
	fmt.Printf("  запас роста %d. В каждом занятом слоте показана его метка — h2.\n\n", t.growthLeft)
	t.dump("  ")
	fmt.Println()

	// Собранные сценарии уезжают в блок 3 — данные для картинки.
	type shot struct {
		id    string
		title string
		base  *table
		key   string
		trace []step
	}
	var shots []shot

	// СЦЕНАРИЙ А: обычная вставка нового ключа.
	fresh := ""
	for _, k := range candidates(4000) {
		h := hash(k)
		g := int(h1(h) & t.mask)
		if matchEmpty(t.ctrl[g]) != 0 && matchH2(t.ctrl[g], h2(h)) == 0 && !contains(base, k) {
			fresh = k
			break
		}
	}
	w := t.clone()
	tr := w.put(fresh, 1)
	printTrace("СЦЕНАРИЙ А · новый ключ, в стартовой группе есть место", fresh, w, tr)
	shots = append(shots, shot{"plain", "новый ключ, в стартовой группе есть место", t, fresh, tr})

	// СЦЕНАРИЙ Б: ложное совпадение метки.
	falseHit := ""
	for _, k := range candidates(20000) {
		h := hash(k)
		g := int(h1(h) & t.mask)
		if matchH2(t.ctrl[g], h2(h)) != 0 && matchEmptyOrDeleted(t.ctrl[g]) != 0 && !contains(base, k) {
			falseHit = k
			break
		}
	}
	if falseHit != "" {
		w = t.clone()
		tr = w.put(falseHit, 1)
		printTrace("СЦЕНАРИЙ Б · метка совпала, а ключ чужой", falseHit, w, tr)
		shots = append(shots, shot{"falsehit", "метка совпала, а ключ чужой", t, falseHit, tr})
	}

	// СЦЕНАРИЙ В: повторная вставка существующего ключа.
	w = t.clone()
	tr = w.put(base[0], 42)
	printTrace("СЦЕНАРИЙ В · ключ уже есть: значение переписывается на месте", base[0], w, tr)
	shots = append(shots, shot{"update", "ключ уже есть: значение переписывается на месте", t, base[0], tr})

	// СЦЕНАРИЙ Г: стартовая группа занята целиком — путь идёт дальше.
	//
	// Первый заход брал первый попавшийся ключ, стартующий в полной группе, и
	// не проверял, нет ли такого ключа уже в таблице. Попался ключ, который в
	// ней лежал, — и вместо пути по группам напечаталось обновление на месте.
	// Отсюда условие `!inTable`: сценарий про путь, а не про совпадение.
	full := ""
	for _, k := range candidates(20000) {
		if contains(base, k) {
			continue
		}
		g := int(h1(hash(k)) & t.mask)
		if matchEmptyOrDeleted(t.ctrl[g]) == 0 {
			full = k
			break
		}
	}
	if full != "" {
		w = t.clone()
		tr = w.put(full, 1)
		printTrace("СЦЕНАРИЙ Г · стартовая группа занята целиком", full, w, tr)
		shots = append(shots, shot{"probe", "стартовая группа занята целиком", t, full, tr})
	}

	// СЦЕНАРИЙ Д: по дороге встретился удалённый слот.
	dt, dkey, dtrace := blockDeleted()
	shots = append(shots, shot{"deleted", "по дороге лежит удалённый слот", dt, dkey, dtrace})

	header(3, "ДАННЫЕ ДЛЯ КАРТИНКИ")
	fmt.Println("  Ровно то, что показывает MapInsert. Переносится в компонент как есть.")
	for _, sh := range shots {
		fmt.Printf("\n--- %s · %s\n", sh.id, sh.title)
		fmt.Println("  таблица до вставки:")
		fmt.Println(jsonSlots(sh.base))
		fmt.Println("  трасса:")
		fmt.Println(jsonTrace(sh.key, sh.base, sh.trace))
	}

	blockSelfCheck()
	blockAgainstRealMap()

	fmt.Println()
	fmt.Println("Все строки выше напечатаны этой программой. Правила — из go1.24.7:")
	fmt.Println("internal/runtime/maps/{map.go,table.go,group.go}.")
}

// blockDeleted строит расклад, в котором путь вставки ПРОХОДИТ через удалённый
// слот раньше, чем доходит до свободного. Только в таком раскладе видно правило
// из PutSlot: первый удалённый слот запоминается, но путь на нём не кончается.
//
// Расклад не рисуется байтами, а достигается настоящими вставками и удалением.
// Подобрать его пришлось по треугольному пути: из группы 1 он идёт
// 1 → 2 → 0 → 3. Значит, группы 1 и 0 должны быть полными, удалённый слот —
// в группе 2, а свободный — только в группе 3.
//
// Первый заход этого не учёл: ключ стартовал в группе 0, путь пошёл 0 → 1 → 3,
// и группу 2 с удалённым слотом просто миновал. Трасса получалась верной, но
// показывала не то правило, ради которого написана.
func blockDeleted() (*table, string, []step) {
	t := newTable(4)

	byGroup := map[int][]string{}
	for _, k := range candidates(6000) {
		g := int(h1(hash(k)) & t.mask)
		byGroup[g] = append(byGroup[g], k)
	}

	// Группы 1 и 0 — под завязку, группа 2 — под завязку и потом одно удаление,
	// в группе 3 остаётся свободное место.
	var placed []string
	fill := func(g, n int) []string {
		var got []string
		for _, k := range byGroup[g] {
			if len(got) == n {
				break
			}
			if int(h1(hash(k))&t.mask) != g {
				continue
			}
			t.put(k, 1)
			got = append(got, k)
			placed = append(placed, k)
		}
		return got
	}
	fill(0, 8)
	fill(1, 8)
	inTwo := fill(2, 8)
	fill(3, 2)

	fmt.Println("  СЦЕНАРИЙ Д · по дороге лежит удалённый слот")
	fmt.Println()
	fmt.Println("    Состояние построено вставками и удалением, а не расстановкой байтов.")
	fmt.Printf("    Группы 0, 1 и 2 заполнены целиком; в группе 2 удаляется ключ %q.\n", inTwo[3])
	t.del(inTwo[3])
	fmt.Println()
	t.dump("    ")
	fmt.Println()
	fmt.Println("    Удалённый слот помечен «удал», а не «····»: группа была полной, и")
	fmt.Println("    пометить слот свободным нельзя — это оборвало бы чужие пути поиска.")
	fmt.Println("    Свободные слоты остались только в группе 3.")
	fmt.Println()

	// Ключ, стартующий в группе 1: треугольный путь ведёт 1 → 2 → 0 → 3, то есть
	// удалённый слот встретится раньше свободного.
	target := ""
	for _, k := range byGroup[1] {
		if !contains(placed, k) && int(h1(hash(k))&t.mask) == 1 {
			target = k
			break
		}
	}
	before := t.clone()
	tr := t.put(target, 1)
	printTrace("вставка ключа, стартующего в полной группе 1", target, t, tr)
	fmt.Println("    Освободившееся место переиспользовано, и запас роста на него не потрачен.")
	fmt.Println()
	t.dump("    ")
	fmt.Println()
	return before, target, tr
}