На мой взгляд, служебные вызовы функции C должны быть такими же низкими, как регистры установки rcx, rdx, rsi, rdi, выполнить быстрый вызов и получить значение rax. Но я слышал о больших накладных расходах в cgo <…>
Ваше мнение необоснованно.
Причина, по которой звонят с Go на C имеет заметные издержки из-за следующих причин:
Давайте сначала рассмотрим C
Хотя язык не требуется языком, типичная C программа, скомпилированная типичным Компилятор, работающий на обычной ОС в качестве обычного процесса, в значительной степени зависит от ОС для выполнения определенных аспектов среды выполнения.
Предположительно, наиболее заметным и важным аспектом является стек: ядро отвечает за его настройку после загрузка и инициализация образа программы и перед передачей выполнения в точку входа кода новорожденного процесса.
Еще один важный момент - опять же, хотя это и не является строго обязательным, большинство программ C полагаются на ОС. Собственные потоки для реализации нескольких одновременно выполняемых потоков через код программы.
Выполнены вызовы функций в коде C обычно компилируются с использованием той же ABI целевой комбинации ОС и аппаратной реализации (если, конечно, программисту явно не удалось сказать компилятору делать иначе - как, скажем, отмечая специфицированную c функцию как имеющую другое соглашение о вызовах).
C не имеет автоматических c средств управления не-стековой памятью («кучей»).
Такое управление обычно выполняется через стандартные библиотечные функции C семейства malloc(3)
. Эти функции управляют кучей и считают любую память, выделенную через них, «своей» (что вполне логично).
C не обеспечивает автоматическую c сборку мусора.
Подведем итог: типичный программа, скомпилированная из C: использует потоки, предоставляемые ОС, и использует стеки, поставляемые ОС, в этих потоках; вызовы функций большую часть времени следуют ABI платформы; память кучи управляется специальным библиотечным кодом; нет G C.
Давайте теперь рассмотрим Go
- Любой бит Go кода (как вашей программы, так и среды выполнения) выполняется в так называемом goroutines, которые похожи на сверхлегкие потоки.
- Планировщик goroutine, предоставляемый средой выполнения Go (которая компилируется / связывается с любой программой, написанной в Go), реализует так называемое планирование M × N из goroutines - где M goroutines мультиплексируются в N потоков, поставляемых OS, где M обычно намного выше N.
- Вызовы функций в Go не соответствуют ABI целевой платформы.
В частности, AFAIK современные версии Go передают все аргументы вызова в стеке. - Программа всегда запускает в потоке, предоставленном ОС.
Программа, ожидающая некоторого ресурса управляемый средой выполнения Go (такой как операция на канале, таймер, сетевой сокет и т. д. c) не занимает поток ОС.
Когда планировщик выбирает для выполнения goroutine он должен назначить его свободному потоку ОС, который находится во владении Go среды выполнения; в то время как планировщик изо всех сил старается поместить программу в тот же поток, в котором он выполнялся до приостановки, это не всегда удается, и поэтому программы могут свободно мигрировать между различными потоками ОС.
Естественно, что указано выше приводят к тому, что у подпрограмм есть свои собственные стеки, которые полностью независимы от тех, которые предоставляются ОС для ее потоков.
В отличие от C, эти стеки можно наращивать и перераспределять.
Управление кучей памяти осуществляется Go время выполнения, автоматически и выполняется напрямую, для этого не используется C stdlib.
Go имеет G C, и этот G C равен одновременным в том смысле, что он работает полностью параллельно с программами, выполняющими код программы.
Напомним: у подпрограмм есть собственные стеки, используется соглашение о вызовах, несовместимое ни с ABI платформы, ни с C, и они могут выполняться в разных потоках ОС в разных точках их выполнения.
Go runtime управляет памятью кучи напрямую и имеет полностью параллельный G C.
Теперь давайте рассмотрим вызовы от Go до C
Как вы, вероятно, должны сейчас видеть, «Миры» сред времени выполнения, в которых выполняется код Go и C, достаточно различны, чтобы иметь большое «несоответствие импеданса», которое требует определенного шлюза при выполнении FFI - с ненулевой стоимостью.
В частности, когда код Go собирается вызвать в C, необходимо сделать следующее:
- Программа должна быть заблокирована для потока ОС, в котором она работает в данный момент. on ("закреплено").
- Поскольку целевой вызов C должен выполняться в соответствии с ABI платформы, текущий контекст выполнения должен быть сохранен - как минимум Регистры, которые будут сброшены вызовом.
- Механизм
cgo
должен проверить, что любая память, которая должна быть передана в целевой вызов C, не содержит указателей на другие блоки памяти, управляемые Go , рекурсивно - это позволяет G C Go продолжать одновременную работу. - Выполнение должно быть переключено со стека goroutine на стек потока: новый кадр стека должен быть создан на последний и параметры к целевому вызову C должны быть размещены там (и в регистрах) в соответствии с ABI платформы.
- вызов сделан.
- По возвращении выполнение должно быть переключено обратно в стек программы, опять же путем направления всех возвращаемых результатов обратно в кадр стека выполняющей программы.
Как вы, вероятно, можете видеть, неизбежные затраты и размещение значений в некоторые регистры процессора являются самыми незначительными из этих затрат.
Что можно сделать с этим
Как правило, Существует два вектора для решения проблемы:
Делать вызовы C нечастыми.
То есть, если каждый вызов C выполняет длительный процессор В интенсивных вычислениях можно предположить, что накладные расходы на выполнение этих вызовов уменьшаются за счет ускорения вычислений, выполняемых этими вызовами.
Запись критических функций в сборке.
Go позволяет писать код непосредственно в сборке целевой платформы H / W.
Один "трюк", который может позволить вам получить лучшее из обоих миров, - использование способность большинства промышленных компиляторов выводить форму ассемблера на языке ассемблера. Таким образом, вы можете использовать аппаратные средства, предоставляемые компилятором C, такие как автоматическая векторизация (для SSE) и агрессивная оптимизация, а затем захватить все, что оно сгенерировало, и обернуть его в тонкий слой сборки, который в основном адаптирует сгенерированный код к нативный ABI Go.
Существует множество сторонних Go пакетов, которые делают это (скажем, this и that ) и, очевидно, Go среда выполнения также делает это.