performance - ¿Consideraciones de rendimiento de Haskell FFI/C?
parallel-processing (4)
Si utilizo Haskell como una biblioteca desde mi programa C, ¿cuál es el impacto en el rendimiento de hacer llamadas? Por ejemplo, si tengo un conjunto de datos mundiales problemáticos de, por ejemplo, 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);
¿Qué va a pasar aquí? ¿Me será posible mantener esos 20kB de datos como una referencia global e inmutable en algún lugar al que se accede mediante el código de Haskell, o debo crear una copia de esos datos cada vez que lo haga?
La preocupación es que estos datos pueden 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 de Haskell.
También, me gustaría paralelizar esto, como un dispatch_apply () GCD o Parallel.ForEach (..) C #. Mi razonamiento para la paralelización fuera de Haskell es que sé que siempre estaré operando en muchas llamadas de función separadas, es decir, 1000 actores, por lo que usar la paralelización de grano fino dentro de la función de Haskell no es mejor que administrarla en el nivel C. ¿Se están ejecutando las instancias de FFI Haskell ''Thread Safe'' y cómo puedo lograrlo? ¿Necesito inicializar una instancia de Haskell cada vez que comience una ejecución paralela? (Parece lento si debo ...) ¿Cómo logro esto con un buen rendimiento?
¿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 ( como esto ), en mi máquina, hacer una llamada de función de C a Haskell, pasar un Int de un lado a otro a través del límite, toma aproximadamente 80,000 ciclos ( 31,000 ns en mi Core 2) - - Determinado experimentalmente a través del registro rdstc.
¿Me será posible mantener esos 20kB de datos como una referencia global e 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á el mismo resultado si:
- enhebre los datos de un lado a otro a través del límite del idioma mediante la ordenación;
- pasar una referencia a los datos de ida y vuelta;
- o caché en un
IORef
en el lado de Haskell.
¿Cuál es la mejor estrategia? Depende del tipo de datos. La forma más idiomática sería pasar una referencia a los datos de C, tratándola como ByteString
o Vector
en el lado Haskell.
Me gustaría paralelizar esto
Recomiendo encarecidamente invertir el control en ese momento, y hacer la paralelización desde el tiempo de ejecución de Haskell; será mucho más sólido, ya que esa ruta se ha probado en gran medida.
Con respecto a la seguridad de subprocesos, es aparentemente seguro hacer llamadas paralelas a funciones foreign exported
que se ejecutan en el mismo tiempo de ejecución, aunque bastante seguro de que nadie lo ha intentado para obtener el paralelismo. Las llamadas adquieren una capacidad, que es esencialmente un bloqueo, por lo que pueden bloquearse múltiples llamadas, lo que reduce las posibilidades de paralelismo. En el caso de -N4
(por ejemplo, -N4
o así), sus resultados pueden ser diferentes (hay múltiples capacidades disponibles), sin embargo, es casi seguro que esta es una mala forma de mejorar el rendimiento.
Una vez más, hacer muchas llamadas de funciones paralelas desde Haskell a través de forkIO
es una forkIO
mejor documentada, 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 de Haskell, que a su vez hará el paralelismo a través de muchos hilos de Haskell. ¡Fácil!
Descargo de responsabilidad: no tengo experiencia con la FFI.
Pero me parece que si desea reutilizar los 20 Kb de datos para no pasarlos cada vez, simplemente podría tener un método que tome una lista de "personalidades" y devuelva 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 de ayuda?
helper :: LotsaData -> [Personality] -> [Decision]
helper data ps = map (f data) ps
¿Y invocar eso? Usando de esta manera, sin embargo, si desea paralelizar, tendría que hacerlo del lado de Haskell con listas paralelas y mapa paralelo.
Me refiero a los expertos para explicar si / cómo los arrays de C se pueden convertir en listas de Haskell (o estructura similar) fácilmente.
Utilizo una combinación de subprocesos C y Haskell para una de mis aplicaciones y no he notado que haya un gran éxito en el cambio entre las dos. Así que elaboré un simple punto de referencia ... que es bastante más rápido / barato que el de Don. Esto mide 10 millones de iteraciones en un i7 de 2.66GHz:
$ ./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 OSX C ++ para manejarlo, debería 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();
}