Что делать – Telegram
Что делать
103 subscribers
209 photos
3 videos
4 files
130 links
Не смешно
Download Telegram
Как conn.getpeername() 10-15к рпс сожрал

Был отличный день для привнесения новых фич. Я и подумал: пора добавить в класс Request новый аттрибут - IP-адрес пользователя. Какого же было мое удивление, когда из-за этого, производительность упала в среднем на уже вышеупомянутые 10-15к рпс. Видимо, просто conn.getpeername() мутит под капотом что-то тёмное, что и дало мне понять: лучше так не делать
👎1
Давно заметил печальный факт, что в погоне за производительностью, я совсем потерял счёт памяти. Всё кэширование у меня работает по принципу "отдельно сам файл, отдельно кэшированный ответ с этим файлом". По сути, я хранил в каждом процессе по 2 дубликата одного и того же файла, просто во втором дубликате были ещё и хттп хедеры. Наконец, руки дошли до поработать над вебсервером, по итогу починиль - теперь пре-рендерятся только хедеры, а само тело файла уже при каждом запросе прихерячивается к заголовкам
👎1
Осталось придумать, как мне сделать нормально кэширование. Поскольку на данный момент, у каждого из процессов свой кэш со своими in-memory закэшированными файлами. Получается, до оптимизации в прошлом посте, расход памяти на кэширование был примерно 2n, где n - количество логических ядер процессора. После оптимизации, расход стал просто n, что всё ещё так-то много. В идеале, должен быть расход 1. Что-то вроде доп. процесса, который занят только кэшированием внутри себя, и отдающим кэшированные версии запросов/файлов процессам. Но учитывая, как работает multiprocessing.Queue (на IPC+Pickle, что опять таки не очень-то и шустро), это неплохо так ухудшит производительность. А этого я допустить не могу. Штош, поставлю себе в TODO, но куда-нибудь далеко
👎1
Что делать
А, новая цель. Выжать 250к RPS на том же статичном файле. #roadtothe250kRPS #родтзе250кРПС
Цель - перевыполнена. Оказалось, что самой необходимой оптимизацией - была смена парсера. http-parser оказался слишком медленным, и сменив его на httptools - производительность выросла на 100к РПС (!!!). Чутка пооптимизировав, я добился ещё 50к рпс. В итоге, имеем +150к рпс. Ну и, соответственно, меньшую задержку
👎1
Новая цель - 400к РПС на статичном файле.
#roadtothe400kRPS #родтзе400кРПС
👎1
Forwarded from Айван
Столкнулся с тем что __pycache__ директории в проекте доставляют неудобства: когда переключаюсь между git ветками, остаются папки которых нет в ветке, из-за того что в них есть незакоммиченные __pycache__.
Решил как-нибудь избавиться от них, нашёл вопрос на stackoverflow.
В комментариях предложили добавить в ~/.bashrc
export PYTHONPYCACHEPREFIX=/tmp
(/tmp папка со временными файлами, будет очищена спустя какое-то время, или при перезапуске компьютера)
👍1👎1
Я вспомнил пароль от канала

Автор перешёл на голанг, теперь буду писать заметки конкретно об этом языке.

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

Чтобы упростить дальнейшее понимание данной штуки - приведу пример на Го:

data := "Hello, world!"

for _, char := range data {
fmt.Println(char)
}

Данный код выведет построчно Hello, world!. При развёртывании же цикла, мы будем иметь нечто вроде:

fmt.Println("H")
fmt.Println("e")
fmt.Println("l")
fmt.Println("l")
fmt.Println("o")
...

И это даст нам несомненный прирост производительности! Однако же, как вы могли бы заметить, данный пример работает ой как не всегда - нужно знать конкретную длину входных данных. Тем не менее, мы можем поступить проще, сделав нечто на подобии

data := "Hello, world!"

i := 0
dataLen := len(data)

for {
fmt.Println(data[i])
fmt.Printlnb(data[i+1])
fmt.Println(data[i+2])
fmt.Println(data[i+3])

i += 4

if dataLen-i < 4 {
break
}
}

