Что делать
120 subscribers
209 photos
3 videos
4 files
133 links
Не смешно
Download Telegram
Сделал для тела запроса структуру, имплементирующую io.Reader. Теперь можно request.Reader(), и получить свой io.Reader. Красиво.

Правда, из-за некоторых особенностей моей потоковой работы с телом, пришлось чутка закостылить с тем, чтобы сообщать ядру о завершении обработки кусочка тела только при следующем вызове. Но работает стабильно

* Требование сообщать ядру о завершении обработки кусочка тела связана с тем, что этот кусочек тела в виде слайса байт тянется прямиком из tcp сервера, а точнее - буфера для чтения. А значит, если убрать эту синхронизацию, то получаем UB - слайс может быть перезаписан новыми данными
Итак, до беты остаётся лишь имплементировать динамический роутинг. Есть идея ставить в пути маркер {name}, где name - либо пустота, либо имя значения в контексте, которое появится в случае, если путь запроса соответствует шаблону. Поскольку маркер может стоять только между двумя слэшами, нам остаётся лишь пройтись по статичным участкам, и при наступлении маркера - схоронить значение в контекст, где концом значения является слэш (или конец строки)

Почему не регекспы? Ну, они медленные. У меня лично сейчас процесс роутинга укладывается в 80нс, при условии, что у меня получение по ключу из двух хэшмап. Регулярки же укладываются в 700-800нс
🔥2
От канала отписался один человек. И он не узнает, насколько быстро мой алгоритм матчит пути по шаблону…
🤡2
В общем, сравнение пути по шаблону я сделал, и вроде шустрое (само по себе - 80-90нс в худшем случае выдаёт). До 245нс жиреет из-за контекстов (без пропатченных - там до 700-800нс вырастает, прям как регулярки). Сейчас буду пилить префиксное дерево, потому что одним только сравнением сыт не будешь
Что делать
В общем, сравнение пути по шаблону я сделал, и вроде шустрое (само по себе - 80-90нс в худшем случае выдаёт). До 245нс жиреет из-за контекстов (без пропатченных - там до 700-800нс вырастает, прям как регулярки). Сейчас буду пилить префиксное дерево, потому…
Кстати, по поводу пропатченных контекстов - мне нужен конкретно WithValue. О нём я уже писал. Он всё ещё был медленным из-за того, что использовал any (просто any, а не как дженерик), а это интерфейс. А интерфейс под капотом хранит указатель. А указатель утекает на кучу. А утекание на кучу приводит к аллокациям

