Некое продолжение статьи Go и Protocol Buffers толика практики (или быстрый старт, для тех кто ещё не знаком). Процессы кодирования/декодирования в определённые форматы в Go тесно связяны с рефлексией. А как мы с Вами, дорогой читатель, знаем — рефлексия — это долго. О том какие методы борьбы существуют эта статья. Думаю что, искушённые вряд ли найдут в ней, что-либо новое.
Корень
Собственно, в упомянутой статье рассказывалось о пакетах github.com/golang/protobuf/{proto,protoc-gen-go}
. Что с ними не так? Именно, то, что используется рефлексия. Предположим у Вас существует проект, который работает с определённы набором структур. И эти структуры то и дело кодируются в Protocol Buffers и обратно. Если б это были всегда разные, непредсказуемые типы, то нет проблем. Но если набор известен заранее, совершенно незачем использовать рефлексию. Как Вам известно, принято использовать некоторый интерфейс, котроый отвечает за кодирование. Вот например кусочек из encoding/json
:
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
Референс: Marshaler, Unmarshaler.
Если кодировщик встречает тип воплощающий один из этих интерфейсов, то в этом случае вся работа возлагается на их методы.
type X struct {
Name string,
Value int,
}
func (x *X) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"name": %q, "value": %d}`, x.Name, x.Value))
}
Не всегда (Un)Marshaler
выглядят так радужно. Вот например тут есть почитать про yaml (англ.) и вообще по этой теме.
Ключ
Решение как всегда простое. Использовать другой пакет:
go get github.com/gogo/protobuf/{proto,protoc-gen-gogo,gogoproto,protoc-gen-gofast}
Эти пакеты просто добавляют удобства и ускорение.
О пакете (ссылки):
Как Вы можете убедиться ускорение от 1.10x и выше. Есть возможность просто использовать набор расширений — без ускорения. Есть возможность просто ускорить. Я остановился на этой команде:
protoc
--proto_path=$GOPATH/src:$GOPATH/src/github.com/gogo/protobuf/protobuf:.
--gogofast_out=. *.proto
и Вы получите и расширения (если есть) и ускорение.
syntax="proto3";
package some;
//protoc
// --proto_path=$GOPATH/src:$GOPATH/src/github.com/gogo/protobuf/protobuf:.
// --gogofast_out=. *.proto
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
// для тестов, создаёт метод Equal, проверять идентичность
option (gogoproto.equal_all) = true;
option (gogoproto.goproto_stringer_all) = false;
// Stringer для всех (для тестов нужно это расширение)
option (gogoproto.stringer_all) = true;
// для тестов - наполнение случайными значениями
option (gogoproto.populate_all) = true;
// генерация набора тестов
option (gogoproto.testgen_all) = true;
// набор бенчмарков
option (gogoproto.benchgen_all) = true;
// нужно
option (gogoproto.marshaler_all) = true;
// размер сообщения
option (gogoproto.sizer_all) = true;
// нужно
option (gogoproto.unmarshaler_all) = true;
// enums, не важно - это для красоты
option (gogoproto.goproto_enum_prefix_all) = false;
enum Bool {
Yes = 0;
No = 1;
DontCare = 2;
}
message Some {
option (gogoproto.goproto_unrecognized ) = false;
option (gogoproto.goproto_getters) = false;
Bool Waht = 1;
int64 Count = 2;
bytes Hash = 3;
}
получится
/*
большая часть выпилена
там ещё куча методов (Size, String и т.д.) и ещё один файл с тестами
*/
type Bool int32
const (
Yes Bool = 0
No Bool = 1
DontCare Bool = 2
)
// ...
type Some struct {
Waht Bool `protobuf:"varint,1,opt,name=Waht,proto3,enum=some.Bool" json:"Waht,omitempty"`
Count int64 `protobuf:"varint,2,opt,name=Count,proto3" json:"Count,omitempty"`
Hash []byte `protobuf:"bytes,3,opt,name=Hash,proto3" json:"Hash,omitempty"`
}
// воплощение интерфейса proto.Message (github.com/golang/protobuf/proto)
func (m *Some) Reset() { *m = Some{} }
func (*Some) ProtoMessage() {}
// собственно вот
func (m *Some) Marshal() (data []byte, err error) {
// ...
}
// и вот
func (m *Some) Unmarshal(data []byte) error {
// ...
}
Как Вы можете заметить, некоторые расширения имеют статус beta, да ещё это замечание по-поводу proto3. Не сомневайтесь. Этот пакет успешно используют многие (см. домашнюю страницу). Всё же от написания тестов это не освобождает. Если не интересуют расширения и прочее, то (как это отмечено в README проекта) вот этой команды будет достаточно:
protoc --gofast_out=. myproto.proto
Камни
ложка дёгтя
Если Вы не заглянули в предыдущий спойлер, то один из его фрагментов я хотел бы подчеркнуть, вот он
func (m *Some) Reset() { *m = Some{} } // очень грубо
Дело в том, что gogo
позволяет генерировать «быстрые» структуры. При этом использовать их можно и со «старым» github.com/golang/protobuf/proto
. При этом будут использоваться методы Marshal
и Unmarshal
— в этом нет проблемы. Но что если Вы используете один и тот же экземпляр структуры много раз. Если структура большая (нет, огромная), то по большому счёту не помешало бы использовать пул и сохранять «отработанные» структуры, а потом извлекать их обратно — использовать повторно.
Подход github.com/golang/protobuf/proto
. Референс.
func Unmarshal(buf []byte, pb Message) error {
pb.Reset() // акцент на этом
return UnmarshalMerge(buf, pb)
}
Вызов Reset
. А следовательно из *m = Some{}
— старая структура выбрасывается, новая создаётся. Эта структура маленькая — плевать — но хотелось бы сохранить Hash []byte
(имею ввиду выделенную память), на тот случай если используется бо-о-ольшой хэш.
Подход github.com/gogo/protobuf/proto
аналогичен — «копипастен». Ни проблеска.
Ну что ж. Можно попробовать использовать метод Unmarshal
напрямую или UnmarshalMerge
— просто добавить свой MyReset
метод, урезать длину слайса — оставить ёмкость. Нет! Вот строка из сгенерированного Unmarshal
:
m.Hash = append([]byte{}, data[iNdEx:postIndex]...)
Новый слайс создаётся — старый летит в топку GC. Собственно если у Вас небольшие структуры (поля структур — и всё вместе тоже) — то проще всего не париться. Для больших — искать обходные пути (читай переписывать генерированный код). При текущей реализации использовать пул не имеет смысла.
Бонус
Библиотека удобная для стримминга. Запись сообщений в io.Writer
, чтение из io.Reader
— такой велосипед уже существует.
Раз уж зашёл разговор о json
: github.com/pquerna/ffjson. Аналогично для json. Не просто генератор — а швейцарский нож для json
+ Go
.
Раз уж зашёл разговор про скорость и про пул: github.com/valyala/fasthttp. «Быстрая» замена net/http
. Ускорение за счёт повторного использования памяти. И то же с дополнительными возможностями.
Автор: deep_orange