Оптимизация компилятора Golang с искусственным интеллектом

задняя часть Go искусственный интеллект

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

Old

New

В ответ на проблему, вызванную слишком умственно отсталым компилятором такого типа, ее может решить только искусственный интеллект - существует столько же искусственных интеллектов, сколько и искусственных интеллектов.

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

Newer

The End

На данный момент эта проблема понятна.В этом процессе самый большой выигрыш:Компилятор Go настолько умственно отсталыйПерестановка инструкций вручную может принести такое большое улучшение.

Я хотел бы поделиться нашим опытом с этой статьей, надеясь представить новую идею для оптимизации производительности.В конце концов, Лу Синь однажды сказал:

Добавить Автора
Ссылка на эту статью:Woohoo.чистый белый.IO/2020/10/14/…
Уведомление об авторских правах. Все статьи в этом блоге распространяются по лицензии BY-NC-SA, если не указано иное. Пожалуйста, укажите источник!