Комментарии 68
Ничто написанное хорошо невозможно ускорить в 16.5 раз
Это заблуждение. Причина не в плохо написанном, а в trade-off'ах самого компилятора и языка. В статье это очень хорошо очерчено. Посмотрите внимательнее как работает планировщик Go и как работают горутины, и зачем вообще происходят такие манипуляции со стеком. Если коротко, то горутины могут мигрировать между потоками, а также имеют динамический стек, который наращивается двумя разными способами, ни один из которых не является простым и однозначно совместимым с внешними библиотеками.
Исходя из такой реализации и появляется вся сложность и просадки производительности.
я знаю как устроен Go, ещё я знаю, что от сегментированного стека они отказались в 2014 году
https://go.dev/doc/go1.3#stacks
Go 1.3 Release Notes
Go 1.3 has changed the implementation of goroutine stacks away from the old, “segmented” model to a contiguous model.
Так что теперь это просто легаси
Насчёт миграции корутин - это решается через запрет... миграции. А появляется миграция в тот момент, когда кто то решает написать тредпул с так называемым "воровством" на одной очереди, что уже спорное решение
Совершенно верно, work-stealing планировка задач - это одна из самых сбалансированных стратегий для общего случая, а вот сегментированный стек (или split-stack) там так и используется, для функций, которые вызывают C код под капотом, потому что новый способ (contiguous model) с такими вызовами не совместим, поскольку требует переписывать адреса сохраненных на стеке указателей при его расширении. А в C коде неизвестно где указатель, а где - нет, т.к. такой код должным образом не инструментирован компилятором Go (в котором реализация сборщика мусора позволяет однозначно понять какие и где хранятся указатели).
Такая формулировка задачи как раз и создает все сложности и проблемы со скоростью.
требует переписывать адреса сохраненных на стеке указателей при его расширении.
Наличие этого в Go достаточно чтобы понять что это за язык и как его дизайнили, если честно
Несмотря на то что дизайн языка и библиотек оставляет желать лучшего, если говорить именно о многозадачности и планировке, то в Go реализация практически state-of-the-art, наравне с виртуальной машиной BEAM (Erlang), хоть и подобный вариант continuations является в каком-то смысле тупиковой ветвью развития в этой сфере.
Много где от такой реализации уже отказались, а в C++ например сейчас страсти крутятся вокруг stackless coroutines и senders/receivers, однако случай Go - это как раз уникальный пример того, насколько технологически круто это реализовано.
Добавлю к высказыванию предыдущего оратора.
Люди, которые дизайнили Go, наверное, наполовину определили развитие ИТ-индистрии в том виде, как мы её имеем.
UNIX, C - их рук дело. Контейнеризация, столь популярная сегодня, во многом основана на идеях из Plan9, которая тоже их рук дело. Сама по себе идея писать системное ПО, включая ядро ОС, на языке высокого уровня, а не на ассемблере - их рук дело.
Go тоже возник не на пустом месте. Он - прямой наследник языка Alef, диалекта C, созданного специально для написания Plan9.
В общем, мне кажется, Ваше высказывание несколько слишком категорично...
Интересный момент. Разработчики Go говорят, что код на Go по скорости сравним (если не быстрее) кода на C. Так, зачем использовать C код, если можно написать всё на одном языке? Или в Go отсутствуют важные возможности?
Зачем и почему мы переписали ядро хранения данных на C++ подробно описано здесь: https://habr.com/ru/companies/flant/articles/878282/
Было бы любопытно глянуть на изолированный пример: как выглядел код на Go и как его пришлось переписать на C++. Такой контраст наверняка интересен и был бы очень показателен.
Аргументация в статье местами кажется несколько "размытой":
«…возникают тысяча и один вопрос, например как garbage collector Go будет всё это обрабатывать. И кажется, именно поэтому в Prometheus такой подход пока не используют, хотя попытки были.
Мы решили эти проблемы, изменив формат работы с WAL.»
Проблема, по сути, обозначается как “много вопросов” и “кажется, что так не делают”, но без конкретики — непонятно, какие именно сложности возникли и пробовали ли их решать средствами Go. В C++ действительно проще с ручным управлением памятью, но остаётся ощущение, что вопрос до конца не раскрыт.
Ну да, проблему "много вопросов" и "кажется" C++ действительно решает. Но осталось неясным:
Как именно Go будет всё это обрабатывать (может, и хорошо справится)?
Можно ли оптимизировать эту задачу средствами Go (возможно, и можно)?
Впрочем, понятно одно — как сделать это в C++ обычно очевиднее :)
В Go такое реализовать бы не получилось по многим причинам. Например, отсутствие SIMD операций. Бинарь на go должен работать на любом процессоре, поэтому он использует только базовые инструкции. Мы же скомпилировали 3 варианта кода и вложили их в бинарь, и при старте выбираем куда разрешить виртуальные функции. Выравнивание. В итоге нам бы пришлось писать структуры поверх типов а ля [13]byte и каждый раз при обращении к полю кастить его через unsafe. Go не создан для высокой производительности кода, он хорошо решает задачи оркестрации, но числодробилки лучше писать на более подходящих языках
Дык я согласен про числодробилки, но какое отношение это имеет к проблеме:
"например как garbage collector Go будет всё это обрабатывать"
"И кажется, именно поэтому в Prometheus такой подход пока не используют, хотя попытки были"
?
Попытки уменьшить количество строк в памяти были в оригинальном проме, смотри реализации labels.Labels через dedup или текущая реализация string_labels. Это частично снижает нагрузку на GC, так как label set теперь упакован в одну строчку, то есть один указатель, вместо 2n+1 (где n это количество лейблов). Но для того, чтобы добиться максимальной компактности мы используем компрессию. Не в общем смысле, не lzma или lz4, а специфичную к данным. Например, используется колоночное хранение с сортировкой, что позволяет использовать DeltaOfDelta кодирование. Используем последовательные идентификаторы в нескольких массивах вместо одного, чтобы идентификаторы были меньше и компактнее записывались в StreamVByte кодировке. Используем union, чтобы хранить до 7 значений in_place, а если больше, то интерпретировать эту память как энкодер + указатель на память с закодированными данными. Всё это можно реализовать на Go, но из-за отсутствия контроля над итоговым машинным кодом, из-за отсутствия SIMD операций, из-за недостаточно гибкой системы типов с теми же union и packed структурами — работа с такими данными на Go будет в десятки раз медленнее. А какой толк выигрывать в памяти, если после этого мониторинг будет жрать ЦПУ.
Спасибо за детальное объяснение! Насколько я понял, оптимизация здесь делится на две части:
Оптимизация хранения лейблов (labelsets)
Оптимизация хранения точек (временных рядов)
Колоночное хранение, DeltaOfDelta, union'ы для in-place значений
Интересно было бы понять границы возможного: какого уровня экономии памяти можно было достичь, используя "обычный" Go.
Вот, например, энкодер/декодер для ID
https://go.dev/play/p/dGYuyrVCoCf
=== Кодирование/Декодирование массива ID ===
1. Простой пример:
Исходные ID: [0 1 127 128 16383 16384 2097151 2097152]
Декодированные: [0 1 127 128 16383 16384 2097151 2097152]
Размер: 32 байт -> 19 байт (40.6% экономии)
2. Реалистичное распределение (как в Prometheus):
✓ Все значения декодированы корректно
Статистика для 10000 ID:
Исходный размер: 40000 байт
Закодированный: 11032 байт
Сжатие: 72.4%
Средний размер: 1.10 байт/ID
Распределение размеров:
- 1 байт: 9268 значений (92.7%)
- 2 байт: 434 значений (4.3%)
- 3 байт: 298 значений (3.0%)
3. Граничные случаи:
Значение 0 требует 1 байт
Значение 127 требует 1 байт
Значение 128 требует 2 байт
Значение 16383 требует 2 байт
Значение 16384 требует 3 байт
Значение 2097151 требует 3 байт
Значение 2097152 требует 5 байт
Значение 4294967295 требует 5 байт
По первой части пока не вижу очевидной необходимости перехода — или я что-то упускаю?
Вот, например, энкодер/декодер для ID
Казалось бы, в этом примере можно было бы при кодировании пройтись по слайсу ID-ёв два раза: первый раз, чтобы подсчитать потребный размер буфера, потом сразу выделить буфер нужного размера и вторым проходом закодировать. И можно было бы обойтись без bytes.Buffer
и реаллокаций...
Например, отсутствие SIMD операций.
Выглядит, если честно, не очень убедительно. Вы же осилили гошный псевдоассемблер, Можно было бы на нём написать SIMD-овские фрагменты, задекларировать, как гошную функцию и звать по указателю, который инициализируется при старте. Вызов псевдоассемблера из Go - вещь недорогая, сами знаете.
структуры поверх типов а ля [13]byte и каждый раз при обращении к полю кастить его через unsafe
Или написать type MyType [13]byte
и методы к нему, которые предоставляют доступ к полям логической структуры, а сами работают напрямую с этими 13-ю байтами...
"Разработчики Go говорят, что..." - так обобщать нельзя.
Не сошлюсь на оригинал но Go разработчики говорят так - "cgo не для скорости, cgo для совместимости".
Эх, вот бы совместимости, IRL нельзя писать нормальные DLL на Go
А там уже починили невозможность держать более одного экземпляра гошного рантайма в одном процессе.
Раньше это выглядело так: пишешь на Go DLL-ку. Грузишь её в сишную програмку. Работает. Грузишь в гошную програмку - причудливо глючит. Причем на разных платформах (я пробовал x86 линух и венду) по-разному. Гошные рантаймы main-а и DLL-ки не поделили процес...
Когда появился UNIX, его ядро (да и вся система) была написана на C. В те времена операционные системы было принято писать на асме. Пайк ходил, всем рассказывал на голубом глазу, что C почти не уступает ассемблеру по производительности, но значительно выигрывает по удобству разработки, надёжности/анализируемости получающегося кода и проч. В те времена C проигрывал асму по скорости раза в два.
Однако практика показала их стратегическую правоту.
Сейчас с Go повторяется та же история. И тот же Пайк ходит, всем рассказывает (ну, вернее, рассказывал. Он, вроде, на пенсию вышел), что Go мало чем уступает C по производительности. Хотя когда компилятор Go переписали с C на Go, он вроде замедлился как раз в те же волшебные два раза.
Посмотрим, что покажет практика, но на это уйдет какое-то время.
С другой стороны, Go не такой уж и медленный. Мне тут понадобилось картинки масштабировать, причем желательно по строкам, а не заливать сразу всю картинку в память. Поэтому готовые решения не подошли, и пришлось написать своё. В общем, довольно наивно написанная реализация на Go показывает скорость, сравнимую с ImageMagic (хотя сишные реализации бывают и побыстрее. Они, правда, норовят все ядра процессора занять, а я не уверен, что в моём случае это достоинство).
Go это идея какого то человека на неделе идей в Гугле или как-то так она называется. Потом идею умножили на корпоративную культуру и задачу от компании: сделать язык для быстрого обучения программистов работе с ним.
Это не продукт работы институтов и учёных.
Go - тупик. Библиотека на с++ сделала бы лучше и больше.
И go никогда не будет сравним в скорости с с++, в языке нет семантики которую можно было бы оптимизировать, переписывание компилятора на go оценивать даже не буду, у меня мало цензурных слов. Скажу только, что go к компиляторам уж точно не приспособлен. Но компилятор на go это ещё одна стена между кодом на go и оптимизациями
Итого:
Одна нормальная идея использовать стекфул корутины и человек который постарался над движком
Дизайн языка основан на идее сделать язык чтобы быстро обучать программистов и ... Быстро сделать сам язык, потому что мвп и корпорация с менеджерами.
С такими вводными можно получить только go
С такими вводными можно получить только go
или JavaScript )
Go придумал Роберт Пайк из AT&T и убедил Гугль профинансировать проект. Вероятно в обмен на то, что название языка будет напоминать слово "Google"
Я бы не стал утверждать, что язык Go плохой. Мне очень нравится сочетание достаточной выразительности и однозначности в его конструкциях, особенно в работе с конкурентностью. Кроме того он достаточно эффективен для широкого круга задач. Проблема скорее в том, что его не редко позиционируют как язык для написания БД, или как высокоэффективный язык. Молоток хороший инструмент, но молотком сложно собирать часы.
Go это идея какого то человека
Это вы "какой-то человек", а автор го - Кен Томпсон, один из создателей UNIX, языка С, UTF-8 и регулярных выражений. Ну и конечно Роберт Пайк, тоже такая же легенда.
сделать язык для быстрого обучения программистов работе с ним.
нет, язык делали не для этого, вы хотя бы с вопрос немного ознакомились бы, а не фантазировали.
Это не продукт работы институтов и учёных.
А какой язык программирования это продукт работы ученых? О чем вообще вы говорите?
Go - тупик. Библиотека на с++ сделала бы лучше и больше.
Нет
И go никогда не будет сравним в скорости с с++
Ему и не нужно. А С++ никогда не сравнится с го по скорости разработки. Языки програмирования выбираются не исключительно по скорости работы программы в наносекундах.
переписывание компилятора на go оценивать даже не буду, у меня мало цензурных слов
Если вы про комплятор тайпскрипта, то если бы вы ознакомились с вопросом и посмотрели сами коммиты, то уведели что речь шла о портировании компилятора, включая полу-автоматическую трансляцию. И перенос в синтаксис го с тайпскрипта гораздо более простая задача, чем перенос в синтаксис С++, особенно с учетом отсуствия GC в плюсах.
Итого:
Итого: в заданой теме вы не разбираетесь, ничего не читали, ничего не понимаете в том, что о чем говорите, но мнение имеете.
А какой язык программирования это продукт работы ученых?
До эры когда каждая компания решила себе сделать скриптовый язык для браузера такими были большинство языков
Fortran, Lisp, C, C++, Haskel и так далее. Плюс в те времена можно было ломать обратную совместимость хоть каждый день, чем очень сильно и пользовались, годами развивая язык, а не копя в нём плохие решения
А С++ никогда не сравнить с го по скорости разработки.
вот это ключевое заблуждение. При наличии библиотек/фреймворков разрабатывать на С++ быстрее или также по скорости, как на Go
Разница в том, что на Go невозможно написать себе эти инструменты, а на С++ - можно. Поэтому Go это язык одной задачи, а для компании способ найма дешёвых разработчиков
Fortran, Lisp, C, C++, Haskel и так далее.
Языки С и С++ созданы не учеными и не институтами, а инженерами Bell Labs. Хватит нести чушь.
До эры когда каждая компания решила себе сделать скриптовый язык
Причем тут го и скриптовые языки?
вот это ключевое заблуждение. При наличии библиотек/фреймворков разрабатывать на С++ быстрее или также по скорости, как на Go
Нет, это у вас отсутствие опыта и знаний, из-за которых вы не понимаете какие задачи решает наличие GC.
Разница в том, что на Go невозможно написать себе эти инструменты, а на С++ - можно.
Язык выбирается для задачи, а не задача для языка. Вы буквально тут сейчас рассказываете что фуры не нужны, потому что они не могут копать землю в отличии от экскаватора.
Смотрю, вы повторяетесь в заблуждении. К С++ GC прикручивается элементарно. Просто переопределяются глобально new/delete. Мало кому нужно
К С++ GC прикручивается элементарно.
Вот у меня написано внутри функции void* x = new A(); x = new A();
Как GC, сделаный просто через переопределение new/delete и запустившийся, скажем, через 10мс, поймет что первый объект А удалять можно, а второй пока нет? Допустим он элементарно обойдет стэк каждого потока(еще надо тогда элементарно stop the world написать) - а если поле структуры будет void* _data
- тогда что делать? Нужно элементарно прикрутить рефлекцию в рантайм?
Принципы работы GC от языка не зависят.
Они зависят от рантайма и системы типов. Не просто так в го или джаве нет void*, а есть только обобщеный fat pointer . С++ с специализорованым рантаймом, урезанной системой типов и GC это будет уже будет не С++, а какой-то уродец, типа Management C++ .Net, который 20 лет назад сделал майкрософт и который благополучно умер за ненадобностью.
Какие то странные фантазии.
Могу посоветовать найти библиотеку boehm c++ и почитать документацию.
Ничего там не урезается.
Вы сами то почитайте. Boehm использует консервативное сканированияе в случае void*, буквально проверяя каждое машиное слово как потенциальный указатель, что во-первых медленно, особенно если там большой объект типа массив, например в миллион интов, а во вторых даёт ошибки ложной достижимости. Очень элементарный GC, да.
Не элементарный GC, а элементарно прикручивается, перевирать не надо.
Фантазии про урезание возможностей проехали.
Теперь дерейл на производительность?
Этот GC я по скорости не смотрел, но D с похожим GC уделывает Go.
В общем прыганье по темам мне надоело, понимания у Вас маловато для адекватного обсуждения.
Фантазии про урезание возможностей проехали.
Если вы не понимаете, что для GC в С++ придется выбирать между созданием подможеством языка или костыльными решения с ликами памяти типа консервативного сканирования, то это лишь результат вашей необзованности и глупости.
D с похожим GC уделывает Go.
Жалко что сам создаль D, Walter Bright, про это не в курсе:

