Go и Protocol Buffers, ускорение

Некое продолжение статьи 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.
Если кодировщик встречает тип воплощающий один из этих интерфейсов, то в этом случае вся работа возлагается на их методы.

Простой json-пример
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. Ускорение за счёт повторного использования памяти. И то же с дополнительными возможностями.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

© Habrahabr.ru