Consideraciones de rendimiento de Haskell FFI / C?


Si utilizas Haskell como una biblioteca llamada desde mi programa C, ¿cuál es el impacto en el rendimiento de hacer llamadas a él? Por ejemplo, si tengo un conjunto de datos mundiales problema de digamos 20kB de datos, y quiero ejecutar algo como:

// Go through my 1000 actors and have them make a decision based on
// HaskellCode() function, which is compiled Haskell I'm accessing through
// the FFI.  As an argument, send in the SAME 20kB of data to EACH of these
// function calls, and some actor specific data
// The 20kB constant data defines the environment and the actor specific
// data could be their personality or state
for(i = 0; i < 1000; i++)
   actor[i].decision = HaskellCode(20kB of data here, actor[i].personality);

Lo que va a suceder aquí - ¿va a ser posible para mí mantener esos 20kB de datos como una referencia global inmutable en algún lugar al que se accede por el código Haskell, o debo crear una copia de esos datos cada vez a través de?

La preocupación es que estos datos podrían ser más grandes, mucho más grandes - también espero escribir algoritmos que actúen sobre conjuntos de datos mucho más grandes, utilizando el mismo patrón de datos inmutables que utilizan varias llamadas del código Haskell.

También, me gustaría paralelizar esto, como un dispatch_apply() GCD o Parallel.ForEach (..) C#. Mi justificación para la paralelización fuera de Haskell es que sé que siempre estaré operando en muchas llamadas a funciones separadas, es decir, 1000 actores, por lo que el uso de la paralelización de grano fino dentro de la función Haskell no es mejor que administrarla en el nivel C. ¿Ejecutar instancias de Haskell de FFI es 'Seguro para subprocesos' y cómo lo logro? ¿Necesito inicializar una instancia de Haskell cada vez que inicio una ejecución paralela? (Parece lento si debo..) ¿Cómo logro esto con un buen rendimiento?

Author: Don Stewart, 2011-04-14

4 answers

¿Cuál es el impacto en el rendimiento de hacer llamadas a él

Suponiendo que inicie el tiempo de ejecución de Haskell solo una vez ( así), en mi máquina, hacer una llamada a la función desde C a Haskell, pasando un Int de ida y vuelta a través del límite, toma aproximadamente 80,000 ciclos (31,000 ns en mi Núcleo 2) determined determinado experimentalmente a través del registro rdstc

¿Va a ser posible para mí mantener esos 20kB de datos como un referencia global inmutable en algún lugar al que se accede mediante el código de Haskell

Sí, eso es ciertamente posible. Si los datos realmente son inmutables, entonces obtendrás el mismo resultado si:

  • enhebrar los datos de ida y vuelta a través del límite del idioma mediante el marshalling;
  • pasar una referencia a los datos de ida y vuelta;
  • o guardarlo en un IORef en el lado de Haskell.

¿Qué estrategia es la mejor? Depende del tipo de datos. El más idiomático la manera sería pasar una referencia a los datos de C hacia adelante y hacia atrás, tratándolo como un ByteString o Vector en el lado de Haskell.

Me gustaría paralelizar esto

Yo fuertemente recomiendo invertir el control entonces, y hacer la paralelización desde el tiempo de ejecución de Haskell'll será mucho más robusto, ya que esa ruta ha sido fuertemente probada.

Con respecto a la seguridad del hilo, aparentemente es seguro hacer llamadas paralelas a foreign exported funciones que se ejecutan en el mismo tiempo de ejecución -- aunque es bastante seguro que nadie ha intentado esto con el fin de ganar paralelismo. Las llamadas en adquirir una capacidad, que es esencialmente un bloqueo, por lo que múltiples llamadas pueden bloquear, reduciendo sus posibilidades de paralelismo. En el caso de varios núcleos (por ejemplo, -N4 más o menos), los resultados pueden ser diferentes (hay múltiples capacidades disponibles), sin embargo, es casi seguro que esta es una mala manera de mejorar el rendimiento.

De nuevo, hacer muchas llamadas a funciones paralelas desde Haskell a través de forkIO es una ruta mejor documentada y mejor probada, con menos sobrecarga que haciendo el trabajo en el lado C, y probablemente menos código al final.