for i < dataLen {
fmt.Println(data[i])
i++
}

Что мы здесь видим? Мы совершаем в ~4 раза меньше итераций, поскольку вместо 4 итераций, мы делаем 4 операции в одной. Можно выставить как 4, так и 6, и 8, и вообще 20 как шаг. Единственное - рекомендую выставлять число, равное степени двойки.

Что по поводу выигрыша - у меня он составил около 35% при шаге в 8 и строке длиною 5,000 элементов. Что, я считаю, очень даже весомо, особенно в ситуациях, когда входных данных достаточно много (экспериментальным путём я пока не успел выяснить, при насколько малых объемах выигрыш становится не столь существенным). Да, это усугубляет читаемость. Да, из-за неё у нас появляется копипаста кода. Да, в некоторых случаях это может превратиться в кучу одинакового кода, если результат каждой операции нужно как-то обрабатывать (к примеру - обрабатывать возврат ошибки функции). И тем не менее, быстрый код - зачастую противоположность красоте, посему не рекомендую городить такие штуки в коде, в котором не настолько важна производительность.

Подводя итоги, хочу сказать, что штука действительно ситуативная. Использовать, или нет - дело сугубо каждое. Посему - спешу откланяться и поблагодарить того, кто прочитал всю эту вероятно бесполезную простыню больного шизофренией маньяка оптимизаций
👍1👎1
#ИнтересноЗнать

Возвращаясь к теме о базовых оптимизациях - нужно также знать о такой штуке, как inlining. Простым языком - в блоке кода, где вызывается функция, вместо непосредственного вызова, код самой функции вставляется прямо в наш искомый блок кода, вызывающий её. Казалось бы, в чём прикол? А прикол в том, что это очень даже неплохо так ускоряет работу программы. Правда, если мы говорим о многократном выполнении одного и того же блока кода, в котором компилятор сделал инлайнинг функции, по сравнению с тем блоком кода, где компилятор этого не сделал.

Но что я бы хотел сегодня поведать - так это о том, как это реализуется в голанге. Сразу хочу предупредить: разговор пойдет об эталонном компиляторе, разработанным Google. Важное уточнение, так как существует gccgo, который более сосредоточен на оптимизациях. И хоть эталонный компилятор также делает оптимизации, он делает их не так вдумчиво в угоду скорости компиляции (что есть чуть ли не главной фишкой рекламной компании голанга). Поэтому, например, он до сих пор не делает loop unrolling, о котором говорилось в посте выше. Также поэтому сам инлайнинг ну уж крайне ограничен. Как гласит выдержка об условиях для инлайна из документации по компилятору:

Function Inlining

Only short and simple functions are inlined. To be inlined a function must contain less than ~40 expressions and does not contain complex things like function calls, loops, labels, closures, panic's, recover's, select's, switch'es, etc.

Правда, актуально это только для 1.16, в более новых версиях свитчи и циклы всё-таки инлайнятся. Тем не менее, список до сих пор довольно обширный, что довольно таки неприятно.

Мораль такова: используя голанг, воспевать к оптимизациям компилятора не стоит. Он это не всегда хорошо умеет :)
👎1
Python Foundation stands with Ukraine💪🇺🇦
👎2
Простое объяснение CDN

Что это? Это - Content Delivery Network/Content Distribution Network, т.е. распределенная сеть серверов. Зачем она? Для того, чтобы клиент мог получать статичные данные* с географически ближайшего сервера для уменьшения задержек. Как устроена? Вот здесь немного по-подробней.

*статичные данные - могут быть как обычными файлами фронтэнда, так и музыкальными трэками (Spotify), видео-материалами (YouTube, Netflix), и любыми другими типами данных, не требующих динамического взаимодействия

Для начала, мы должны ввести два понятия:
1) Origin, он же ориджин - главный сервер, который является источником статических данных.
2) PoP (point of presence), она же точка присутствия - непосредственно сервер-репликант, доставляющий контент клиентам

