Почему производительность cgo настолько медленная? что-то не так с моим тестовым кодом?

Я делаю тест: сравнивайте время excecution cgo и чистые функции Go, выполняемые 100 миллионов раз каждый. Функция cgo занимает больше времени по сравнению с функцией Голанга, и я смущен этим результатом. Мой тестовый код:

package main

import (
    "fmt"
    "time"
)

/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void show() {

}

*/
// #cgo LDFLAGS: -lstdc++
import "C"

//import "fmt"

func show() {

}

func main() {
    now := time.Now()
    for i := 0; i < 100000000; i = i + 1 {
        C.show()
    }
    end_time := time.Now()

    var dur_time time.Duration = end_time.Sub(now)
    var elapsed_min float64 = dur_time.Minutes()
    var elapsed_sec float64 = dur_time.Seconds()
    var elapsed_nano int64 = dur_time.Nanoseconds()
    fmt.Printf("cgo show function elasped %f minutes or \nelapsed %f seconds or \nelapsed %d nanoseconds\n",
        elapsed_min, elapsed_sec, elapsed_nano)

    now = time.Now()
    for i := 0; i < 100000000; i = i + 1 {
        show()
    }
    end_time = time.Now()

    dur_time = end_time.Sub(now)
    elapsed_min = dur_time.Minutes()
    elapsed_sec = dur_time.Seconds()
    elapsed_nano = dur_time.Nanoseconds()
    fmt.Printf("go show function elasped %f minutes or \nelapsed %f seconds or \nelapsed %d nanoseconds\n",
        elapsed_min, elapsed_sec, elapsed_nano)

    var input string
    fmt.Scanln(&input)
}

и результат:

cgo show function elasped 0.368096 minutes or 
elapsed 22.085756 seconds or 
elapsed 22085755775 nanoseconds

go show function elasped 0.000654 minutes or 
elapsed 0.039257 seconds or 
elapsed 39257120 nanoseconds

Результаты показывают, что вызов функции C медленнее, чем функция Go. Что-то не так с моим тестовым кодом?

Моя система: mac OS X 10.9.4 (13E28)

Ответ 1

Как вы обнаружили, есть довольно высокие накладные расходы при вызове кода C/С++ через CGo. Поэтому в целом вам лучше всего попытаться свести к минимуму количество вызовов CGo, которые вы делаете. Для приведенного выше примера, вместо того, чтобы неоднократно вызывать функцию CGo в цикле, может иметь смысл перемещать цикл до C.

Существует несколько аспектов того, как среда выполнения Go устанавливает свои потоки, которые могут нарушить ожидания многих фрагментов кода C:

  • Goroutines работают на относительно небольшом стеке, обрабатывая рост стека через сегментированные стеки (старые версии) или путем копирования (новые версии).
  • Потоки, созданные при запуске Go, могут неправильно взаимодействовать с реализацией локального хранилища libpthread.
  • Обработчик сигналов UNIX в режиме ожидания может помешать традиционному коду C или С++.
  • Go повторно использует потоки ОС для запуска нескольких Goroutines. Если код C вызвал системный вызов блокировки или иначе монополизировал поток, это может нанести ущерб другим goroutines.

По этим причинам CGo выбирает безопасный подход к запуску кода C в отдельном потоке, настроенном на традиционный стек.

Если вы исходите из таких языков, как Python, где нередко переписывать горячие точки кода в C, чтобы ускорить работу программы, вы будете разочарованы. Но в то же время существует намного меньший разрыв в производительности между эквивалентными C и кодом Go.

В целом я резервирую CGo для взаимодействия с существующими библиотеками, возможно с небольшими функциями оболочки C, которые могут уменьшить количество вызовов, которые мне нужно сделать из Go.

Ответ 2

Обновление для James answer: кажется, что в текущей реализации нет нити-переключателя.

Смотрите этот поток на golang-nut:

Там всегда будут некоторые накладные расходы. Это дороже, чем простой вызов функции, но значительно дешевле, чем переключатель контекста (agl помнит о более ранней реализации; мы отключили переключатель потока до публичного выпуска). В настоящее время расходы в основном сделать полный переключатель набора регистров (без участия ядра). Я бы предположил, что это сопоставимо с десятью вызовами функций.

См. также этот ответ, который ссылается на "cgo is not Go" blog пост.

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

Таким образом, у cgo есть накладные расходы, поскольку он выполняет переключатель стека, а не переключатель потока.

Он сохраняет и восстанавливает все регистры при вызове функции C, в то время как это не требуется, когда вызывается функция Go или функция сборки.


Кроме того, соглашения, вызывающие вызов cgo, запрещают передачу указателей Go непосредственно на C-код, а обычным обходным путем является использование C.malloc и поэтому вводят дополнительные распределения. Подробнее см. этот вопрос.

Ответ 3

В вызове функций C из Go есть немного накладных расходов. Это невозможно изменить.