TGTGInsightintelligence telegramLIVE / telegram public index
← Experimental chill
Experimental chill avatar

TGINSIGHT POST

Post #219

@experimentalchill

Experimental chill

Visualizzazioni9,500Numero di visualizzazioni
Pubblicato17 gen17/01/2023, 12:58
Contenuto del post

Contenuto

Мы тут выложили Protobuf Table-Driven Parser. Основная идея, что для каждого отдельного типа Protobuf мы генерировали огромный кусок кода, и он рос линейно/супер линейно с количеством полей в протобуфе. Рост происходил из-за того, что компилятору мы давали код в духе auto [tag, wire_format] = ReadTag(ptr); if (tag == kMessageProtoTagNumber) { ptr = ParseMessage(ptr); } if (tag == kIntProtoTagNumber) { ptr = ParseVarInt32(ptr); } И так далее. В итоге был огромный кусок кода и компилятор уставал аллоцировать регистры, делать push, pop регистров при входе/выходе из вложенных функций, компиляция росла. Я писал об этом аж 1.5 года назад https://t.me/experimentalchill/95 про аттрибут [[musttail]]. И теперь то самое начало давать свои плоды. Так как формат фиксированный, мы можем просто брать функцию из значения тега и вызывать её, сохраняя все регистры и заставив компилятор не делать push/pop. 6 параметров регистров мы спрятали PROTOBUF_TC_PARAM_DECL. #define PROTOBUF_TC_PARAM_DECL \ ::proto2::MessageLite *msg, const char *ptr, \ ::proto2::internal::ParseContext *ctx, \ ::proto2::internal::TcFieldData data, \ const ::proto2::internal::TcParseTableBase *table, uint64_t hasbits В итоге оно выглядит как data.read_tag<type>(); data.read_data<type>(); ptr += size_read; SetField(data.offset(), ptr); has_bits |= 1 << data.index_idx(); ReadInstruction() dispatch to next Instruction from table Пример можно посмотреть здесь. Плюсы, которые мы получили: 1. Мы сильно уменьшили размер генерируемого кода и время компиляции. Теперь нам нужна только таблица с инструкциями что делать для того или иного поля 2. У нас нет переполнения стека из-за отсутствия push/pop, теперь мы поддерживаем сообщения любой вложенности 3. По перфу нейтрально -- выиграли в push/pop, проиграли в статичности функций 4. Легче понимать профили бинарей, что они парсят на уровне сообщений 5. Только 1 имплементация парсинга на всех 6. Мы сможем менять формат протобуфов даже на ходу Как мы сможем его менять? Скажем, при типе int32 мы пишем varint. Если число отрицательное, мы пишем 5 байт из-за того, что отрицательные числа наполнены единицами. А что если мы хотим написать его в типе sint32, чтобы писать меньше? В прошлые разы мы генерировали парсинг на основании типа и формата поля, то есть цикл парсинга мог воспринимать тип только строго. Теперь мы можем писать int32, скажем, как 1. Если положительное, просто в varint 2. Если отрицательное, в отрицательном varint 3. Цикл парсинга всё поймёт, так как привязки к типу нет, и число в память напишется как напишется 4. Мы сэкономили байты и не проиграли в парсинге Такие оптимизации можно делать даже на ходу -- алгоритм сериализации можно подменить так же. Проблема в том, что очень много старых версий протобуфов, где логика парсинга строго привязана к типу. Она была привязана к типу из-за перфа, не очень хотелось писать код, который парсит int32 в зависимости от потенциально разных wire formats, теперь -- это не важно и по дизайну не медленнее старого подхода. Пройдёт 3-5 лет, и мы сможем так делать по дефолту (build horizon). Пока -- под опцией.