И работает CDN по принципу, что оригинальные статичные данные хранятся на ориджине, откуда уже доставляются к точкам присутствия, где они кэшируются, и раздаются клиентам. И именно поэтому CDN работает разве что со статичными данными, поскольку для динамического контента в любом случае придётся обращаться к ориджину
Возвращаясь к приколам с []byte, или как быстро и без накладных приводить чары в ловеркейс

Важно: данный трюк маловероятно сработает с utf8

Ну а если у нас есть массив байт в кодировке ASCII, то для перевода всех символов в нижний регистр можно просто применить к каждому символу операцию bitwise OR со значением 0x20. Пример:

text := []byte("Hello, World!")
for i, char := range text {
text[i] = char | 0x20
}

...после чего, переменная text будет уже в себе хранить значение hello, world!. Сравнение с bytes.ToLower() см. постом ниже
👍1
Что делать
Возвращаясь к приколам с []byte, или как быстро и без накладных приводить чары в ловеркейс Важно: данный трюк маловероятно сработает с utf8 Ну а если у нас есть массив байт в кодировке ASCII, то для перевода всех символов в нижний регистр можно просто применить…
Прикладываю результаты бенчмарков по множеству просьб подписчиков. Тестирование проводилось в таком виде. Результаты следующие:

Для bytes.ToLower():
BenchmarkSTDShortLowercase-12           23080384                51.11 ns/op
BenchmarkSTDLongLowercase-12 7538932 161.1 ns/op
BenchmarkSTDShortMixed-12 19974499 59.08 ns/op
BenchmarkSTDLongMixed-12 3973327 304.0 ns/op
BenchmarkSTDShortUppercase-12 27904444 42.65 ns/op
BenchmarkSTDLongUppercase-12 5482158 220.9 ns/op

Для собственной реализации, код которой лежит в файле с бенчмарками, в функции ToLowercase():
BenchmarkOwnShortLowercase-12           41382306                29.19 ns/op
BenchmarkOwnLongLowercase-12 10526306 108.4 ns/op
BenchmarkOwnShortMixed-12 35293806 29.89 ns/op
BenchmarkOwnLongMixed-12 10962948 111.6 ns/op
BenchmarkOwnShortUppercase-12 40000665 29.95 ns/op
BenchmarkOwnLongUppercase-12 10810712 111.2 ns/op

И здесь мы видим, что в среднем, наша функция быстрее в 2 раза, нежели реализация в std-либе

Однако, почему так? Вопрос кроется в том, что bytes.ToLower() корректно работает также с utf8
Ответ кроется в том числе в исходном коде самой функции ToLower(). Как мы видим, сначала идёт итерация по входному массиву для определения того, состоит ли массив из обычных ASCII-символов (и дополнительно проверка на корнер-кейс в случае со строкой и без того в ловеркейсе). В случае, если в строке не присутствуют unicode-символы, берётся беспонтовый алгоритм со смещением, равным разнице алфавитов в нижнем и верхнем регистрах. Сравнивать производительность с нашим вариантом я не буду, так как мне лень не имеет смысла и оба варианта достаточно шустры. В результате, мы итерируемся дважды по одному и тому же массиву. И не дай боже нам встретится unicode-символ! Ибо только бог знает, как работает та функция, поскольку там творятся страшные вещи. Очень страшные вещи. Даже не пытайтесь понять, что там происходит
Крч делюсь инфой про тайпкасты 🔥
Это когда вы переводите один тип переменной в другой тип.

Например, в переменную типа byte на 8 бит не получится запихнуть значение типа ushort на 16 бит или вообще int на 32.
Логично? Логично 🌚

Так вот при тайпкасте производится сужение или же расширение конвертируемых значений.
В программировании это также называется narrowing и widening conversions.
И таким образом, в случае расширяющего преобразования, значение 4 типа byte будет представлено как 00000100 в двоичной системе.
Оно же после расширения в ushort будет выглядеть как 0000000000000100.
Значение расширилось до 16 бит (разрядов).

