c performance haskell ffi

¿Cuánto le cuesta a Haskell FFI ingresar a C y volver?



performance (2)

Si quiero llamar a más de una función C, cada una dependiendo del resultado de la anterior, ¿es mejor crear una función C que contenga las tres llamadas? ¿Costará lo mismo que usar Haskell FFI sin convertir tipos?

Supongamos que tengo el siguiente código de Haskell:

foo :: CInt -> IO CInt foo x = do a <- cfA x b <- cfB a c <- cfC c return c

Cada función cf* es una llamada C

¿Es mejor, en términos de rendimiento, crear una única función de C como cfABC y hacer solo una llamada externa en Haskell?

int cfABC(int x) { int a, b, c; a = cfA(x); b = cfB(a); c = cfC(b); return c; }

Código Haskell:

foo :: CInt -> IO CInt foo x = do c <- cfABC x return c

¿Cómo medir el costo de rendimiento de una llamada C de Haskell? No el costo de la función C en sí, sino el costo del "cambio de contexto" de Haskell a C y viceversa.


Eso probablemente depende mucho de su compilador de Haskell exacto, el compilador de C y el pegamento que los une. La única forma de saberlo con seguridad es midiéndolo.

En una melodía más filosófica, cada vez que mezcla idiomas, crea una barrera para los recién llegados: en este caso no es suficiente tener fluidez en Haskell y C (que ya ofrece un conjunto limitado), pero también debe conocer el llamado. Convenciones y todo lo necesario para trabajar con ellos. Y muchas veces hay problemas sutiles que manejar (incluso llamar a C desde C ++, que son lenguajes muy similares no es para nada trivial). A menos que haya razones muy convincentes, me quedo con un solo idioma. La única excepción que se me ocurre de repente es para crear, por ejemplo, enlaces Haskell a una biblioteca compleja preexistente, algo como NumPy para Python.


La respuesta depende principalmente de si la llamada externa es una llamada safe o unsafe .

Una llamada C unsafe es básicamente una llamada de función, por lo que si no hay una conversión de tipo (no trivial), hay tres llamadas de función si hace tres llamadas externas, y entre una y cuatro cuando escribe una envoltura en C, dependiendo de cuántas Las funciones de los componentes se pueden alinear al compilar la C, ya que una llamada externa a C no puede ser incorporada por el GHC. Tal llamada de función es generalmente muy barata (es solo una copia de los argumentos y un salto al código), por lo que la diferencia es pequeña en cualquier caso, la envoltura debe ser un poco más lenta cuando no se puede insertar una función C en la envoltura, y un poco más rápido cuando todo se puede insertar [y ese fue el caso en mi evaluación comparativa, + 1.5ns resp. -3.5ns donde las tres llamadas extranjeras tomaron aproximadamente 12.7ns para todo lo que acaba de devolver el argumento]. Si las funciones hacen algo no trivial, la diferencia es insignificante (y si no están haciendo algo no trivial, es mejor que las escriba en Haskell para permitir que GHC inserte el código).

Una llamada C safe implica guardar una cantidad de estado no trivial, el bloqueo y, posiblemente, generar un nuevo subproceso del sistema operativo, por lo que lleva mucho más tiempo. Entonces, la pequeña sobrecarga de quizás llamar a una función más en C es insignificante en comparación con el costo de las llamadas externas [a menos que pasar los argumentos requiera una cantidad inusual de copia, muchas struct enormes más o menos]. En mi punto de referencia de no hacer nada

{-# LANGUAGE ForeignFunctionInterface #-} module Main (main) where import Criterion.Main import Foreign.C.Types import Control.Monad foreign import ccall safe "funcs.h cfA" c_cfA :: CInt -> IO CInt foreign import ccall safe "funcs.h cfB" c_cfB :: CInt -> IO CInt foreign import ccall safe "funcs.h cfC" c_cfC :: CInt -> IO CInt foreign import ccall safe "funcs.h cfABC" c_cfABC :: CInt -> IO CInt wrap :: (CInt -> IO CInt) -> Int -> IO Int wrap foo arg = fmap fromIntegral $ foo (fromIntegral arg) cfabc = wrap c_cfABC foo :: Int -> IO Int foo = wrap (c_cfA >=> c_cfB >=> c_cfC) main :: IO () main = defaultMain [ bench "three calls" $ foo 16 , bench "single call" $ cfabc 16 ]

donde todas las funciones de C simplemente devuelven el argumento, la media para la única llamada envuelta es un poco superior a 100ns [105-112], y para las tres llamadas separadas alrededor de 300ns [290-315].

Por lo tanto, una llamada c safe requiere aproximadamente 100 ns y, por lo general, es más rápido envolverlos en una sola llamada. Pero aún así, si las funciones llamadas hacen algo suficientemente no trivial, la diferencia no importará.