Considérations de Performance de Haskell FFI / C?
Si vous utilisez Haskell comme bibliothèque {[3] } appelée depuis mon programme C, Quel est l'impact sur les performances des appels? Par exemple, si j'ai un ensemble de données monde problématique de dire 20kB de données, et je veux exécuter quelque chose comme:
// 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'est-ce qui va se passer ici - est-ce que je vais être possible de garder ces 20kB de données en tant que référence immuable globale quelque part accessible par le code Haskell, ou dois-je créer une copie de ces données à chaque fois à travers?
La préoccupation est que ces données pourraient être plus grandes, beaucoup plus grandes-j'espère aussi écrire des algorithmes qui agissent sur des ensembles de données beaucoup plus grands, en utilisant le même modèle de données immuables utilisées par plusieurs appels du code Haskell.
Aussi, je voudrais paralléliser ceci, comme un dispatch_apply () GCD ou Parallel.ForEach(..) C#. Ma raison d'être de la parallélisation en dehors de Haskell est que je sais que je vais toujours opérer sur de nombreux appels de fonctions distincts, c'est-à-dire 1000 acteurs, donc en utilisant la parallélisation fine à l'intérieur de la fonction Haskell n'est pas meilleure que de la gérer au niveau C. L'exécution des instances FFI Haskell est-elle 'Thread Safe' et comment puis - je y parvenir-dois-je initialiser une instance Haskell chaque fois que je lance une exécution parallèle? (Semble lent si je dois..) Comment puis-je y parvenir avec de bonnes performances?
4 réponses
Quel est l'impact sur les performances des appels
En supposant que vous démarrez le runtime Haskell une seule fois (comme ceci), sur ma machine, faire un appel de fonction de C dans Haskell, passer un int avant et en arrière à travers la limite, prend environ 80 000 cycles (31,000 ns sur mon noyau 2) -- déterminé expérimentalement via le registrerdstc
Est-ce qu'il sera possible pour moi de garder ces 20kB de données en tant que référence immuable globale quelque part accessible par le code Haskell
Oui, c'est certainement possible. Si les données sont vraiment immuables, alors vous obtenez le même résultat si vous:
- enfilez les données d'avant en arrière à travers la limite de la langue en marshalling;
- passer une référence aux données avant et en arrière;
- ou le mettre en cache dans un
IORef
du côté Haskell.
Quelle est la meilleure stratégie? Il dépend du type de données. Les plus idiomatiques le moyen serait de passer une référence aux données C en arrière, en les traitant comme un ByteString
ou Vector
du côté de Haskell.
Je voudrais paralléliser ceci
Jerecommande fortement d'inverser le contrôle alors, et de faire la parallélisation à partir du runtime Haskell-ce sera beaucoup plus robuste, car ce chemin a été fortement testé.
En ce qui concerne la sécurité des threads, il est apparemment sûr de faire des appels parallèles aux fonctions foreign exported
s'exécutant dans le même temps d'exécution -- bien que assez sûr que personne n'a essayé cela afin de gagner le parallélisme. Les appels acquièrent une capacité, qui est essentiellement un verrou, de sorte que plusieurs appels peuvent bloquer, réduisant vos chances de parallélisme. Dans le cas multicœur (par exemple -N4
ou plus), vos résultats peuvent être différents (plusieurs capacités sont disponibles), cependant, c'est presque certainement un mauvais moyen d'améliorer les performances.
Encore une fois, faire de nombreux appels de fonctions parallèles depuis Haskell via {[5] } est un chemin mieux documenté et mieux testé, avec moins de frais généraux que de faire le travail du côté C, et probablement moins de code à la fin.
Il suffit de faire un appel dans votre fonction Haskell, qui à son tour fera le parallélisme via de nombreux threads Haskell. Facile!
J'utilise un mélange de threads C et Haskell pour l'une de mes applications et je n'ai pas remarqué que beaucoup de performances frappent la commutation entre les deux. J'ai donc créé une référence simple... ce qui est un peu plus rapide/moins cher que celui de Don. cela mesure 10 millions d'itérations sur un i7 2.66 GHz:
$ ./foo
IO : 2381952795 nanoseconds total, 238.195279 nanoseconds per, 160000000 value
Pure: 2188546976 nanoseconds total, 218.854698 nanoseconds per, 160000000 value
Compilé avec GHC 7.0.3 / x86_64 et gcc-4.2.1 sur 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)
Et une application OSX C++ pour le conduire, devrait être simple à ajuster à Windows ou 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();
}
Avertissement: Je n'ai aucune expérience avec le FFI.
Mais il me semble que si vous voulez réutiliser les 20 Ko de données pour ne pas les transmettre à chaque fois, alors vous pourriez simplement avoir une méthode qui prend une liste de" personnalités", et renvoie une liste de"décisions".
Donc, si vous avez une fonction
f :: LotsaData -> Personality -> Decision
f data p = ...
Alors pourquoi ne pas créer une fonction d'aide
helper :: LotsaData -> [Personality] -> [Decision]
helper data ps = map (f data) ps
Et invoquer cela? En utilisant cette façon, si vous voulez paralléliser, vous devez le faire Haskell-side avec listes parallèles et carte parallèle.
Je m'en remets aux experts pour expliquer si/comment les tableaux C peuvent être marshalés dans des listes Haskell (ou une structure similaire) facilement.