В общем прыганье по темам мне надоело, понимания у Вас маловато для адекватного обсуждения.
Поражаюсь как вам не стыдно нести чушь, врать, позориться и что-то еще говорить про адекватное обсуждение. Идите книжки читайте по теме, может поможет.
Если читать внимательно, то можно понять, что Брайт говорит, что в Го быстрее механизм GC. О скорости языка речь не идёт.
Впрочем, за эти годы Го прилично ускорился, я бы сказал сейчас о паритете с Ди.
А GC в Ди сделан таким намеренно(опуская подробности), чтобы не возникало причин написания подобных статей - ради интероперабельности.
Так вы бы сейчас сказали "о паритет с Ди"
(BTW го побеждает в 70% бенчмарков по ссылке) или "D с похожим GC уделывает Go"
? Или вместо попытки увести разговор в сторону вы просто признаете что были не правы?
Сравнивать бенчмарки ещё сложнее чем читать цитату того же Брайта.
Моё выражение, что к С++ элементарно прикручивается GC остаётся верным, а Ваше, что это потребует урезание возможностей языка С++ - неверно.
Также есть факт на примере D, насколько ориентировочно сказывается на производительности такой GC. Там же есть сравнение и с С++ без GC.
С++ элементарно прикручивается GC остаётся верным
Да не является оно верным. Если бы вы на практике попробовали использовать тот же BDW GC, то быстро бы обнаружили что для нормальной работы приложения у вас весь код покрывается GC_malloc_atomic, GC_word, GC_malloc_explicitly_typed и прочими вещами. А если "просто переопределяются глобально new/delete" то просто получаем лики памяти из-зи ложной достижимости.
Пока листал issue bdwgc на предмет утечек, обнаружилось что он же используется в tinygo...
Потому что надо читать не issue, а документацию для начала. Это не issue и не баг, это фундаментальное свойство алгоритма
These false pointers can be either permanent. In the former case, the program will still run correctly, but the
block will never be reclaimed.
Unexpected heap growth can be due to one of the following:
Pointer misidentification. The garbage collector is interpreting integers or other data as pointers and
retaining the "referenced" objects
.
https://github.com/ivmai/bdwgc/blob/master/docs/debugging.md#unexpectedly-large-heap
Пока листал issue bdwgc на предмет утечек, обнаружилось что он же используется в tinygo...
Это манипуляция - не используется, а поддерживается возможность по своему желанию скомплировать tinygo, заменив стандартный GC на bdwgc. И добавили эту фичу только в марте этого года, она официально даже не задокументирована. Впрочем коллектор с консервативным сканированием как опция был доступен и раньше в чистой го реализации.
C - это продукт работы ровно тех же людей, что и Go
Фреймворк - это такая штука, которая не просто полезная библиотека, а всю жизнь под себя подчиняет. В Go нет фреймворков потому, что библиотеки удобно интегрируются вокруг функций и интерфейсов, предоставляемых стандартной библиотекой. Можно сказать, что Go stdlib - это и есть такой фреймворк.
А какой язык программирования это продукт работы ученых?
PL/1, наверное...
Очень ценю вас как c++ специалиста, но тейки слижком уж радикальные:
"Go это идея какого то человека на неделе идей в Гугле..." - имена авторов го всем известны, они на решении CS проблем стаю бродячих собак съели и видимо со своими идеями попали в цель, судя по тому, какой адопшен приобрёл Go.
"...переписывание компилятора на go оценивать даже не буду, у меня мало цензурных слов..." - солидные мeжчины из Microsoft вполне оценили и переписали компилятор TypeScript на го (подорвав немало хопстерских пуканов).
Хоть Go и оценивают как "язык-framework" (что в этом плохого?) из за минималистичного набора фич и довольно прямолиненйного дизайна, всётаки это вполне себе general purpose язык, стройно сделаный и, что очень и очень важно, с превосходным тулингом, который идёт с ним одним бандлом.
"Библиотека на с++ сделала бы лучше и больше" - так то оно так, только поправде нафиг никому ненужна эта библиотека т.к. c++ в наше время (увы) довольно маргинален. C++ коммьюнити слишком любит велосипеды, userver тот же даже не адоптит стандартные корутины.
Отличная работа по оптимизации! Было бы здорово увидеть decision matrix в начале
статьи - когда FastCGo оправдан, а когда лучше остаться с CGo. Это помогло бы
инженерам быстрее понять применимость решения в их контексте.
Классная статья, круто раскрывает, почему CGo тормозит, и как вы выжали из FastCGo в 16,5 раз больше скорости. Бенчмарки, разбор ассемблера и тонкости со стеком и ABI - всё чётко и по делу, для разработчиков на Go и C++ прям находка. Проблемы с выравниванием, упаковкой структур и памятью расписаны с примерами, что реально помогает въехать. ИМХО.
В статье этого нет в явном виде или я не увидел, но основное падение производительности на CGO это премиса с entersyscall вызовом, потому что она говорит шедулеру создать новый поток ОС (M) в пуле, а текущий из пулла убирает, так как неизвестно будет ли с-функция блокирующей или нет. Плюс в решении из статьи не получится использовать колл-бэки, т.к. вызов вернется в стэк другой горутины.
1) Создание потока ОС занимает десятки микросекунд (!!!) и в исходном коде функции runtime.entersyscall я на нашел кода по созданию потока ОС (поэтому вы этого в статье и не увидели)
2) Да, вы совершенно правы, что наше решение пока не позволяет вызывать Go callback'и и я об этом в статье сказал
1) Создание потока ОС занимает десятки микросекунд (!!!) и в исходном коде функции runtime.entersyscall я на нашел кода по созданию потока ОС (поэтому вы этого в статье и не увидели)
Потому что это асинхроная операция, entersyscall не создает поток, а говорить шедулеру, что это надо сделать. Вот об этом комментарий в самой премисе https://github.com/golang/go/blob/b3251514531123d7fd007682389bce7428d159a0/src/runtime/cgocall.go#L153
А вот как это происходит в исходном коде entersyscall https://github.com/golang/go/blob/b3251514531123d7fd007682389bce7428d159a0/src/runtime/proc.go#L4621
gp.m.syscalltick = gp.m.p.ptr().syscalltick
pp := gp.m.p.ptr()
pp.m = 0
gp.m.oldp.set(pp)
gp.m.p = 0
atomic.Store(&pp.status, _Psyscall)
Запоминаем syscalltick
(счетчик системных вызовов) из P в M. - Получаем указатель на текущий P (pp
).
Отсоединяем P от M: pp.m = 0
(теперь P не привязан к M) и gp.m.p = 0
(M не привязан к P).
Сохраняем P в gp.m.oldp
(чтобы потом попытаться вернуть тот же P при выходе из системного вызова).
Меняем статус P на _Psyscall
(ожидание системного вызова). Теперь когда планировщик видит P в статусе _Psyscall
, он может создать новый M (поток ОС) в пуле для выполнения горутин.
Без этой логики сискол в какой-то библиотечной функции может привести к thread pool starvation на всем процессе.
Насколько я понимаю, CGo call не создаёт поток операционной системы, но говорит планировщиуку Go, что текущий поток не надо учитывать, если возникнет вопрос о создании нового, потому что он ушел в Си, и никто не знает, вернётся ли.
Но вот будет ли на самом деле создан новый поток - вопрос потребностей. В данном случае лишь временно поднимают квоту.
Как люди извращаются, чтобы решить проблемы, которые сами же и выбрали. Хотите я ускорю ваши C++ вызовы в 1000 раз? Просто. Используйте. C++.
И не надо мне писать про преимущества Go, современный C++ такой же безопасный и на нем так же быстро решаются продуктовые задачи как и на Go, при том что он часто быстрее и гораздо более универсальный и мощный.
Очередной вброс без агрументов.
Да, написать всё на C++ очевидно было бы эффективнее. Однако на такое ушло бы не 2 года, а 5 или 7. Prometheus состоит не только из tsdb, в нём также есть query engine, который достаточно развесистый и сложный с большим количеством тонкостей реализации, которые пришлось бы переносить. Ну, а про клиенты ко всевозможным системам Service Discovery и говорить не стоит. При этом есть сомнения, что это будет настолько эффективнее, насколько дольше и трудозатратнее. А через 3 года возможно, что и системы мониторинга станут совершенно другими.
Да, написать всё на C++ очевидно было бы эффективнее
Да и не факт. C++-ные программы имеют тенденцию активно создавать короткоживущие объекты в куче (строки, вектора, ...). Но куча в C++ ощутимо медленнее, чем в Go...
В статье много интересных технических деталей, но у меня всё-таки так и не сложилась ясная картина, куда CGo целые 70ns на вызов девает...
И жалко, что нет цифер для x86 - чтобы можно было сопоставить с собственным опытом на более привычной/распространённой платформе.
Дело в том, что при CGo вызове происходит перевод горутины в режим системного вызова и переключение стека со стека горутины на системный. В рамках этих 2 задач выполняется множество небольших операций (например изменение полей в структуре g, где происходят atomic-операции), которые как раз в сумме и дают эти 78ns
В конце статьи есть ссылка на репозиторий с бенчмарками. Вы можете запустить их на платформе x86 и посмотреть разницу.
Может проще было все переписать на с++? Есть удобные библиотеки для работы и быстрые. Тот же userver
Тогда бы нам пришлось переписать целиком Prometheus (https://github.com/prometheus/prometheus) на C++, что потребовало бы большого количества времени (исчисляемого годами) и сил. А так мы переписали только проблемные части Prometheus, сохранив полную совместимость с исходным продуктом.
Да и нет смысла переписывать то, что итак хорошо работает)
Переписать весь прометеус на С++? Это несколько лет минимум, кто за это платить будет?
А почему не стали использовать VictoriaMetrics?
У неё потребление памяти намного ниже и разные полезные фичи есть.
Prom++, в отличие от VictoriaMetriсs, полностью совместим с Prometheus и является его полной заменой (достаточно просто заменить исполняемый файл и вы получите снижение потребления ОЗУ в несколько раз).
А еще Prom++ потребляет ОЗУ в несколько раз меньше, чем VictoriaMetrics. Подробнее об этом вы можете прочитать в статье моих коллег https://habr.com/ru/companies/flant/articles/878282/
FastCGo: как мы ускорили вызов C-кода в Go в 16,5 раза