Однако эти операции также делятся на явные (explicit) и неявные (implicit).
Так вот в случае неявных расширяющих преобразований (implicit widening conversion), всё предельно ясно и просто.
Компилятор за нас всё перекидывал, лишь расширяя разряды.

Причем для беззнаковых типов данных меньшей разрядности к беззнаковому типу большей разрядности, он лишь добавлял биты со значением 0.
Логично?
Не думал, что буду форвардить посты Хауди хо. Но написано вроде как корректно и тема донесена простым языком
Вижу, что многие пользуются black, isort или как минимум форматируют код в PyCharm. Инструменты хорошие, но они ориентируются на устаревший PEP-8, поэтому в ближайшее время придётся подыскивать им замену.

Опубликован черновик нового стайл-гайда PEP-9001, который через какое-то время станет обязательным для соблюдения, поэтому рекомендую всем ознакомиться уже сейчас и присоединиться к обсуждению:

https://peps.pythondiscord.com/pep-9001/

#pep
🔥2
Как устроена цифровая подпись

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

RSA имеет два ключа: одним мы можем только зашифровать, но не можем расшифровать, и то же самое со вторым, только наоборот. Это даёт нам возможность без зазрения совести делиться публичным ключом, ведь расшифровать зашифрованные им данные может только владелец приватного ключа. По этому принципу, к слову, работает https (да и практически вся криптография в интернете)

А непосредственно для подписи - хэш письма шифруется приватным ключом. В результате, любой, имея публичный ключ, может расшифровать хэш и сравнить с тем, что представлено в письме, однако не имея возможности этот самый хэш изменить
🤮2
Как найти источник излишнего количества аллокаций?

go build -gcflags=“-m”. Данная команда выведет результат эскейп-анализа. Другими словами, компилятор сообщит вам, какие переменные требуют аллокации на куче, а какие - спокойно себе живут на стэке. Для более подробного отчёта, можно использовать команду go build -gcflags="-m -m”
🤔1
Префиксное дерево

…также известное, как: нагруженное дерево, и trie

Это очень похоже на бинарное дерево, только имеющее иную цель, и позволяющее держать произвольное количество листьев. Суть сводится к тому, что это ассоциативная структура данных, визуализация которой видна на пикриле. Широко используется, например, в Т9

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


const tail = byte(0)

type Leaf struct {
char byte
leaves []Leaf
}

func (l Leaf) IsTail() bool {
return getLeaf(tail, l.leaves) != nil
}

func (l Leaf) Validate(data []byte) (isValid bool) {
if len(data) == 0 {
return l.IsTail()
}

leaf := getLeaf(data[0], l.leaves)

if leaf == nil {
return false
}

return leaf.Validate(data[1:])
}

func getLeaf(char byte, leaves []Leaf) *Leaf {
for _, leaf := range leaves {
if leaf.char == char {
return &leaf
}
}
return nil
}


Данным кодом мы реализовали рекурсивный проход по префиксному древу. Как мы можем видеть, в методе IsTail() мы возвращаем логическое значение, является ли текущий узел хвостовым посредством поиска “магической” константы в списке своих узлов. Метод Validate() же возвращает логическое значение, совпадает ли поданная строка посимвольно с деревом. В случае, если поданная строка закончилось, однако дерево подразумевает продолжение (т.е. последняя ветка не является хвостовой), то такая строка тоже отсекается как невалидная.

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

Важно упомянуть, что по скорости данное решение будет не то, чтобы медленнее, а даже местами быстрее хэшмапы и сбалансированного дерева на получение по ключу. По памяти - тоже выигрыш, однако при условии, что наше дерево будет “сжато” - то есть, промежуточные узлы, ведущие к единственному не-промежуточному узлу. Например, дерево с узлами a->b->c->d|e можно сжать до вида abc->d|e, другими словами, объеденив несколько промежуточных узлов в один
👍21