Pull to refresh

Comments 40

a := "" + string(foo) + string(bar)

b := "" + string(foo) + string(bar)

У вас две одинаковые строчки, хотя, наверное, подразумевалось по-другому

В первой строчке подразумевался символ _ внутри кавычек

Все верно, опечатка в статье. Спасибо, исправил

Мне одному кажется что если

наша цель — экономия наносекунд, так как функция может вызываться тысячи и миллионы раз

то тут уже не Go нужен?

Ну, если есть возможность каким-либо образом оптимизировать горячие места в коде, а остальное оставить как есть - то почему бы и не Go.

Намного интереснее в итоге сравнить выхлоп от этого решения в деньгах против затрат на поиск и поддержку. Ну или хотя бы итоговые числа времени работы метода. А то как бы оптимизация это хорошо, а потом у тебя поход в бд сожрёт 99% времени.

Звучит это всегда очень легко….но….

У самого монолит на Go и борьба за наносекунды. Еще на заре планирования был выбор C++ или Go. Выбор пал на Go из-за низкой планки вхождения в многопоточность.

Выбором доволен, да и со временем узнал что тема многопоточности в C++ – очень сложная, требующая очень большого опыта.

Я теперь понимаю, откуда на собесах по Go берутся вопросы вида "Какой ассемблерный код компилятор выдаст вот на это?"

Go для io-bound задач. Пишите лучше на Расте )

но прочитал с интересом )

На расте, кстати, мне кажется профилировать такие вещи будет сильно сложнее из-за того что язык сложнее (итераторы там, как минимум, и прочее). И не факт что будет быстрее. Безопаснее - да, но вот про скорость я не уверен.

Конечно, я ассемблерный выхлоп раста для итераторов не смотрел, но насколько я помню документацию, for name in names.into_iter() - один из рекомендуемых способов писать цикл. Так компилятор гарантированно знает, что не будет выхода за границы массива и, следовательно, не нужны дополнительные проверки.

У вас не будет накладных расходов на интерфейсы и шланг половину ваших итераторов развернёт в автовекторизации. Учитывая что уходит ещё и GC, то там профит появится практически на ровном месте ещё просто из-за уменьшения потребления памяти. Часть имеющихся аллокаций конечно можно дополнительно потимизировать (типа тех же small object) но это уже зависит от того где боттлнеки. А так - flamegraph есть, criterion/hyperfine есть, встроенные бенчмарки есть - не знаю в каком месте можно будет столкнуться с проблемами c профилированием.

Как альтернативу ржавчины можно конечно попробовать Zig, но там инфраструктура ещё не выросла, но выглядит код очень похоже на Go, но не имеет его минусов.

Пытался я тут как-то через flamegraph-rs профилировать достаточно большой сервис на базе axum - такое себе удовольствие. Стеки вызовов высотой с небоскрёб с кучей перемежающихся poll на футуры и прочих потрохов tokio.

Без async, наверное, всё будет сильно проще - но куда без него нынче :)

Вот тут не могу сказать. Говорят асинк на расте это максимум боли. Но примеры автора не затрагивали многопоточность/асинхронность в принципе, поэтому никого из них и не упомянул. Ну и оптимизировать эту часть обычно сложнее - внедрение lock-free, оптимизация планировщика, оптимизации пиннига памяти и вот это вот всё - вероятно есть окно, но принципиальных оптимизаций в эту часть добавить не выйдет на мой взгляд.

Потрогал некоторые примеры ручками на Go 1.22. Фокус с BCE кажется больше не работает:

BenchmarkBCE-24      	1000000000	         0.3343 ns/op
BenchmarkNoBCE-24    	1000000000	         0.3342 ns/op

Разница в `_ = b[7]`. Но асм внезапно разный: https://godbolt.org/z/a7hGTn6a7

Подскажи, дорогой хабр, где я накривил.

Вот кстати да:

