Получатель метода в Go: копия, которой не видно, и «указатель быстрее», которое неверно
Собеседование по получателю идёт лестницей: что такое получатель — почему изменения не видны — почему тип «не реализует» интерфейс — когда компилятор берёт адрес сам, а когда отказывается — и во что обходится копия. Урок проходит её целиком: замер показывает, что до восьми слов разницы нет вовсе, а на килобайте она десятикратна.
Полное техническое изложение
TL;DR
Метод с получателем-значением работает с копией, метод с
получателем-указателем может изменить исходное значение. Причина простая:
получатель — это первый аргумент метода, просто записанный слева, а
аргументы в Go передаются копией. Вызов при этом выглядит одинаково: c.Inc()
компилируется и в том, и в другом случае.
Отсюда три места, где эта разница выходит наружу. Набор методов у T и у
*T разный: метод с получателем-указателем в набор методов значения не
входит — отсюда Point does not implement Stringer, проверено через reflect:
Point даёт false, *Point даёт true. Компилятор берёт адрес сам, но
только у адресуемого: c.IncPointer() на переменной работает, на элементе
карты не компилируется вовсе. И for _, v := range даёт копию элемента, так
что метод-указатель меняет её, а не срез, — компилятор при этом молчит.
«Получатель-указатель быстрее» — не правило, а зависимость от размера. Замерено на одном и том же методе: до 8 слов разницы нет (×1,01), на 32 словах ×2,71, на 128 — ×9,5. Числа сняты в одном прогоне на одной машине, поэтому содержателен ход ряда сверху вниз, а не отдельная кратность.
- у типа бывают методы, и вызывают их как
c.Inc(); - аргумент, переданный в функцию, — это копия: изменив его внутри, оригинал не изменишь;
- в Go есть указатели:
&c— адрес значения, и по нему значение можно изменить.
- что такое набор методов, адресуемость и автоматическое взятие адреса;
reflect,copylocks, побег значения в кучу, цена копии в наносекундах.
Что здесь на самом деле спрашивают
Лестница почти всегда такая:
- «Чем отличается получатель-значение от получателя-указателя?» — проверяют, скажете ли вы «копия» или ограничитесь словом «мутирует».
- «Почему мой метод ничего не изменил?» (с кодом) — та самая копия.
- «Почему тип не реализует интерфейс?» — про наборы методов.
- «Go сам берёт адрес? Всегда?» — про адресуемость; здесь чаще всего отвечают «всегда», и это неверно.
- «Что выбрать по умолчанию и почему?» — проверяют, назовёте ли вы однородность набора, а не «указатель, он быстрее».
- «Насколько быстрее?» — вопрос на честность: без числа обе крайности одинаково плохи.
Урок идёт по этой лестнице, и идёт в одну сторону: сначала семантика — меняет метод состояние или нет, что из этого следует для наборов методов и интерфейсов, — и только потом цена. Спина у лестницы одна: получатель — обычный аргумент, а аргументы копируются.
База: один вопрос, с которого начинается выбор
Метод в Go — это функция, привязанная к типу: c.Inc() вызывает Inc «на»
значении c. Само это значение называется получателем, и объявляют его
слева от имени метода: func (c Counter) Inc().
Вся тема держится на одном различии, и оно семантическое. Метод с получателем-значением работает с копией: что бы он в ней ни поменял, снаружи этого не видно. Метод с получателем-указателем получает адрес и может изменить исходное значение.
Поэтому выбор получателя решается не размером структуры и не привычкой команды, а одним вопросом про смысл метода:
Метод работает с копией значения — или должен изменить исходное состояние?
Если менять — получатель обязан быть указателем; вариантов нет. Если не менять — дальше идут остальные соображения, и они разобраны ниже: наборы методов, дерево решения и, последней, цена копии.
Почему вопрос именно такой, видно, если прочитать объявление как обычную функцию:
func (c Counter) IncValue() { c.n++ }
func (c *Counter) IncPointer() { c.n++ }Это IncValue(c Counter) и IncPointer(c *Counter). Разница ровно та же, что у
любых двух функций с такими сигнатурами, — и никакой магии в получателе нет.
Отсюда сразу два вывода:
- Получатель-значение получает копию. Увеличивать поле копии можно, смысла в этом нет: копия умрёт на выходе из метода.
- Получатель-указатель получает адрес. Копируется одно слово, а идёт оно к оригиналу.
Ловушка в том, что вызов выглядит одинаково: и c.IncValue(), и
c.IncPointer() пишутся идентично. Разница видна только в объявлении метода,
то есть в другом месте файла, а часто и в другом файле.
The rule about pointers vs. values for receivers is that value methods can be
invoked on pointers and values, but pointer methods can only be invoked on
pointers.
Правило про указатели и значения у получателей таково: методы-значения можно вызывать и на указателях, и на значениях, а методы-указатели — только на указателях.
Последняя половина этой фразы и есть источник обеих ошибок урока: и «метод ничего не изменил», и «тип не реализует интерфейс».
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования: разница между получателями — это разница между работой с копией и работой с оригиналом. Всё дальнейшее — про то, что из этого следует для интерфейсов, где компилятор подставляет адрес сам, где копия появляется незаметно и во что она обходится.
Механизм 1: набор методов — это не стилистика
Наборы методов у T и у *T разные, и разницу видно без компилятора —
через reflect:
| методы | |
|---|---|
Point | Len |
*Point | Len, String |
String объявлен с получателем-указателем, и в набор методов значения он
не попал. Проверка реализации интерфейса даёт то же самое:
реализует Stringer | |
|---|---|
Point | false |
*Point | true |
Причина не в синтаксисе, и её стоит уметь назвать: значение в интерфейсе — копия, и адреса у неё нет. Метод с получателем-указателем не смог бы изменить оригинал; тихо менять копию было бы хуже ошибки сборки. Обратное направление разрешено: у указателя адрес есть, разыменовать его для метода-значения можно всегда.
Спецификация формулирует это одним предложением:
The method set of a defined type T consists of all methods declared with
receiver type T. The method set of a pointer to a defined type T … is the set of
all methods declared with receiver *T or T.
Набор методов определённого типа T состоит из всех методов, объявленных с получателем типа T. Набор методов указателя на определённый тип T — это множество всех методов, объявленных с получателем *T или T.
Механизм 2: компилятор берёт адрес сам — но не везде
Вот место, где почти все отвечают «Go сам возьмёт адрес», и это верно только наполовину.
Прежде всего стоит сказать, чем это не является. Автоматическое взятие
адреса — удобство синтаксиса, а не изменение набора методов. Набор у T
остаётся прежним: компилятор просто дописывает & там, где имеет право. Ровно
поэтому присваивание значения интерфейсу из предыдущего раздела не спасается тем
же механизмом — там переменной, у которой можно взять адрес, уже нет.
Условие названо в спецификации прямым текстом:
if x is addressable and &x's method set contains m, x.m() is shorthand for
(&x).m()
Если x адресуемо и набор методов &x содержит m, то x.m() — сокращение для (&x).m().
Ключевое слово — адресуемо. Что адресуемо, а что нет, стоит держать списком:
| выражение | адресуемо | c.IncPointer() |
|---|---|---|
переменная c | да | работает |
поле переменной s.field | да | работает |
элемент среза xs[i] | да | работает |
элемент карты m["k"] | нет | не компилируется |
результат функции f() | нет | не компилируется |
константа, литерал Counter{} | нет | не компилируется |
У переменной адрес есть, поэтому c.IncPointer() работает. А у элемента карты
адреса нет — карта переезжает при росте, — и
m := map[string]Counter{"k": {}}
m["k"].IncPointer() // не компилируетсяне соберётся вовсе: cannot call pointer method on m["k"]. Обходной путь —
map[string]*Counter: переезжает указатель, объект стоит на месте.
И это, как ни странно, хорошая новость. Там, где адреса нет, компилятор отказывается собирать — ошибку видно сразу. Опаснее случай, где адрес есть: там компилятор молча берёт адрес копии, и об этом — следующий раздел.
Механизм 3: три места, где копия возникает незаметно
Все три — частые вопросы «найдите ошибку», и ни одно не ловится компилятором.
Числа ниже напечатаны прогоном bench/gorecv/internals.go.
Первое — range по срезу значений. Переменная цикла — копия элемента:
for _, v := range items {
v.Add() // меняет копию; срез не изменится
}
for i := range items {
items[i].Add() // меняет элемент
}Результат прогона: 0 в первом случае, 1 во втором. Это самая частая из
трёх, потому что for _, v := range пишут не задумываясь.
Второе — присваивание структуры. cp := src копирует её целиком, и дальше
это два независимых объекта: после cp.Add() в прогоне src=5 cp=6.
Третье — хранение значений там, где нужны указатели. map[string]Acc не
даст вызвать метод-указатель вовсе; []Acc даст, но через индекс, а не через
переменную цикла.
Общее у всех трёх: вызов выглядит одинаково. Компилятор берёт адрес, когда переменная адресуема, и молчит; когда адреса нет — отказывается собирать. Между этими двумя случаями и живёт ошибка.
Механизм 4: дерево решения
Правило выбора не про скорость. Вопросы задаются по порядку, и первый же ответ «да» закрывает выбор:
Три верхних вопроса — про корректность, и только четвёртый про цену. Вот те же вопросы словами:
- Метод меняет получателя? Тогда указатель — иначе он меняет копию. Это не рекомендация, а единственный работающий вариант.
- Тип содержит то, что нельзя копировать?
sync.Mutex,sync.WaitGroup,strings.Builder— копия такого типа ломается молча, иgo vetловит это отдельной проверкойcopylocks. - У части методов уже есть получатель-указатель? Тогда и у остальных пусть будет: иначе набор методов зависит от того, что именно присвоено.
if some of the methods of the type must have pointer receivers, the rest should
too, so the method set is consistent regardless of how the type is used
Если части методов типа нужны получатели-указатели, остальным тоже стоит их дать, чтобы набор методов был одинаков независимо от того, как тип используется.
И только если ни один из трёх вопросов не решил дело, остаётся размер. Хорошие
кандидаты на получатель-значение — маленькие неизменяемые типы: time.Time,
свои обёртки над числом или строкой. Там копия — это признак, а не цена.
Глубже: «указатель быстрее» — зависимость от размера, а не правило
Вот числа, ради которых стоит переключить вкладку на картинке выше. Один и тот же метод — прочитать два поля — на структурах разного размера:
| размер | байт | значение | указатель | кратность |
|---|---|---|---|---|
| 1 слово | 8 | 2,25 нс | 2,24 нс | 1,01 |
| 2 слова | 16 | 1,92 | 1,94 | 0,99 |
| 8 слов | 64 | 2,10 | 1,93 | 1,09 |
| 32 слова | 256 | 5,24 | 1,93 | 2,71 |
| 128 слов | 1024 | 19,88 | 2,09 | 9,50 |
Читать эту таблицу надо снизу вверх. Внизу видно то, о чём спрашивают: копия стоит денег, и на килобайте вызов дороже почти вдесятеро. Наверху видно то, о чём забывают: на одном-двух словах разницы нет вовсе — копировать нечего, значение и так живёт в регистрах.
Поэтому ответ «получатель-указатель быстрее» неверен как правило. Но и обратное правило — «берите значение, копия дешёвая» — неверно ровно так же: нижняя строка таблицы его опровергает.
Верно третье, и оно не правило, а зависимость: дорожает копия, и её цена растёт вместе с размером. Тело метода при этом одинаковое — читаются те же два поля. Сами кратности — свойство этого прогона на этой машине: на другой они выйдут другими, а вот направление ряда — то же.
И размер — лишь один из факторов, причём последний по важности. Указатель заставляет значение уходить в кучу чаще (об этом урок про escape-анализ), а значение даёт компилятору больше свободы. Что перевесит в конкретной функции — меряется профилем, а не выводится из таблицы.
if the receiver is large, a big struct for instance, it will be much cheaper to
use a pointer receiver
Если получатель большой — например, крупная структура, — получатель-указатель окажется намного дешевле.
Обратите внимание на слово «большой» — в самом FAQ оговорка стоит после правил про изменение и однородность, а не вместо них.
Как отвечать на собеседовании
Короткий ответ: метод с получателем-значением работает с копией, а метод с получателем-указателем может изменить исходное значение. Получатель — это первый аргумент метода, записанный слева, а аргументы копируются; отсюда и «метод ничего не изменил», и «тип не реализует интерфейс».
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «в чём разница» отвечайте «получатель — это первый аргумент». «Значение копируется, указатель — нет; всё остальное отсюда». Ответ сразу показывает, что вы не заучили два правила, а понимаете одно.
Про «метод ничего не изменил» называйте место, а не правило. «Скорее всего,
for _, v := range: переменная цикла — копия, и метод-указатель поменял её».
Это распознаётся как опыт мгновенно.
Про интерфейс говорите про адрес. «У копии в интерфейсе нет адреса, поэтому метод с получателем-указателем в набор методов значения не входит». Формулировка «надо передавать указатель» верна, но не объясняет ничего.
Про автоматическое взятие адреса добавьте оговорку. «Берёт, если выражение адресуемо; у элемента карты адреса нет, и там это не компилируется». Половина кандидатов отвечает «всегда».
Про скорость дайте диапазон, а не крайность. «Разница — это цена копии: до восьми слов её нет вовсе, на килобайте выходило вдесятеро». Одно число без второго превращает верный замер в неверное правило.
Про выбор по умолчанию назовите однородность. «Если хоть одному методу нужен указатель — указатель у всех, чтобы набор методов не зависел от того, что присвоено».
Дальше спросят
Что будет, если у типа половина методов со значением, а половина с указателем?
Скомпилируется, и в этом главная неприятность. Набор методов начинает зависеть
от того, что именно вы присвоили: T реализует один интерфейс, *T — другой.
Ошибка находится не там, где сделана, а в чужом пакете, где значение положили
в интерфейс.
Отсюда и рекомендация FAQ: однородность важнее локальной оптимизации. Исключение одно — методы, которые обязаны работать на нулевом значении типа и не менять его, но и там обычно проще сделать всё указателями.
Можно ли объявить метод на срезе или на карте?
На своём именованном типе — да: type Stack []int и дальше
func (s *Stack) Push(v int). На []int напрямую — нет: получатель обязан
быть определённым в этом же пакете типом.
Тонкость про Stack: получатель-указатель здесь нужен именно для Push, потому
что append может вернуть новый заголовок. Метод-значение получил бы копию
заголовка, положил бы в неё элемент и потерял результат — ровно та же копия,
только на три слова.
Что говорит go vet про копирование мьютекса?
Проверка copylocks ловит копию типа, содержащего sync.Locker: присваивание,
передачу в функцию по значению и — что важнее всего — получатель-значение у
метода такого типа. Сообщение вида passes lock by value.
Причина в том, что копия мьютекса не наследует его состояния: копия
заблокированного мьютекса приезжает разблокированной, и защита исчезает без
единой ошибки. go vet входит в go test, так что эта проверка обычно уже
работает у вас в CI.
Метод можно вызвать на nil-указателе?
Да, и это законно: метод с получателем-указателем получает nil как обычное
значение. Падение произойдёт только при разыменовании поля, а если метод к полям
не обращается — отработает нормально.
На этом даже строят идиомы: у дерева метод func (t *Node) Size() int с ранним
if t == nil { return 0 } избавляет вызывающего от проверок. А ещё отсюда
понятно, почему *Point реализует Stringer даже когда указатель нулевой:
набор методов — свойство типа, а не значения.
Как получатель связан с ловушкой типизированного nil?
Напрямую. Если Error() объявлен с получателем-указателем, то error
реализует *MyErr, а не MyErr — значит, в интерфейс кладут указатель. А
нулевой указатель в интерфейсе даёт тот самый err != nil при отсутствии
ошибки.
То есть две темы — набор методов и два слова интерфейса — сходятся в одной
ошибке. Получатель решает, что попадёт в интерфейс, а устройство интерфейса
решает, как это сравнится с nil.
Стоит ли делать получатель-указатель ради «оптимизации» маленькой структуры?
Замер говорит нет: до восьми слов разницы нет вовсе. А цена у такого решения есть — указатель может заставить значение уехать в кучу, и тогда вы меняете бесплатную копию на выделение памяти и работу сборщика.
Поэтому порядок такой: сначала обязательное (изменение, некопируемые поля, однородность), потом размер. «Указатель на всякий случай» — это оптимизация наугад, и она бывает отрицательной.
Частые заблуждения
получатель — особая конструкция языка
Это первый аргумент, записанный слева от имени метода. func (c Counter) Inc() — то же самое, что func Inc(c Counter). Отсюда сразу всё: аргументы копируются, значит, получатель-значение получает копию.
метод-указатель нельзя вызвать на значении
Можно — если значение адресуемо: компилятор сам подставит (&x).m(). Не выйдет только там, где адреса нет: элемент карты, результат функции, значение в интерфейсе. Формулировка «нельзя» скрывает как раз то условие, о котором и спрашивают.
Go всегда сам возьмёт адрес
Только у адресуемого выражения. m["k"].IncPointer() на map[string]Counter не компилируется: карта переезжает при росте, и адреса у элемента нет. Лечится map[string]*Counter.
получатель-указатель быстрее
Это зависимость от размера, а не правило. Замерено на одном и том же методе: 1 слово — ×1,01, 2 слова — ×0,99, 8 слов — ×1,09, 32 слова — ×2,71, 128 слов — ×9,50. До восьми слов разницы нет, потому что копировать нечего.
в for _, v := range можно менять v
Можно, но это копия элемента, и срез не изменится: прогон даёт 0 вместо 1. Компилятор молчит, потому что v адресуема и метод-указатель к ней применим. Менять надо через индекс: items[i].Add().
если у типа есть нужный метод, он реализует интерфейс
Только если метод входит в набор методов того, что присваивают. Через reflect это видно прямо: Point реализует Stringer — false, *Point — true, при том что метод объявлен один.
смешивать получатели у одного типа нормально
Скомпилируется, но набор методов начнёт зависеть от того, что присвоено: T будет реализовывать один интерфейс, *T — другой. FAQ языка советует однородность прямо, и причина практическая: ошибка находится не там, где сделана.
метод на nil-указателе всегда паникует
Не всегда: получатель-указатель принимает nil как обычное значение, и паника случается только при разыменовании поля. На этом строят идиомы вроде func (t *Node) Size() int с ранним if t == nil.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
func (c Counter) IncValue() { c.n++ }
func (c *Counter) IncPointer() { c.n++ }
c := Counter{}
c.IncValue()
c.IncPointer()
fmt.Println(c.n)
cs := []Counter{{}, {}}
for _, v := range cs {
v.IncPointer()
}
fmt.Println(cs[0].n)
for i := range cs {
cs[i].IncPointer()
}
fmt.Println(cs[0].n)
p := &Counter{}
p.IncValue()
fmt.Println(p.n)Практика · оцените
Проверка знаний
items — это []Acc. Что сделает for _, v := range items { v.Add() }, если Add объявлен с получателем-указателем?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Метод с получателем-значением работает с копией, метод с получателем-указателем может изменить исходное значение. Причина простая: получатель — это первый аргумент метода, просто записанный слева, а аргументы в Go передаются копией. Вызов при этом выглядит одинаково:
c.Inc()компилируется и в том, и в другом случае. - Отсюда три места, где эта разница выходит наружу. Набор методов у
Tи у*Tразный: метод с получателем-указателем в набор методов значения не входит — отсюдаPoint does not implement Stringer, проверено черезreflect:Pointдаёт false,*Pointдаёт true. Компилятор берёт адрес сам, но только у адресуемого:c.IncPointer()на переменной работает, на элементе карты не компилируется вовсе. Иfor _, v := rangeдаёт копию элемента, так что метод-указатель меняет её, а не срез, — компилятор при этом молчит. - «Получатель-указатель быстрее» — не правило, а зависимость от размера. Замерено на одном и том же методе: до 8 слов разницы нет (×1,01), на 32 словах ×2,71, на 128 — ×9,5. Числа сняты в одном прогоне на одной машине, поэтому содержателен ход ряда сверху вниз, а не отдельная кратность.
На самом деле
- Это первый аргумент, записанный слева от имени метода.
func (c Counter) Inc()— то же самое, чтоfunc Inc(c Counter). Отсюда сразу всё: аргументы копируются, значит, получатель-значение получает копию. - Можно — если значение адресуемо: компилятор сам подставит
(&x).m(). Не выйдет только там, где адреса нет: элемент карты, результат функции, значение в интерфейсе. Формулировка «нельзя» скрывает как раз то условие, о котором и спрашивают. - Только у адресуемого выражения.
m["k"].IncPointer()наmap[string]Counterне компилируется: карта переезжает при росте, и адреса у элемента нет. Лечитсяmap[string]*Counter. - Это зависимость от размера, а не правило. Замерено на одном и том же методе: 1 слово — ×1,01, 2 слова — ×0,99, 8 слов — ×1,09, 32 слова — ×2,71, 128 слов — ×9,50. До восьми слов разницы нет, потому что копировать нечего.
- Можно, но это копия элемента, и срез не изменится: прогон даёт
0вместо1. Компилятор молчит, потому чтоvадресуема и метод-указатель к ней применим. Менять надо через индекс:items[i].Add(). - Только если метод входит в набор методов того, что присваивают. Через
reflectэто видно прямо:PointреализуетStringer— false,*Point— true, при том что метод объявлен один. - Скомпилируется, но набор методов начнёт зависеть от того, что присвоено:
Tбудет реализовывать один интерфейс,*T— другой. FAQ языка советует однородность прямо, и причина практическая: ошибка находится не там, где сделана. - Не всегда: получатель-указатель принимает
nilкак обычное значение, и паника случается только при разыменовании поля. На этом строят идиомы вродеfunc (t *Node) Size() intс раннимif t == nil.
Что разобрано
- Что здесь на самом деле спрашивают
- База: один вопрос, с которого начинается выбор
- Механизм 1: набор методов — это не стилистика
- Механизм 2: компилятор берёт адрес сам — но не везде
- Механизм 3: три места, где копия возникает незаметно
- Механизм 4: дерево решения
- Глубже: «указатель быстрее» — зависимость от размера, а не правило
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
3 ИСТОЧНИКА
- Спецификация Go — Method sets, Method declarations, Calls, SelectorsОфициальная документация. Правило, из которого выводится вся тема: «The method set of a defined type T consists of all methods declared with receiver type T. The method set of a pointer to a defined type T … is the set of all methods declared with receiver *T or T» (Набор методов определённого типа T состоит из всех методов, объявленных с получателем типа T. Набор методов указателя на определённый тип T … — это множество всех методов, объявленных с получателем *T или T). И про то, когда компилятор берёт адрес сам: «A method call x.m() is valid if the method set of (the type of) x contains m … if x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m()» (Вызов метода x.m() допустим, если набор методов (типа) x содержит m … если x адресуемо и набор методов &x содержит m, то x.m() — сокращение для (&x).m()).https://go.dev/ref/spec#Method_sets
- Go FAQ — Should I define methods on values or pointers?Официальная документация. Ответ авторов языка на тот самый вопрос собеседования. Про изменение: «if the method needs to mutate the receiver, the receiver must be a pointer» (если методу нужно изменять получателя, получатель обязан быть указателем). Про однородность: «if some of the methods of the type must have pointer receivers, the rest should too, so the method set is consistent regardless of how the type is used» (если части методов типа нужны получатели-указатели, остальным тоже стоит их дать, чтобы набор методов был одинаков независимо от того, как тип используется). И оговорка про эффективность: «if the receiver is large, a big struct for instance, it will be much cheaper to use a pointer receiver» (если получатель большой — например, крупная структура, — получатель-указатель окажется намного дешевле).https://go.dev/doc/faq#methods_on_values_or_pointers
- Effective Go — Pointers vs. ValuesОфициальная документация. Про то, что правило про адресуемость — не деталь реализации, а язык: «The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers» (Правило про указатели и значения у получателей таково: методы-значения можно вызывать и на указателях, и на значениях, а методы-указатели — только на указателях).https://go.dev/doc/effective_go#pointers_vs_values