Haskell -> C производительность FFI - PullRequest
1 голос
/ 21 февраля 2020

Это двойной вопрос Вопросы производительности Haskell FFI / C? : я хотел бы вызвать функцию C с минимальными издержками.

Чтобы установить сцену, у меня есть следующая функция C:

typedef struct
{
    uint64_t RESET;
} INPUT;

typedef struct
{
    uint64_t VGA_HSYNC;
    uint64_t VGA_VSYNC;
    uint64_t VGA_DE;
    uint8_t VGA_RED;
    uint8_t VGA_GREEN;
    uint8_t VGA_BLUE;
} OUTPUT;

void Bounce(const INPUT* input, OUTPUT* output);

Давайте запустим ее с C и рассчитаем время с gcc -O3:

int main (int argc, char **argv)
{
    INPUT input;
    input.RESET = 0;
    OUTPUT output;

    int cycles = 0;

    for (int j = 0; j < 60; ++j)
    {
        for (;; ++cycles)
        {
            Bounce(&input, &output);
            if (output.VGA_HSYNC == 0 && output.VGA_VSYNC == 0) break;
        }

        for (;; ++cycles)
        {
            Bounce(&input, &output);
            if (output.VGA_DE) break;
        }
    }

    printf("%d cycles\n", cycles);
}

Запуск его в течение 25152001 циклов занимает ~ 400 мс:

$ time ./Bounce
25152001 cycles

real    0m0.404s
user    0m0.403s
sys     0m0.001s

Теперь давайте напишем некоторый код Haskell для настройки FFI (обратите внимание, что экземпляр Storable Bool действительно использует полный int):

data INPUT = INPUT
    { reset :: Bool
    }

data OUTPUT = OUTPUT
    { vgaHSYNC, vgaVSYNC, vgaDE :: Bool
    , vgaRED, vgaGREEN, vgaBLUE :: Word64
    }
    deriving (Show)

foreign import ccall unsafe "Bounce" topEntity :: Ptr INPUT -> Ptr OUTPUT -> IO ()

instance Storable INPUT where ...
instance Storable OUTPUT where ...

И давайте сделаем то, что я считаю функционально эквивалентным нашему C коду, указанному ранее:

main :: IO ()
main = alloca $ \inp -> alloca $ \outp -> do
    poke inp $ INPUT{ reset = False }

    let loop1 n = do
            topEntity inp outp
            out@OUTPUT{..} <- peek outp
            let n' = n + 1
            if not vgaHSYNC && not vgaVSYNC then loop2 n' else loop1 n'
        loop2 n = do
            topEntity inp outp
            out <- peek outp
            let n' = n + 1
            if vgaDE out then return n' else loop2 n'
        loop3 k n
          | k < 60 = do
              n <- loop1 n
              loop3 (k + 1) n
          | otherwise = return n

    n <- loop3 (0 :: Int) (0 :: Int)
    printf "%d cycles" n

Я создаю его с помощью GH C 8.6. 5, используя -O3, и я получаю .. более 3 секунд!

$ time ./.stack-work/dist/x86_64-linux/Cabal-2.4.0.1/build/sim-ffi/sim-ffi
25152001 cycles

real   0m3.468s
user   0m3.146s
sys    0m0.280s

И это не постоянные накладные расходы при запуске: если я запускаю 10 раз циклов, я получаю примерно 3,5 секунд от C и 34 секунд от Haskell.

Что можно сделать, чтобы уменьшить накладные расходы Haskell -> C FFI?

1 Ответ

2 голосов
/ 22 февраля 2020

Мне удалось уменьшить накладные расходы, чтобы вызовы 25 М теперь заканчивались sh за 1,2 секунды. Изменения были:

  1. Сделать loop1, loop2 и loop3 строгими в аргументе n (используя BangPatterns)
  2. Добавить INLINE прагму peek в OUTPUT экземпляре Storable

Точка №1, конечно, глупа, но это то, что я получаю за то, что не профилировал ранее. Одно только это изменение дает мне 1,5 секунды ...

Пункт № 2, однако, имеет массу смысла и в целом применим. В нем также рассматривается комментарий @Thomas M. DuBuisson:

Вам когда-нибудь нужна структура Haskell в haskell? Если вы можете просто сохранить его как указатель на память и иметь несколько тестовых функций, таких как vgaVSYNC :: Ptr OUTPUT -> IO Bool, тогда это сохранит журнал копирования, выделения, G C работает при каждом вызове.

В возможной полной программе мне нужно просмотреть все поля OUTPUT. Тем не менее, с peek встроенным, GH C с удовольствием выполнит преобразование case-case-case, поэтому я вижу в Core, что теперь не выделено значение OUTPUT; выход peek потребляется напрямую.

...