Переделал под дженерики. С 635нс производительность улучшилась до 245нс, количество аллокаций с 6-10 уменьшились до 3-5, т.е. в два раза. Пока что перемога, посмотрим, насколько префиксное дерево будет влиять на итоговый перфоманс
Я не я, если нет оптимизаций. Для роутинга я решил сделать три уровня поведения:
- Пути все статичные. В таком случае, динамический роутинг нам не нужен, мы его отключаем и фоллбекаемся к текущей реализации
- Пути динамические, но least unique string (самый короткий уникальный префикс) не включает в себя динамическую часть. Тогда мы берём из мапы по этому least unique string нужный нам шаблон, и сверяем
- Пути динамические, и least unique string вмещает в себе динамическую часть. В таком случае, берём это самое наше префиксное дерево, о реализации которого я отпишусь позже
Зашли-вышли, приключение на 20 минут
😁1
Что делать
Я не я, если нет оптимизаций. Для роутинга я решил сделать три уровня поведения: - Пути все статичные. В таком случае, динамический роутинг нам не нужен, мы его отключаем и фоллбекаемся к текущей реализации - Пути динамические, но least unique string (самый…
В общем, с least unique string я решил забить, ибо ну его нафиг. А вот по поводу префиксного дерева, я сделал следующую штуку:

Делим путь на сегменты. Сегмент - это кусочек между слэшами. Каждый сегмент может быть как статическим, так и динамическим. Если статический - мы берём из хэшмапы следующий узел, и идём дальше. А вот если динамический - кладём этот самый сегмент в контекст, и точно также идём дальше

получилось не слишком медленно, что-то около 390нс в самом длинном шаблоне, представленным у меня в тестах. Производительность всего роутера с фоллбеком к префиксному дереву я пока не мерял
А вот интереснее у меня устроен сам роутинг. Ведь если у нас только статические пути, то зачем, спрашивается, использовать префиксное дерево, если оно медленней обычной хешмапы?

Вот и я так подумал. И сделал такую штуку, что у нас динамически, при старте сервера, выбирается имплементация роутинга. Это - просто функция, которая лежит полем в структуре, и мы её вызываем, чтобы получить обработчик для запроса
https://github.com/golang/go/discussions/56010


var all []*Item
for _, item := range items {
all = append(all, &item)
}


Здесь, в all будет len(item) одинаковых указателей, равных указателю на последний элемент в item, потому что item - переменная цикла - является не per-iteration, а per-loop. То есть, переменная item у нас одна-единственная на все итерации, и при каждой итерации её значение затирается.

Это, кстати, как раз та штука, почему надо быть аккуратным с замыканиями в циклах

Кстати, чинится добавлением в теле цикла item := item

А вот ишью, кстати, советую глянуть. Интересная штука
👍1
Голанд хороший, голанд умный

Написал бенчмарки, и можно тут же запустить с профилировщиком. Полезно, теперь я знаю, насколько херовый у меня код
🥰1
Я тут заголовки переделал

В первую очередь, все значения заголовков теперь хранятся в одном большом слайсе байт. Это уменьшает нагрузку на гц за счёт того, что теперь меньше указателей и разрозненных участков в куче (не забываем, что слайс держит указатель на низлежащий массив).

Это также нам дало возможность подстроить не просто максимальную длину значения заголовка, а выделить конкретное количество пространства под значения. А значит, возможен более гибкий контроль над количеством используемой памяти

Ну и ещё изменений по мелочи, благодаря которым сервер стал в два раза меньше памяти на запрос тратить
Что делать
Я тут заголовки переделал В первую очередь, все значения заголовков теперь хранятся в одном большом слайсе байт. Это уменьшает нагрузку на гц за счёт того, что теперь меньше указателей и разрозненных участков в куче (не забываем, что слайс держит указатель…
Я ошибся. Моё решение не снижает количество указателей, ведь слайс всё равно указывает на память. Но теперь она хотя бы менее фрагментирована, тут большой слайс для всех всё же хорош
Вот что называется приятно
👍2
Вот значения заголовков у меня лежат в одном-большом пространстве, все вместе. Это решает проблему с уменьшением аллокаций, и убирает надобность в обжект пуле (реализация которого всё равно была бы сложной, либо сам обжект пул был бы тупым). Ну и можно указывать максимальное пространство, доступное для них, прикольно

Но вот с ключами я, почему-то, того же самого сделать не додумался. Я просто шёл по запросу до двоеточия, добавляя каждый символ индивидуально. Потом аллоцировал новую строку под ключ. Зачем? Не знаю

Ну сделал то же самое, что и со значениями - заводим отдельный большой байтовый массив, кладём туда из входного потока ключ, берём на эту область слайс и свапаем к строке через ансейф. И - вуаля! - количество аллокаций снижается в два раза! Да и памяти оно теперь меньше на запрос жрёт

А ещё, индюшка теперь в бете🎉
Раз уж заговорили за бету, то держите ликбез о семвере

Semver - semantic versioning. Суть данного подхода к версионированию состоит в том, что у нас есть три разряда - major.minor.patches.

Инкрементируем patches, когда сделали изменения, не добавляющие ничего нового для пользователя - внутренние изменения, не влияющие на использование. Например, оптимизация, или фикс баги

Инкрементируем minor, когда добавили новую фичу, которая НЕ ЛОМАЕТ обратную совместимость - изменения, влияющие на удобство пользования, но не ломающие существующий пользовательский код. Например, добавили новый метод для класса, делающий какую-нибудь полезную работу

Инкрементируем major, когда добавили новую фичу, которая ЛОМАЕТ обратную совместимость - изменения, ломающие существующий пользовательский код. Изменение поведения, которое может сломать существующий код, также должно инкрементировать major-версию

К семверу иногда также добавляют build number (обычно это то самое страшное пятизначное число), добавляется оно, как ещё один разряд версии - major.minor.patches.build-number

Но не номером билда едины, так ведь? А мы докинем ещё tag!. Фактически, это те самые alpha, beta, prerelease, release, и как их там только не называют. Докидывают их через слэш, major.minor.patches[.build-number]-tag

Стоит упомянуть, что при инкрементации старшего разряда, принято обнулять младшие. Но учитывая страшные пятизначные build number, не все любят обнулять все младшие разряды

А, ну и любят докидывать префиксом v - v1.0.0, например.

ПыСы: гитхаб не любит вариант v.1.0.0, лучше пишите без точки между v и мажорной версией, предпочитая v1.0.0
У меня тут память текла. И текла очень ощутимо

Текла она от подключений. А точнее даже - после их закрытия, она не освобождается. Первым делом начал грешить на таймеры в tcp сервере - я их там на каждое чтение создавал. Починил, просто обнуляя один таймер на клиента каждый раз. Сервер стал выдавать на ~10к рпс больше, но проблему это не решило

Почитав пару статей об утечках памяти, подумал - а у меня ведь 3 горутины на клиента, и все они общаются по каналам. Ну, думаю, точно где-то какая-то горутина в дедлоке остаётся. Поставил маркеры на выход из каждой горутины

И - да! Две горутины нормально себе завершали выполнение, кроме одной - httpServer.requestProcessor. Она ответственена за то, чтобы когда ядро уведомляет её о готовности запроса, она вызвала обработчик, и при окончании работы которого оповестила обратно ядро

При отключении клиента, ожидалось, что в ядро прилетит nil-слайс, и, соответственно, проверка у нас была if data == nil {}, которая слала сигнал об отключении нашей httpServer.requestProcessor. Посмотрев в tcp сервер, оказалось, что при дисконнекте, он передаёт просто пустой слайс 😃

Поменяв буквально одну строчку кода, проблема утечки памяти решилась. А вот профайлером пользоваться стоит
//go:nosplit

Думаю, все мы знаем про safe- и unsafe-участки функций. Вкратце - unsafe-участком функции считается участок, находясь в котором, гц не может очищать мусор (иначе получим кровь кишки). Такие места также называют "атомарными"

Также все мы знаем, что находясь в unsafe-участке, шедулер не может переключать горутину - даже при условии, что время пришло. И всё, что делает nosplit - указывает компилятору, что функцию не следует прерывать по таймеру. Каким образом? Тело функци целиком помечается, как unsafe-участок
Безусловные переходы

...или просто goto. Ненавистная, однако, штука у разработчиков!

Но почему же? Да потому, что в далёкие и бородатые времена, у программистов не была столь развита культура написания кода. Соответственно, народ бывало переборщивал с безусловными переходами, в следствии чего мы получали отменный спагетти-код, сваренный в аль-денте.

Но ведь сейчас всё не так, сейчас такого не будет, верно? А вот и нет. Программы, построенные на безусловных переходах, легко превращаются в goto hell: это когда непонятно, что и куда идёт.

Но ведь goto hell можно не допускать, если ты знаешь, что ты делаешь, верно? Да, абсолютно верно. Но количество кейсов, где не прибегая к гото, нам приходится костылить жуткий код с булевыми флагами, столь мизерное, что люди стараются не использовать эту штуку совсем с тех пор, когда появились процедурные языки, позволяющие писать Тьюринг-полные программы, не применяя безусловные переходы.
Продолжая тему предыдущего поста, я хочу рассказать, как я пришёл к тому, чтобы переписать парсер под безусловные переходы и получить до 2х буста производительности

Для начала, стоит рассказать, как у меня этот самый парсер вообще устроен. Кратко - это обычный for-range по слайсу байт, и вложенный большой свитч по состояниям парсера.

Занимаясь предельной оптимизацией парсера (в который у меня всё по итогу и упирается), я додумался, что вместо того, чтобы при каждой итерации делать свитч по состоянию, я могу воспользоваться тем, что состояние обычно редко меняется (проход по пути запроса, ключу/значению заголовка, etc.), а значит, свитч в теле цикла у меня лишний. А свитч-то не маленький, на 32 состояния.

Подумал, и решил вынести независимые циклы по слайсу байт в кейсы свитча по состояниям. И тут я столкнулся с проблемой: я в парсере сохраняю негласную конвенцию о написании кода - практически везде, где нужно сравнение байта с 1 и более вариантов, я пишу свитч. А как известно, break в свитче прерывает этот самый свитч. А мне надо прерывать также цикл. Идея использовать if-else цепочку меня категорически отталкивает.

И тут я вспомнил о безусловных переходах! Панацея, подумал Штирлиц. И начал вставлять лейблы в парсер. Как оказалось позже, это вам не шахматы, тут думать надо. Решил проблему кардинально: убрал полностью главный цикл, свитч по состояниям вынес в начало функции для того, чтобы исходя из последнего состояния, сделать переход в нужную часть кода. Для "одноразовых" участков (участков, которые сразу же меняют состояние дальше), правда, пришлось немного покопипастить, но в целом терпимо. Запустил бенчмарки - парсер стал от 14% до 300% быстрее. Великолепно, подумал Миллер