Что делать
119 subscribers
209 photos
3 videos
4 files
133 links
Не смешно
Download Telegram
Оказывается, на маке нельзя ничего ручками (кроме чтения) сделать с /usr/bin - даже с судо. Видите ли, политики безопасности (ну да, если у малвари судо, то он без /usr/bin все равно ничего сделать не сможет, правда ведь?). Собственно, задача: куда без переустановки компилятора, добавить исполняемый файл go, чтобы он был доступен по всей системе, а не только в интерактивном шелле zsh?
💩1
Еще про оптимизации.

Иногда, чем проверять символ на вхождение в множество других символов посредством свитча,
switch char {
case 'a', 'b', 'c': ...
}
дешевле сделать лукап по массиву.

То есть, эксплуатируя особенность ascii-кодировки, в которой каждый символ закодирован строго одним байтом, мы можем просто завести [256]bool. Константа 256 - максимальное значение uint8 (алиасом на который byte, собственно, и является). В таком случае, заполняя массив следующим образом, мы можем получить значение, лежащее по индексу, равному значению символа. То есть, if table['a'] { ... }

Преимущество перед свитчом это, правда, начинает иметь только начиная с определенного момента. Авторы json-iter, например, решили, что свитч с числами на 16 кейсов уже будет дороже, чем лукап по массиву.

А, и да. Это вполне себе можно считать идеальной хэш-мапой:)
Раз уж зашла тема, то стоит еще обсудить мапу.

По О-нотации она довольно эффективна (в среднем сложность константная), отнюдь на практике достаточно дорогая в некоторых ситуациях. Например, у меня в парсере обработка заголовков - это довольно интенсивная как запись, так и чтение, потому что значений у заголовка может быть несколько - приходится держать их всех в слайсе.

И парсер был довольно медленным. Профайлер показывал, что бОльшая часть времени уделяется операциям с хэшмапой. Поэтому я решил попробовать заменить ее просто на []string, где четный индекс всегда ключ, а нечетный - всегда значение заголовка. Теперь асимптотика стабильно линейная, что является регрессией. Но в абсолютных числах - в бенчмарках с большим количеством заголовков, он стал в среднем в два раза быстрее (но чем заголовков больше, тем больше и разрыв).

Но почему так? Потому что при интенсивной записи, не очень интенсивном чтении и небольшом количестве ключей (лимит - 50 заголовков, в среднем 10-20), обычный перебор будет быстрее. Но тенденция сохраняется до 20-30 элементов, далее скорость начинает линейно отставать от в среднем константной у хэшмапы (но и с 50 элементами перебор не намного медленнее хэшмапы). Также заметную часть расходов удалось убрать за счет отсутствия операции очищения мапы.

Но в целом, обычный одномерный слайс строк просто-напросто более удачная структура данных по части абсолютной производительности для конкретного кейса¯\_(ツ)_/¯
Хороший трелло, и таски интересные
👍1
И снова сказ о том, как я парсер свой оптимизировал.

Решил я из интереса написать http-forwarder, для этого мне нужно из запроса достать заголовки Host, Content-Length и Transfer-Encoding (Host чтобы знать, кому перенаправлять; Content-Length и Transfer-Encoding - чтобы знать, что делать с телом запроса). Решил я для этого использовать bytes.IndexByte(). Результат (после пары часов пост-оптимизирования, конечно же) - 11.7gb/s throughput.

Неплохо. Посмотрел на свой парсер в индиге - 1.7гб/с. Уныленько. Переломал все к чертям, позаменял на bytes.IndexByte(). Стало 4.5гб/с. Уже лучше, хоть и все еще уныленько - но эту проблему я пока решаю.

Так а в чем же прикол, собственно? Да суть вся в том, что bytes.IndexByte() для х86 архитектур использует SIMD, позволяя, тем самым, обходить строки гооораздо быстрее. Собственно, весь выигрыш и сводится к тому, насколько грамотно получится вкрутить эту штуку себе в код. В парсере для http-forwarder'а, например, большую часть данных просто пропускают мимо ушей, потому что основных (и самых важных заголовков) - очень маленькое подмножество, благодаря чему и получается достичь относительно высокой производительности. В индиге отставание в практически 3 раза, но уже потому, что приходится гораздо больше данных копировать - все же тут парсеру приходится быть немного более general-purpose, и извлекать больше деталей из запроса. А, ну и путь запроса много сжирает, да.
в семье все должно быть поровну
Интерпретатор, совместимый с go1.19 и go1.20. Интересный проект, чтобы быстро проверить идею - отнюдь никаких оптимизаций не предусмотрено, и даже никакой виртуальной машины для исполнения нет
👍1
С юбилеем меня
👍2
Начал замечать, что с тех пор, как добавили женерики - начали добавлять и интересные пакеты. Вроде все того же slice, арены (отнюдь не уверен во взаимосвязи), итераторы планируют. Может, пустые интерфейсы - и правда не настолько хорошее решение by design?
👏1
А в чем прикол бустить каналы премиум-подписками?
Что делать
А в чем прикол бустить каналы премиум-подписками?
Теперь и с каналов ливать. Ну что ж ты будешь делать
😁1🐳1
Переписываю gzip-декомпрессор из стд либы (пришлось, потому что иначе очень неудобно использовать в моем случае). Тут есть такая штука: если стоит флаг extra - следующие два байта после заголовков - длина данных, и непосредственно сами данные. Так вот: возможно, это не такой уж и hot-path, но на это все равно аллоцируют каждый раз новый слайс.

