¿Por qué el rendimiento de cgo es tan lento? ¿Hay algo mal con mi código de prueba?

Estoy haciendo una prueba: compare los tiempos de ejecución de cgo y las funciones puras de Go ejecutan 100 millones de veces cada una. La función cgo lleva más tiempo en comparación con la función de Golang, y estoy confundido con este resultado. Mi código de prueba es:

package main import ( "fmt" "time" ) /* #include  #include  #include  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) } 

y el resultado es:

 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 

Los resultados muestran que invocar la función C es más lento que la función Ir. ¿Hay algo mal con mi código de prueba?

Mi sistema es: mac OS X 10.9.4 (13E28)

Como ha descubierto, hay una sobrecarga bastante alta al llamar el código C / C ++ a través de CGo. Por lo tanto, en general, es mejor tratar de minimizar la cantidad de llamadas de CGo que realiza. Para el ejemplo anterior, en lugar de llamar repetidamente a una función CGo en un bucle, podría tener sentido mover el bucle hacia abajo a C.

Hay una serie de aspectos de cómo el tiempo de ejecución de Go configura sus hilos que pueden romper las expectativas de muchas piezas de código C:

  1. Los goroutines se ejecutan en una stack relativamente pequeña, manejando el crecimiento de la stack a través de stacks segmentadas (versiones antiguas) o copiando (nuevas versiones).
  2. Los subprocesos creados por el tiempo de ejecución de Go pueden no interactuar correctamente con la implementación de almacenamiento local de subprocesos de libpthread .
  3. El controlador de señales UNIX del tiempo de ejecución de Go puede interferir con el código C o C ++ tradicional.
  4. Go reutiliza los hilos del sistema operativo para ejecutar múltiples Goroutines. Si el código C se llama una llamada al sistema de locking o si de otra manera monopoliza el hilo, podría ser perjudicial para otros goroutines.

Por estas razones, CGo elige el método seguro de ejecutar el código C en un subproceso separado configurado con una stack tradicional.

Si viene de idiomas como Python, donde no es infrecuente reescribir puntos de acceso de código en C como una forma de acelerar un progtwig, se sentirá decepcionado. Pero al mismo tiempo, existe una brecha mucho menor en el rendimiento entre el código C y Go equivalentes.

En general, reservo CGo para interactuar con bibliotecas existentes, posiblemente con pequeñas funciones de envoltorio de C que pueden reducir la cantidad de llamadas que necesito hacer desde Go.

Actualización para la respuesta de James: parece que no hay un cambio de hilo en la implementación actual.

Ver este hilo en golang-nuts:

Siempre va a haber alguna sobrecarga. Es más costoso que una simple llamada de función, pero significativamente menos costoso que un cambio de contexto (agl es recordar una implementación anterior; cortamos el cambio de hilo antes de la publicación pública ). En este momento, el gasto es básicamente tener que hacer un cambio de conjunto de registro completo (sin participación del núcleo). Supongo que es comparable a diez llamadas de función.

Vea también esta respuesta que enlaza la publicación del blog “cgo is go” .

C no sabe nada sobre la convención de llamadas de Go ni sobre las stacks que se pueden cultivar, por lo que una llamada al código C debe registrar todos los detalles de la stack goroutine, cambiar a la stack C y ejecutar el código C que no tiene conocimiento de cómo se invocó , o el mayor tiempo de ejecución de Go a cargo del progtwig.

Por lo tanto, cgo tiene una sobrecarga porque realiza un cambio de stack , no un cambio de hilo.

Guarda y restaura todos los registros cuando se llama la función C, mientras que no se requiere cuando se llama la función Ir o la función de ensamblaje.


Además de eso, las convenciones de llamada de cgo prohíben pasar punteros Go directamente al código C, y la solución común es usar C.malloc , y así introducir asignaciones adicionales. Vea esta pregunta para más detalles.

Hay una pequeña sobrecarga al llamar a las funciones C desde Go. Esto no se puede cambiar.