Вызов функции C из Golang - PullRequest
       21

Вызов функции C из Golang

0 голосов
/ 17 марта 2020

Я хочу писать логики контроллеров c и обрабатывать json и базы данных в Golang, в то время как моя модель математической обработки - в C. На мой взгляд, накладные расходы на вызов функции C должны быть такими же низкими, как при настройке регистров rcx, rdx, rsi, rdi, выполнить быстрый вызов и получить значение rax. Но я слышал о больших накладных расходах в cgo

Скажите, что у меня есть общая функция fastcall x64 c int64 f(int64 a,b,c,d){return a+b+c+d} Как я могу вызвать ее из go , чтобы получить наивысший потенциальный результат теста в go testing.B тесте?

PS нет передачи указателя, нет хитростей, просто интересует, как получить доступ к интерфейсу C наиболее надежным способом

Ответы [ 2 ]

3 голосов
/ 18 марта 2020

На мой взгляд, служебные вызовы функции 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, необходимо сделать следующее:

  1. Программа должна быть заблокирована для потока ОС, в котором она работает в данный момент. on ("закреплено").
  2. Поскольку целевой вызов C должен выполняться в соответствии с ABI платформы, текущий контекст выполнения должен быть сохранен - ​​как минимум Регистры, которые будут сброшены вызовом.
  3. Механизм cgo должен проверить, что любая память, которая должна быть передана в целевой вызов C, не содержит указателей на другие блоки памяти, управляемые Go , рекурсивно - это позволяет G C Go продолжать одновременную работу.
  4. Выполнение должно быть переключено со стека goroutine на стек потока: новый кадр стека должен быть создан на последний и параметры к целевому вызову C должны быть размещены там (и в регистрах) в соответствии с ABI платформы.
  5. вызов сделан.
  6. По возвращении выполнение должно быть переключено обратно в стек программы, опять же путем направления всех возвращаемых результатов обратно в кадр стека выполняющей программы.

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

Что можно сделать с этим

Как правило, Существует два вектора для решения проблемы:

  • Делать вызовы C нечастыми.

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

  • Запись критических функций в сборке.

    Go позволяет писать код непосредственно в сборке целевой платформы H / W.

Один "трюк", который может позволить вам получить лучшее из обоих миров, - использование способность большинства промышленных компиляторов выводить форму ассемблера на языке ассемблера. Таким образом, вы можете использовать аппаратные средства, предоставляемые компилятором C, такие как автоматическая векторизация (для SSE) и агрессивная оптимизация, а затем захватить все, что оно сгенерировало, и обернуть его в тонкий слой сборки, который в основном адаптирует сгенерированный код к нативный ABI Go.

Существует множество сторонних Go пакетов, которые делают это (скажем, this и that ) и, очевидно, Go среда выполнения также делает это.

0 голосов
/ 18 марта 2020
package main

// #include <stdint.h>
// int64_t f(int64_t a, int64_t b, int64_t c, int64_t d) {return a+b+c+d; }
import "C"
import (
    "fmt"
    "math/rand"
    "time"
)

func f(a, b, c, d int64) int64 {
    return a + b + c + d
}

func main() {

    start := time.Now()
    for i := 1; i < 2000; i++ {
        a, b, c, d := rand.Int63(), rand.Int63(), rand.Int63(), rand.Int63()
        ans_c := int64(C.f((C.int64_t)(a), (C.int64_t)(b), (C.int64_t)(c), (C.int64_t)(d)))
        ans_c = ans_c
    }
    fmt.Printf("cgo time %v\n", time.Since(start))

    start = time.Now()
    for i := 1; i < 2000; i++ {
        a, b, c, d := rand.Int63(), rand.Int63(), rand.Int63(), rand.Int63()
        ans_go := f(a, b, c, d)
        ans_go = ans_go
    }
    fmt.Printf("go time %v\n", time.Since(start))
}

На моем компьютере go примерно в два раза быстрее. Но это может быть не так для всех функций. Например, функция C, оптимизированная для SIMD, может работать быстрее, чем эквивалент go, даже с учетом накладных расходов на вызов

cgo time 696.943µs
go time 370.842µs
...