Собственно, а в чем вопрос? Так можно ведь буфер потом переиспользовать (в худшем случае - реаллоцировать под нужный размер, но это никак не будет отличаться от текущего решения). Просто сохранить его в структуре Reader. Ридер ведь переиспользуют! Я и уверен, что много кто и переиспользует, и хуже это явно не сделает. Так почему? Почему так?
Forwarded from Ivan Sokolov
в C++20 можно легально заставить работать такой синтаксис:
5_m + 2 // вызывает ваш произвольный operator+. суффикс может быть любым начинающимся с _
Как я оптимизировал flate на 5-10мб/с

Пришлось копаться с внутренностями стандартного gzip компрессора, и он под капотом flate использует (что логично, ведь gzip - это просто надстройка в виде пары заголовков для flate-потока). Там, в декомпрессоре, было одно интересное поле - step func(*decompressor) . Читай - указатель на функцию. Туда подставлялся метод декомпрессора, который должен быть использован для следующего шага (и делал он это довольно странным образом).

Собственно, почему это странно? Потому что вызов функции по указателю (а не по идентификатору) - это indirect call, и тут уже дело конкретно в железе. Потому что для совершения такого вызова, процессору сначала придется обратиться по адресу в памяти, на который указывает наш func ptr. В С, например, компилятор может соптимизировать это так, чтобы разницы в производительности не было (предварительно положив адрес в регистр).

Да вот только мы не в С :). Я даже попробовать сделать бенчмарк - и правда, обычный вызов справляется за 0.2нс. В то время, как indirect-вариант - за 1.1нс. Стоит сделать помарку: 0.2нс - это всегда подозрительно, и, скорее всего, так и есть, потому что в измеряемой мною функции сразу производился возврат, и результат никуда не присваивался - компилятор мог просто-напросто вырезать такой вызов. Поэтому я повторил с флагом -N (дабы отключить оптимизации), и результат стал 1.1нс и 1.9нс соответственно. То есть, разница все еще присутствует.

Так какой из этого можно сделать вывод? Вызов по func ptr дороже. В пределах пикосекунд, но есть. И именно это и сыграло роль (хоть и минимальную, особенно смотря на коэффициент).

Вот ссылка на мой PR, если интересно. Можете посмотреть там сравнения до и после патча. А еще, там есть интересный момент, связанный с занулением статического массива:)
🔥1
func sum(a, b int) (c int) {
с = a + b
defer func() {
с++
}()

return
}


Что вернет эта функция в случае sum(5, 5)? Для многих будет очевидно, что 11 - записали в c сумму, и в дефере инкрементировали. Окей, а если так?

func sum(a, b int) (c int) {
result := a + b
defer func() {
result++
}()

return result
}


казалось бы, ситуация практически 1 в 1. А нет: здесь дефер уже никак не влияет на возвращенный результат сложения. Почему так?

Давайте рассмотрим еще один, показательный случай:

func sum(a, b int) (c int) {
result := a + b
defer func() {
с++
}()

return result
}


Что мы имеем? Складываем а и б в переменную result, после чего возвращаем ее. А в дефере мы инкрементируем, казалось бы, ничего общего не имеющую с результатом возврата переменную с. Но что мы видим?

Ba-dum-tsss. Имеем снова sum(5, 5) == 11! Вот это уже интересно. А теперь давайте разберемся, что же все-таки это такое, именованный возврат?

Начать стоит с того, что у нас есть стэк. При вызове функции, мы аллоцируем новый стэкфрейм функции. При возврате, мы пишем значение в стэкфрейм caller'a и делаем jmp к инструкции, на которой тот остановился. Соответственно, когда мы возвращаем во втором примере переменную result, она копируется туда, где потом будет использоваться на месте вызова. И как раз туда, куда она копируется, и указывает с!

Поэтому, в последнем примере, когда мы инкрементируем с - он уже равен result, хоть мы явно этого и не делали. Получается, мы инкрементируем значение прямо на стэкфрейме caller'a. Это, к слову, также дает нам ответ на вопрос, почему при именованном возврате из функции можно сделать просто пустой return.

А теперь самое (не)интересное: это как-то влияет на перфоманс?

Я не знаю.