BenchmarkSmallAndPlainObjects/SmallObject-8  1000000000  0.4390 ns/op  0 B/op  0 allocs/op
BenchmarkSmallAndPlainObjects/PlainObject-8  1000000000  0.4400 ns/op  0 B/op  0 allocs/op

go1.22.1, Ubuntu 22.04, i5-9300HF

Может автор на ардуино каком-нибудь бенчмарки запускает? ))

Мне кажется как-то так ни на один пример в статье ссылок на годболт так и не получил.

Судя по листингу ассемблера один и тот же код используется для обеих функций, только для nobce сперва еще выполняется `_ = b[7]`. По сути nobce вызывает внутри bce-код. Поэтому по замерам одно и то же. Не уверен как это решить на уровне компилятора.

Но асм внезапно разный

Попробуйте через GOSSAFUNC прогнать, может там позже оптимизация происходит

Очень интересная статья, спасибо

Я не встречал высокопроизводительного кода, который был бы лёгок в чтении, тестировании и модификации

Что означает высоко производительный? Этот термин примерно такой же как и highload - многие употребляют но без особого понимания. В современном мире намного важнее что бы система была горизонтально масштабируемая, а это достигается задолго до написания кода. А для бизнес заказчиков (ведь вы не пилите сервис просто что бы пилить сервис) важна скорость разработки фич. Большинство из этих трюков не нужны и даже вредны (если они конечно не находятся в каких то кор библиотеках). Мне кажется кажется что типовой паттерн использования интерфейсов - абстракция от внешних зависимостей (БД, ФС и тп). И то что вы выиграете 1нс от отказа от использовая интерфейсов глобальную картину не поменяет, но сильно усложнит жизнь в будущем.

Можно сказать и по-другому.

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

Подобная охота лучше всего реализуется в С/C++ ну или Расте.

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

Да легко: финансовые/математические/физические симуляции, аггрегации портфелей индивидуальных рисков и тому подобное.

Уменьшение времени выполнения с суток до часов, а то и минут - лично наблюдал.

Или ускорение превью рана с минут до секунд(ы).

Что, если не это пример кардинального улучшения юзер экспириенса и экономии.

Но какие там оптимизации были? Тут нет речи об ускорении с минут до секунд.

Это понятно, что у ВК при их масштабах экономия даже 0.1% от времени запроса может сократить расходы на десятки или сотни тысяч у.е. Для случая замены интерфейса, пусть даже при запросе 30мс это будет 30мс * 0.1% / 30нс = 1000 раз за 1 запрос, и это при расходах на cpu для этих запросов 10млн у.е. в год. Есть сомнения, что там может быть такой код.

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

Да вот именно, что как раз подобные низкоуровневые хаки и были, я как увидел шапку статьи - так сразу зашел почитать, хотя от Го далёк.

Конретно у нас "хаки" были вроде девиртуализации любыми способами (отказ от интерфейсов, виртуальных методов, делегатов, десятки генерик-аргументов - правда генерики в том числе для обхода возможного боксинга), структуры всюду, где только можно (и имеет смысл) в обмен на удобство наследования и переиспользование кода, поиск оптимальных размеров структур и хаки с паддингами/выравниванием, небезопасные балк-операции прямо над памятью вместо обращению к полям/пропертям, ручной инлайнинг повсюду (привет дублированию кода), небезопасное приведение типов (практически всюду), обход проверок границ, арифметика с указателями, ин-проц и ин-мемори всего, что только возможно, практически весь горячий путь не следовал вообще никаким "хорошим паттернам и практикам", а точнее - весь был написан вопреки.
Это только первое, что пришло на ум. Хватит? ;)

Алгоритмы и архитектура (уровня классов/модулей/внутреннего дата-флоу, в первую очередь) тоже, конечно.

Доля "хаков" в приросте производительности была действительно львиная.

...про кэш-лайны и локальность данных ещё забыл ;)

