Подтвердить что ты не робот

Сколько стоит Haskell FFI для перехода на C и обратно?

Если я хочу вызвать более одной функции C, каждая из которых зависит от результата предыдущей, лучше ли создать функцию C-оболочки, которая обрабатывает три вызова? Будет ли это стоить так же, как с использованием Haskell FFI без преобразования типов?

Предположим, что у меня есть следующий код Haskell:

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

Каждая функция cf* является вызовом C.

Лучше ли с точки зрения производительности создать одну C-функцию типа cfABC и сделать только один внешний вызов в Haskell?

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

Код Haskell:

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

Как измерить стоимость исполнения вызова C от Haskell? Не стоимость самой функции C, а стоимость "переключения контекста" с Haskell на C и обратно.

4b9b3361

Ответ 1

Ответ зависит в основном от того, является ли внешний вызов вызовом safe или unsafe.

An unsafe C вызов - это просто вызов функции, поэтому, если нет (нетривиального) преобразования типа, есть три вызова функций, если вы делаете три внешних вызова, а между 1 и 4, когда вы пишете обертку в C, в зависимости от того, сколько компонентов компонента может быть встроено при компиляции C, поскольку внешний вызов C не может быть встроен GHC. Такой вызов функции, как правило, очень дешев (это просто копия аргументов и переход к коду), поэтому разница небольшая в любом случае, оболочка должна быть немного медленнее, если в оболочку не может быть включена функция C, и немного быстрее, когда все могут быть встроены [и это действительно имело место в моем бенчмаркинге, + 1.5ns соответственно. -3.5ns, где три внешних вызова заняли около 12.7ns для всего, что только вернуло аргумент]. Если функции делают что-то нетривиальное, разница незначительна (и если они ничего не делают нетривиальными, вы, вероятно, лучше напишите их в Haskell, чтобы GHC установил код).

A safe C вызов включает сохранение некоторого нетривиального количества состояния, блокирование, возможно, появление нового потока ОС, так что это займет гораздо больше времени. Тогда небольшие накладные расходы, возможно, вызывая одну функцию больше в C, пренебрежимо малы по сравнению со стоимостью внешних вызовов [если передача аргументов не требует необычного количества копий, многие огромные struct или около того]. В своем тесте do-nothing

{-# 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
            ]

где все функции C просто возвращают аргумент, среднее значение для одиночного обернутого вызова немного выше 100ns [105-112], а для трех отдельных вызовов около 300ns [290-315].

Таким образом, вызов safe c принимает примерно 100 нс и, как правило, он быстрее сворачивает их в один вызов. Но все же, если вызываемые функции делают что-то достаточно нетривиальное, разница не имеет значения.

Ответ 2

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

На более философской мелодии каждый раз, когда вы смешиваете языки, вы создаете барьер для новичков: в этом случае недостаточно быть свободным в Haskell и C (это уже дает узкий набор), но вы также должны знать соглашения о вызовах и многое другое, чтобы работать с ними. И много раз есть тонкие проблемы для обработки (даже вызов C из С++, которые являются очень похожими языками, вовсе не тривиальны). Если у нас нет веских причин, я бы придерживался одного языка. Единственное исключение, о котором я могу думать, - это создание, например, Привязки Haskell к существующей сложной библиотеке, что-то вроде NumPy для Python.