Есть столько интеллекта, так как есть люди. --Лу Xun.
источник
Как мы все знаем, ByteDance в основном использует Thrift для внутреннего использования.Чтобы лучше контролировать сгенерированный код, мы внедрили инструмент генерации кода Thrift в Go.
И наша старая (ши) штука (гу) началась с рефакторинга...
После мягкого рефакторинга и публикации, как только я нес сумку с компьютером и выбежал, я уже планировал вернуться и достать свой KDA Kaisha мощностью 30 Вт, чтобы убить «четверку», но деловая сторона сдержала это. у них было падение производительности на 10% после использования новой версии сгенерированного кода.
Внутренний зев: Что? ? ? Есть ли у вас другие логические изменения?Как код, который я пишу, может содержать ошибки?Как может быть разница в производительности в сгенерированном коде с одной и той же логикой?
Что ж, во избежание внезапной деактивации учетной записи в один прекрасный день, я терпеливо задал бизнес-стороне вопрос:
Итак, после ряда операций, таких как различные стандартные выравнивания, экологические выравнивания и т. д. (здесь опущено 2^10^10 слов), мы, наконец, разобрались с ситуацией:
Сгенерированный код после рефакторинга действительно на 10% хуже, чем до рефакторинга на холостом ходу со стороны бизнеса!
какие? ? ? Хотя сгенерированный код был рефакторинг, новый сгенерированный код (почти) такой же, как старый с точки зрения семантики и реализации.Как производительность может быть низкой? ? ?
Что ж, чтобы продолжить славную традицию моего большого IG не работать сверхурочно, мы решили просто закончить работу с 15 голосами - вернуть сгенерированный код обратно в старую версию. Хорошо, проблема решена. (На следующий день, HR: Сяо Ву, рассчитайся с зарплатой в финансовом отделе).
текст
Сначала прикрепите IDL, который мы использовали для объяснения сгенерированного кода:
struct Example {
1: list<list<i64>> data1,
2: map<i64, list<byte>> data2,
3: list<map<i64, byte>> data3,
}
service Serialize {
Example Method (1: Example req),
}
Во-первых, сравните старый и новый сгенерированный код (поскольку кодов много, напрямую в статье он выкладываться не будет).
старый код:gist.GitHub.com/чистый белый W U…
Новый код:gist.GitHub.com/чистый белый W U…
Видно, старый и новый коды, сгенерированные при кодировании и декодировании, полностью логически эквивалентны! Тем не менее, локальные переменные, использующие старый код, новый код представляет собой поле, соответствующее структуре напрямую. Мы подозреваем, что разница здесь не вызвана (что может привести к компенсации расчета стоимости), затем сгенерировать ассемблер для сравнения (из-за большой компиляции, не опубликованной напрямую, заинтересованной студентами генерировать свои собственные предложения о внешнем виде), найденные действительно, для вычисления смещения используется не только оператор MOVQ!
MOVQ "".p+144(SP), AX
Вроде виновника вроде нашли? Но в чем они чувствуют себя не совсем правильно, так это в том, что современный конвейер ЦП-стадии, тем более оператор MOVQ, для ЦП с многоступенчатой конвейерной архитектурой, разрыв в производительности, как бы ни был маловероятен такой большой отрыв в 10%, в частности Хотя этот оператор находится в цикле for, но в общей доле выполнения инструкций и не более 10% так.
Чтобы проверить наши сомнения, мы изменили версию сгенерированного кода, чтобы использовать временные переменные, как изначально сгенерированные, и обнаружили, что после удаления этого утверждения производительность не изменилась. Другими словами, проблема производительности не вызвана этой косвенностью.
Затем, исходя из ассемблерных различий сгенерированного кода, мы сделали множество предположений и потратили много времени на их проверку, но ни одна из них не была причиной плохой производительности (слишком грустно, чтобы ее игнорировать).
В итоге мы его нашли, потому что в новом сгенерированном коде, по сравнению со старой версией сгенерированного кода, при возврате ошибки он будет дополнительно оборачиваться:
if err := ...; err != nil {
return thrift.PrependError(fmt.Sprintf("%T read field x 'xxx' error: ", p), err)
}
А старая версия сгенерированного кода — это прямая возвратная ошибка:
if err := ...; err != nil {
return err
}
Хотя они вызываются только при возникновении ошибки и не используются в обычном процессе, на эту логику приходится значительная часть сгенерированного ассемблерного кода:
Однако компилятор Go не изменил для нас эти инструкции, что привело к значительному увеличению промахов кэша L1, когда он фактически работает, что значительно снижает производительность.См. следующие экспериментальные результаты:
В ответ на проблему, вызванную слишком умственно отсталым компилятором такого типа, ее может решить только искусственный интеллект - существует столько же искусственных интеллектов, сколько и искусственных интеллектов.
Поскольку компилятор автоматически не переупорядочивает инструкции, давайте поможем это сделать компилятору.Посмотрим сгенерированный код после завершения преобразования:gist.GitHub.com/чистый белый W U…
Более критический подход заключается в том, что мыreturn thrift.PrependError
, были изменены наgoto XXXError
,следующее:
func (p *Example) Read(iprot thrift.TProtocol) error {
var err error
var fieldTypeId thrift.TType
var fieldId int16
if _, err = iprot.ReadStructBegin(); err != nil {
goto ReadStructBeginError
}
for {
_, fieldTypeId, fieldId, err = iprot.ReadFieldBegin()
if err != nil {
goto ReadFieldBeginError
}
if fieldTypeId == thrift.STOP {
break
}
switch fieldId {
case 1:
if fieldTypeId == thrift.LIST {
if err := p.ReadField1(iprot); err != nil {
goto ReadFieldError
}
} else {
if err := iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
}
case 2:
if fieldTypeId == thrift.MAP {
if err := p.ReadField2(iprot); err != nil {
goto ReadFieldError
}
} else {
if err := iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
}
case 3:
if fieldTypeId == thrift.LIST {
if err := p.ReadField3(iprot); err != nil {
goto ReadFieldError
}
} else {
if err := iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
}
default:
if err := iprot.Skip(fieldTypeId); err != nil {
goto SkipFieldError
}
}
if err := iprot.ReadFieldEnd(); err != nil {
goto ReadFieldEndError
}
}
if err := iprot.ReadStructEnd(); err != nil {
goto ReadStructEndError
}
return nil
ReadStructBeginError:
return thrift.PrependError(fmt.Sprintf("%T read struct begin error: ", p), err)
ReadFieldBeginError:
return thrift.PrependError(fmt.Sprintf("%T read field %d begin error: ", p, fieldId), err)
ReadFieldError:
return thrift.PrependError(fmt.Sprintf("%T read field %d '%s' error: ", p, fieldId, fieldIDToName_Example[fieldId]), err)
SkipFieldError:
return thrift.PrependError(fmt.Sprintf("%T field %d skip type %d error: ", p, fieldId, fieldTypeId), err)
ReadFieldEndError:
return thrift.PrependError(fmt.Sprintf("%T read field end error", p), err)
ReadStructEndError:
return thrift.PrependError(fmt.Sprintf("%T read struct end error: ", p), err)
}
Таким образом, в нашем обычном процессе, если ошибка оценивается как ошибочная, больше нет большого сегмента инструкций по обработке, а есть только простая инструкция jmp; и соответствующая логика обработки ошибок помещает ее после возврата нормального процесса как насколько это возможно, чтобы максимально сократить количество инструкций по загрузке процессора и уменьшить количество промахов icache L1; в то же время сделать так, чтобы вся логика обработки ошибок появлялась только один раз в окончательной сборке, а не появлялась несколько раз.
Здесь должна быть волна жалоб, компилятор Go иногда будет "интимно" помогать вам переместить эти коды обратно в вышеприведенные, но поскольку это происходит только один раз, а другие места обработки ошибок будут непосредственно jmp, проблема невелика, и Последующие действия могут быть выполнены.Рассмотрите возможность выделения этой логики в отдельную функцию и отметьте noinline, чтобы увидеть, можно ли снова улучшить производительность (полностью исключив ее из основного потока).
С этой настройкой производительность perf значительно улучшилась и, вероятно, лучше, чем в более старых версиях:
The End
На данный момент эта проблема понятна.В этом процессе самый большой выигрыш:Компилятор Go настолько умственно отсталыйПерестановка инструкций вручную может принести такое большое улучшение.
Я хотел бы поделиться нашим опытом с этой статьей, надеясь представить новую идею для оптимизации производительности.В конце концов, Лу Синь однажды сказал:
Добавить Автора
Ссылка на эту статью:Woohoo.чистый белый.IO/2020/10/14/…
Уведомление об авторских правах. Все статьи в этом блоге распространяются по лицензии BY-NC-SA, если не указано иное. Пожалуйста, укажите источник!