Rebel Go: разветвление кодировки / json
Что происходит, когда меня слишком долго оставляют одну
В моем представлении разговор проходил примерно так.
Фил: я бы хотел улучшить кодировку / json. Я хотел бы сохранить выделения при маршалинге, добавив новый интерфейс для настраиваемых маршалеров.
Весь мир. Учитывая, что это можно легко изучить с помощью сторонних пакетов, это похоже на спад. Оставляем открытым на неделю для окончательных комментариев.
Фил: Интересно, как будет выглядеть успешное исследование?
Мир: одним из последствий написания собственного пакета является то, что вы можете его использовать, и вам не потребуется вносить изменения в стандартную библиотеку.
Фил: Ты только что сказал мне самому пойти форком?
Мир: Кажется, я убил Боба.
Я действительно не уверен, что хочу использовать кодировку encoding / json. Я даже не уверен, как бы я мог форкнуть часть репо, или, по крайней мере, не таким образом, чтобы я мог легко объединить исправления из апстрима. И я был бы шокирован, если бы кто-нибудь им воспользовался. Скорее всего, это будет грустная и одинокая сирота. Зачем создавать что-то с такой несчастной судьбой?
Почему? Ну, в основном потому, что сегодня суббота, @lizrice уехала на конференцию, и мне больше нечем заняться.
Я не собираюсь начинать с вилки. Я просто создам ветку и покажу, какие изменения повлекут за собой изменения и какие будут улучшения. Как всегда, я начну с теста.
В Ravelin мы передаем много JSON, и этот JSON включает поля, которые имеют значение time.Time
. Мы также приняли решение много лет назад использовать https://github.com/guregu/null
для выражения нулевых значений. На данный момент ни одно из этих решений не выглядит лучшим. Если бы мы начали с нуля, я не уверен, что буду использовать JSON, или когда-либо использовать time.Time
в качестве основного метода работы со временем, или использовать https://github.com/guregu/null
, но на данный момент эти решения очень трудно отменить.
Итак, давайте создадим эталон, отражающий эти решения. Мы маршалируем структуру, в которой есть несколько из этих элементов. На самом деле в Ravelin наши структуры имеют гораздо больше полей, чем это, но очень многие из полей имеют значения null.*
и time.Time
.
func BenchmarkEncodeMarshaler(b *testing.B) { b.ReportAllocs()
m := struct { A null.Int B time.Time C time.Time D null.String }{ A: null.IntFrom(42), B: time.Now(), C: time.Now().Add(-time.Hour), D: null.StringFrom(`hello`), }
b.RunParallel(func(pb *testing.PB) { enc := json.NewEncoder(ioutil.Discard)
for pb.Next() { if err := enc.Encode(&m); err != nil { b.Fatal("Encode:", err) } } }) }
Если мы запустим этот тест с Go 1.13 и прогоним вывод через benchstat, мы получим следующее. Да, для маршалинга этой небольшой структуры в JSON требуется 13 распределений.
name time/op EncodeMarshaler-8 753ns ± 2%
name alloc/op EncodeMarshaler-8 496B ± 0%
name allocs/op EncodeMarshaler-8 13.0 ± 0%
В Go 1.14 уже запланированы некоторые улучшения, которые позволят сократить это количество до 5 (некоторые от меня! Сообщение, сообщение, вклад, вклад). Но с грубой версией предлагаемого изменения мы можем полностью избавиться от распределения. Вот сравнение того же теста в Go tip с моей веткой с изменениями.
name old time/op new time/op delta EncodeMarshaler-8 626ns ± 3% 315ns ± 1% -49.65% (p=0.000 n=8+7)
name old alloc/op new alloc/op delta EncodeMarshaler-8 128B ± 0% 0B -100.00% (p=0.000 n=8+8)
name old allocs/op new allocs/op delta EncodeMarshaler-8 5.00 ± 0% 0.00 -100.00% (p=0.000 n=8+8)
Вот почему я считаю это хорошей идеей. Время работы сокращено вдвое, и все распределения ушли. Изменения полностью обратно совместимы и на данный момент заняли менее 3 часов (включая написание большей части сообщения в блоге).
Так какие же изменения? Что ж, начнем с добавления нового интерфейса маршалера. Идея заключается в том, что реализации MarshalAppendJSON
не нужно выделять []byte
, который она возвращает, который содержит маршалированный JSON. Он может построить свой ответ, добавив к фрагменту in
, который ему передает кодирование / json. С текущей функцией MarshalJSON
это неверно, поэтому почти каждый вызов метода MarshalJSON
приводит к выделению памяти.
type MarshalAppender interface {
MarshalAppendJSON(in []byte) ([]byte, error)
}
Когда у нас есть интерфейс, мы добавляем код, чтобы определить его присутствие для типа и назначить соответствующую функцию маршалинга. Нам нужно убедиться, что мы используем новый MarshalAppender
интерфейс вместо Marshaler
, когда он присутствует.
var ( marshalerType = reflect.TypeOf((*Marshaler)(nil)).Elem() marshalAppenderType = reflect.TypeOf((*MarshalAppender)(nil)).Elem() textMarshalerType = reflect.TypeOf((*encoding.TextMarshaler)(nil)).Elem() )
// newTypeEncoder constructs an encoderFunc for a type. // The returned encoder only checks CanAddr when allowAddr is true. func newTypeEncoder(t reflect.Type, allowAddr bool) encoderFunc { // If we have a non-pointer value whose type implements // Marshaler with a value receiver, then we're better off taking // the address of the value - otherwise we end up with an // allocation as we cast the value to an interface. if t.Kind() != reflect.Ptr && allowAddr && reflect.PtrTo(t).Implements(marshalAppenderType) { return newCondAddrEncoder(addrMarshalAppenderEncoder, newTypeEncoder(t, false)) } if t.Implements(marshalAppenderType) { return marshalAppenderEncoder }
if t.Kind() != reflect.Ptr && allowAddr && reflect.PtrTo(t).Implements(marshalerType) { return newCondAddrEncoder(addrMarshalerEncoder, newTypeEncoder(t, false)) }
Последнее изменение, заставляющее новый интерфейс работать, - это добавление кода для вызова нового метода. Это соответствует шаблону, установленному для существующего Marshaler
, за исключением того, что мы предполагаем, что MarshalAppender
реализации создают допустимый компактный JSON. Текущий Marshaler
не предполагает этого, поэтому он вызывает функцию compact
на выходе. compact
проверяет и уплотняет JSON, возвращенный из Marshaler
.
Я думаю, что лучший выбор - довериться разработчикам библиотек в создании правильных реализаций. Нам не нужно миллионы и миллиарды раз проверять вывод в работающих приложениях. Итак, моя реализация ниже просто копирует результат MarshalAppendJSON
в буфер кодировщика.
func marshalAppenderEncoder(e *encodeState, v reflect.Value, opts encOpts) { if v.Kind() == reflect.Ptr && v.IsNil() { e.WriteString("null") return } m, ok := v.Interface().(MarshalAppender) if !ok { e.WriteString("null") return } b, err := m.MarshalAppendJSON(e.scratch[:0]) if err != nil { e.error(&MarshalerError{v.Type(), err, "MarshalAppendJSON"}) return }
// We trust implementers of MarshalAppender to generate valid, maximally compact JSON e.Write(b) }
func addrMarshalAppenderEncoder(e *encodeState, v reflect.Value, opts encOpts) { va := v.Addr() if va.IsNil() { e.WriteString("null") return } m := va.Interface().(MarshalAppender) b, err := m.MarshalAppendJSON(e.scratch[:0]) if err != nil { e.error(&MarshalerError{v.Type(), err, "MarshalAppendJSON"}) return }
// We trust implementers of MarshalAppender to generate valid, maximally compact JSON e.Write(b) }
Следующим шагом, чтобы сделать это полезным, является реализация некоторых настраиваемых маршалеров. Сначала сделаем time.Time
. Для меня это очень важно - должно быть очень большое количество людей с time.Time
в структурах, которые маршалируются в JSON. Функция time.AppendFormat
упрощает реализацию этого.
// MarshalAppendJSON implements the json.Marshaler interface. // The time is a quoted string in RFC 3339 format, with sub-second precision added if present. func (t Time) MarshalAppendJSON(b []byte) ([]byte, error) { if y := t.Year(); y < 0 || y >= 10000 { // RFC 3339 is clear that years are 4 digits exactly. // See golang.org/issue/4556#c15 for more discussion. return nil, errors.New("Time.MarshalAppendJSON: year outside of range [0,9999]") }
b = append(b, '"') b = t.AppendFormat(b, RFC3339Nano) b = append(b, '"') return b, nil }
Наконец, для моего теста мне нужно создать MarshalAppendJSON
методов для Int
и String
из нулевого пакета. Int
просто из-за функции strconv.AppendInt
.
func (i Int) MarshalAppendJSON(b []byte) ([]byte, error) {
if !i.Valid {
return nullLiteral, nil
}
return strconv.AppendInt(b, i.Int64, 10), nil
}
Версия String
намного проблематичнее. Мне нужно JSON закодировать строковое значение. Но если я просто позвоню json.Marshal
, чтобы сделать это, я не получу никаких улучшений в производительности. На самом деле мне нужно было бы добавить эффективный EncodeString
метод в кодировку / json. Но и без этого, по крайней мере, новый интерфейс позволяет работать и улучшать производительность. Здесь я просто следил за определением строки JSON из json.org и сам внедрил кодировку. И это может быть даже частично верно (это определенно еще не совсем правильно)! И, по крайней мере, это показывает, что можно было бы легко реализовать эффективный метод EncodeString
.
func (s String) MarshalAppendJSON(b []byte) ([]byte, error) {
if !s.Valid {
return append(b, nullLiteral...), nil
}
b = append(b, '"')
for i, r := range s.String {
switch r {
case '\\', '"':
b = append(b, '\\', byte(r))
case '\n':
b = append(b, '\\', 'n')
case '\r':
b = append(b, '\\', 'r')
case '\t':
b = append(b, '\\', 't')
default:
if r < 32 {
b = append(b, '\\', 'u', '0', '0', hex[r>>4], hex[r&0xF])
} else if r < utf8.RuneSelf {
b = append(b, byte(r))
} else {
// append in its natural form
b = append(b, s.String[i:i+utf8.RuneLen(r)]...)
}
}
}
b = append(b, '"')
return b, nil
}
Это было все, что было необходимо для удаления всех выделений из этого конкретного теста маршалинга JSON. Надеюсь, я объяснил, почему это полезное и позитивное изменение. Думаю, я также подчеркнул, почему это должно быть сделано в стандартной библиотеке. Это действительно полезно, только если разработчики библиотеки реализуют MarshalAppender
. И я думаю, что они сделают это только в том случае, если MarshalAppender
является частью библиотеки JSON де-факто. Конечно, я не могу представить, что пакет стандартной библиотеки time
будет изменен для реализации стороннего метода кодирования JSON!
Дорогой читатель, вы зашли так далеко, но у меня есть еще одна задача. Я хочу знать, следует ли мне создать вилку encoding / json, чтобы добавить MarshalAppender
и, возможно, внести другие изменения, которые не будут приемлемы для команды Go. Я действительно не хочу заниматься этим только для собственного развлечения. Я хочу делать это только в том случае, если люди действительно будут этим пользоваться. У меня нет системы голосования, но если этот пост станет «популярным» (а мои стандарты популярности смехотворно низки), я могу просто сделать это. Так что, если вы хотите, чтобы я сделал форк-пюре с кнопкой / ретвитнуть / аплодировать / проголосовать за все, что вы обнаружили в этом посте.