Simplemente haga una llamada a su función Haskell, que a su vez hará el paralelismo a través de muchos hilos Haskell. ¡Tranquilo!

 20
Author: Don Stewart,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-14 17:31:44

Utilizo una mezcla de hilos de C y Haskell para una de mis aplicaciones y no he notado que gran parte de un éxito de rendimiento de conmutación entre los dos. Así que diseñé un punto de referencia simple... que es un poco más rápido/más barato que Don. Esto está midiendo 10 millones de iteraciones en un 2.66 GHz i7:

$ ./foo
IO  : 2381952795 nanoseconds total, 238.195279 nanoseconds per, 160000000 value
Pure: 2188546976 nanoseconds total, 218.854698 nanoseconds per, 160000000 value

Compilado con GHC 7.0.3 / x86_64 y gcc-4.2.1 en OSX 10.6

ghc -no-hs-main -lstdc++ -O2 -optc-O2 -o foo ForeignExportCost.hs Driver.cpp

Haskell:

{-# LANGUAGE ForeignFunctionInterface #-}

module ForeignExportCost where

import Foreign.C.Types

foreign export ccall simpleFunction :: CInt -> CInt
simpleFunction i = i * i

foreign export ccall simpleFunctionIO :: CInt -> IO CInt
simpleFunctionIO i = return (i * i)

Y una aplicación de OSX C++ para manejarlo, debe ser fácil de ajustar a Windows o Linux:

#include <stdio.h>
#include <mach/mach_time.h>
#include <mach/kern_return.h>
#include <HsFFI.h>
#include "ForeignExportCost_stub.h"

static const int s_loop = 10000000;

int main(int argc, char** argv) {
    hs_init(&argc, &argv);

    struct mach_timebase_info timebase_info = { };
    kern_return_t err;
    err = mach_timebase_info(&timebase_info);
    if (err != KERN_SUCCESS) {
        fprintf(stderr, "error: %x\n", err);
        return err;
    }

    // timing a function in IO
    uint64_t start = mach_absolute_time();
    HsInt32 val = 0;
    for (int i = 0; i < s_loop; ++i) {
        val += simpleFunctionIO(4);
    }

    // in nanoseconds per http://developer.apple.com/library/mac/#qa/qa1398/_index.html
    uint64_t duration = (mach_absolute_time() - start) * timebase_info.numer / timebase_info.denom;
    double duration_per = static_cast<double>(duration) / s_loop;
    printf("IO  : %lld nanoseconds total, %f nanoseconds per, %d value\n", duration, duration_per, val);

    // run the loop again with a pure function
    start = mach_absolute_time();
    val = 0;
    for (int i = 0; i < s_loop; ++i) {
        val += simpleFunction(4);
    }

    duration = (mach_absolute_time() - start) * timebase_info.numer / timebase_info.denom;
    duration_per = static_cast<double>(duration) / s_loop;
    printf("Pure: %lld nanoseconds total, %f nanoseconds per, %d value\n", duration, duration_per, val);

    hs_exit();
}
 9
Author: Nathan Howell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-15 17:53:39
 3
Author: ShiDoiSi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-14 16:57:59

Descargo de responsabilidad: No tengo experiencia con la FFI.

Pero me parece que si quieres reutilizar los 20 Kb de datos para no pasarlos cada vez, entonces podrías simplemente tener un método que toma una lista de "personalidades" y devuelve una lista de "decisiones".

Así que si tienes una función

f :: LotsaData -> Personality -> Decision
f data p = ...

Entonces, ¿por qué no hacer una función auxiliar

helper :: LotsaData -> [Personality] -> [Decision]
helper data ps = map (f data) ps

E invocar eso? Usando esta manera, sin embargo, si desea paralelizar, tendría que hacerlo Haskell-side con listas paralelas y mapa paralelo.

Defiendo a los expertos para explicar si/cómo los arrays C pueden ser organizados en listas de Haskell (o estructura similar) fácilmente.

 1
Author: Dan Burton,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-14 17:51:46