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?

28
demandé sur Don Stewart 2011-04-14 18:59:33

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!

20
répondu Don Stewart 2011-04-14 17:31:44

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();
}
9
répondu Nathan Howell 2011-04-15 17:53:39

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.

1
répondu Dan Burton 2011-04-14 17:51:46