>>десятки генерик-аргументов - правда генерики в том числе для обхода возможного боксинга

Можете пожалуйста вот эту тему расшифровать немного? О каком языке программирования речь? Java?

С#.

Если вкратце, то здесь два аспекта: девиртуальзация и обход боксинг.

С девиртуализацией всё просто: void Method(IInterface strategy) и void Method<T>(T strategy) where T : IInterface в рантайме будут скомпилированы в машкод по-разному. Вариант с дженериком может быть полностью девиртуализирован JITом (зависит от конкретной имплементации T, конечно, но исходим из того, что T написан правильно с точки зрения перфоманса). JIT в .нете становится умнее и старается девиртуализировать и интерфейсы/абстрактные классы/виртуальниые методы и даже делегаты на горячем пути, но это всё эвристики и без гарантий.

Боксинг: выше я писал "структуры всюду, где только можно (и имеет смысл)". "Всюду" означает как для данных, так и для логики, отличный пример - енумераторы. Стандартный пример - енумератор класса List<>, реализован как struct. Для того, чтобы он не был boxed, вызывающий код/JIT должны знать, что это List<T>.Enumerator, а не IEnumerator<T>, возвращаемый из IEnumerable<T>.GetEnumerator().

Таким же образом реализуем "стратегии" - struct имплементации интерфейсов IStrategy. new Struct()/default(Struct) создаётся на стеке, не нагружает GC. Главное, избежать боксинга, а значит передавать не как IStrategy, а как T where T : IStrategy.

Теперь представляем себе, что в некоторые методы или в качестве генерик-аргументов типа мы должны передать несколько таких разных "T", каждый из которых имеет вложенные генерик-аргументы.

Так получаются "десятки генерик-аргументов" о которых я писал. "Десятки", наверное, перебор, но по 10+ у нас бывало.

...а где девиртуализация - там и возможный автоматический инлайнинг на уровне JITа.

А где структуры - там и локальность данных.

Нахлынули воспоминания ;)

>>это при расходах на cpu для этих запросов 

Сокращение времени исполнения запроса на 0.1% на расходы на CPU скорее всего никак не повлияет. По крайней мере у компаний типа ВК, которые свои серверные мощности используют, причем с определенным запасом. Который явно не доли процентов.

Почему? Это значит, что они серверов могут купить меньше, и счёт за электричество меньше будет.

Потому что если экономия 0,1% (1/1000) времени CPU, я (теоретически) смогу купить 999 серверов вместо 1000. И это если у меня реально есть 1000 серверов под эту задачу.

А практически я ничего не сэкономлю, потому что при плановой нагрузке, требующей 1000 (или ладно, 999 :) ) серверов, мне еще хотя бы 10 ( а лучше 100) надо держать в резерве. На случай внеплановых пиковых нагрузок, выхода каких-то стоек из строя, аварийного обесточивания отдельных ЦОД и т.д.

Экономия будет если я требуемые ресурсы CPU сокращаю в разы.

Так и считайте резерв тоже от 999 vs 1000. Про электроэнергию пункт вы проигнорировали.

Экономия тоже будет 0.1%. Но когда у вас расходы на вычисления 10тыс/год, вам 10 погоды не делают. А когда 100млн, то это уже интереснее.

Большое количество серверов как раз и даёт возможность экономить на изменениях даже в долю процента, потому что относительный прирост производительности от одного сервера становится маленьким.

Ускорение с минут до секунд - это хорошо, безусловно.

Речь, мне кажется, о том, что приводимые в статье примеры такое ускорение дать не могут в принципе

Ускорение здесь линейно от количества вызовов горячего пути для получения результата. И если вызовов миллионы или миллиарды - запросто.

Очень интересная статья, спасибо

"Поэтому по возможности не используйте Go. Забудьте, что есть Go, если вы хотите выжать максимум."
Такая формулировка на фоне статьи выглядит более правильной :)

Sign up to leave a comment.