42 votes

Haskell Thrift library 300x plus lente que C ++ dans les tests de performances

Je suis en train de construire une application qui contient deux composants - serveur écrit en Haskell, et le client écrit en Qt (C++). Je suis l'aide de l'épargne pour communiquer entre eux, et je me demande pourquoi est-ce de travailler de manière lente.

J'ai fait un test de performance et voici le résultat sur ma machine

Résultats

C++ server and C++ client:

Sending 100 pings                    -    13.37 ms
Transfering 1000000 size vector      -   433.58 ms
Recieved: 3906.25 kB
Transfering 100000 items from server -  1090.19 ms
Transfering 100000 items to server   -   631.98 ms

Haskell server and C++ client:

Sending 100 pings                       3959.97 ms
Transfering 1000000 size vector      - 12481.40 ms
Recieved: 3906.25 kB
Transfering 100000 items from server - 26066.80 ms
Transfering 100000 items to server   -  1805.44 ms

Pourquoi est-Haskell si lent dans ce test? Comment puis-je améliorer la performance?

Voici les fichiers:

Fichiers

les performances.l'épargne

namespace hs test
namespace cpp test

struct Item {
    1: optional string    name
    2: optional list<i32> coordinates
}

struct ItemPack {
    1: optional list<Item>     items
    2: optional map<i32, Item> mappers
}


service ItemStore {
    void ping()
    ItemPack getItems(1:string name, 2: i32 count) 
    bool     setItems(1: ItemPack items)

    list<i32> getVector(1: i32 count)
}

Principal.hs

{-# LANGUAGE ScopedTypeVariables #-}   
module Main where

import           Data.Int  
import           Data.Maybe (fromJust) 
import qualified Data.Vector as Vector
import qualified Data.HashMap.Strict  as HashMap
import           Network

-- Thrift libraries
import           Thrift.Server

-- Generated Thrift modules
import Performance_Types
import ItemStore_Iface
import ItemStore


i32toi :: Int32 -> Int
i32toi = fromIntegral

itoi32 :: Int -> Int32
itoi32 = fromIntegral

port :: PortNumber
port = 9090

data ItemHandler = ItemHandler

instance ItemStore_Iface ItemHandler where
    ping _                   = return () --putStrLn "ping"
    getItems _ mtname mtsize = do 
        let size = i32toi $ fromJust mtsize
            item i = Item mtname (Just $ Vector.fromList $ map itoi32 [i..100])
            items = map item [0..(size-1)]
            itemsv = Vector.fromList items 
            mappers = zip (map itoi32 [0..(size-1)]) items 
            mappersh = HashMap.fromList mappers
            itemPack = ItemPack (Just itemsv) (Just mappersh)
        putStrLn "getItems"
        return itemPack

    setItems _ _             = do putStrLn "setItems"
                                  return True

    getVector _ mtsize       = do putStrLn "getVector"
                                  let size = i32toi $ fromJust mtsize
                                  return $ Vector.generate size itoi32

main :: IO ()
main = do
    _ <- runBasicServer ItemHandler process port 
    putStrLn "Server stopped"

ItemStore_client.cpp

#include <iostream>
#include <chrono>
#include "gen-cpp/ItemStore.h"

#include <transport/TSocket.h>
#include <transport/TBufferTransports.h>
#include <protocol/TBinaryProtocol.h>

using namespace apache::thrift;
using namespace apache::thrift::protocol;
using namespace apache::thrift::transport;

using namespace test;
using namespace std;

#define TIME_INIT  std::chrono::_V2::steady_clock::time_point start, stop; \
                   std::chrono::duration<long long int, std::ratio<1ll, 1000000000ll> > duration;
#define TIME_START start = std::chrono::steady_clock::now(); 
#define TIME_END   duration = std::chrono::steady_clock::now() - start; \
                   std::cout << chrono::duration <double, std::milli> (duration).count() << " ms" << std::endl;

int main(int argc, char **argv) {

    boost::shared_ptr<TSocket> socket(new TSocket("localhost", 9090));
    boost::shared_ptr<TTransport> transport(new TBufferedTransport(socket));
    boost::shared_ptr<TProtocol> protocol(new TBinaryProtocol(transport));

    ItemStoreClient server(protocol);
    transport->open();

    TIME_INIT

    long pings = 100;
    cout << "Sending " << pings << " pings" << endl;
    TIME_START
    for(auto i = 0 ; i< pings ; ++i)
        server.ping();
    TIME_END


    long vectorSize = 1000000;

    cout << "Transfering " << vectorSize << " size vector" << endl;
    std::vector<int> v;
    TIME_START
    server.getVector(v, vectorSize);
    TIME_END
    cout << "Recieved: " << v.size()*sizeof(int) / 1024.0 << " kB" << endl;


    long itemsSize = 100000;

    cout << "Transfering " << itemsSize << " items from server" << endl;
    ItemPack items;
    TIME_START
    server.getItems(items, "test", itemsSize);
    TIME_END


    cout << "Transfering " << itemsSize << " items to server" << endl;
    TIME_START
    server.setItems(items);
    TIME_END

    transport->close();

    return 0;
}

ItemStore_server.cpp

#include "gen-cpp/ItemStore.h"
#include <thrift/protocol/TBinaryProtocol.h>
#include <thrift/server/TSimpleServer.h>
#include <thrift/transport/TServerSocket.h>
#include <thrift/transport/TBufferTransports.h>

#include <map>
#include <vector>

using namespace ::apache::thrift;
using namespace ::apache::thrift::protocol;
using namespace ::apache::thrift::transport;
using namespace ::apache::thrift::server;


using namespace test;
using boost::shared_ptr;

class ItemStoreHandler : virtual public ItemStoreIf {
  public:
    ItemStoreHandler() {
    }

    void ping() {
        // printf("ping\n");
    }

    void getItems(ItemPack& _return, const std::string& name, const int32_t count) {

        std::vector <Item> items;
        std::map<int, Item> mappers;

        for(auto i = 0 ; i < count ; ++i){
            std::vector<int> coordinates;
            for(auto c = i ; c< 100 ; ++c)
                coordinates.push_back(c);

            Item item;
            item.__set_name(name);
            item.__set_coordinates(coordinates);

            items.push_back(item);
            mappers[i] = item;
        }

        _return.__set_items(items);
        _return.__set_mappers(mappers);
        printf("getItems\n");
    }

    bool setItems(const ItemPack& items) {
        printf("setItems\n");
        return true;
    }

    void getVector(std::vector<int32_t> & _return, const int32_t count) {
        for(auto i = 0 ; i < count ; ++i)
            _return.push_back(i);
        printf("getVector\n");
    }
};

int main(int argc, char **argv) {
    int port = 9090;
    shared_ptr<ItemStoreHandler> handler(new ItemStoreHandler());
    shared_ptr<TProcessor> processor(new ItemStoreProcessor(handler));
    shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));
    shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());
    shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());

    TSimpleServer server(processor, serverTransport, transportFactory, protocolFactory);
    server.serve();
    return 0;
}

makefile

GEN_SRC := gen-cpp/ItemStore.cpp gen-cpp/performance_constants.cpp gen-cpp/performance_types.cpp
GEN_OBJ := $(patsubst %.cpp,%.o, $(GEN_SRC))

THRIFT_DIR := /usr/local/include/thrift
BOOST_DIR := /usr/local/include

INC := -I$(THRIFT_DIR) -I$(BOOST_DIR)

.PHONY: all clean

all:   ItemStore_server ItemStore_client

%.o: %.cpp
    $(CXX) --std=c++11 -Wall -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H $(INC) -c $< -o $@

ItemStore_server: ItemStore_server.o $(GEN_OBJ) 
    $(CXX) $^ -o $@ -L/usr/local/lib -lthrift -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H

ItemStore_client: ItemStore_client.o $(GEN_OBJ)
    $(CXX) $^ -o $@ -L/usr/local/lib -lthrift -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H

clean:
    $(RM) *.o ItemStore_server ItemStore_client

Compiler et exécuter

Je générer des fichiers (à l'aide de l'épargne de 0,9 disponible ici) avec:

$ thrift --gen cpp performance.thrift
$ thrift --gen hs performance.thrift

Compiler avec

$ make
$ ghc Main.hs gen-hs/ItemStore_Client.hs gen-hs/ItemStore.hs gen-hs/ItemStore_Iface.hs gen-hs/Performance_Consts.hs gen-hs/Performance_Types.hs -Wall -O2

Exécuter Haskell test:

$ ./Main& 
$ ./ItemStore_client

Exécution C++ test:

$ ./ItemStore_server&
$ ./ItemStore_client

N'oubliez pas de les tuer après chaque serveur de test

Mise à jour:

Édité getVector méthode Vector.generate au lieu de Vector.fromList, mais toujours pas d'effet

Mise à jour 2:

Grâce à la suggestion de @MdxBhmt j'ai testé l' getItems fonction comme suit:

getItems _ mtname mtsize = do let size = i32toi $! fromJust mtsize
                                  item i = Item mtname (Just $!  Vector.enumFromN (i::Int32) (100- (fromIntegral i)))
                                  itemsv = Vector.map item  $ Vector.enumFromN 0  (size-1)
                                  itemPack = ItemPack (Just itemsv) Nothing 
                              putStrLn "getItems"
                              return itemPack

qui est stricte et il a amélioré Vecteur génération vs alternative, basée sur mon original de mise en œuvre:

getItems _ mtname mtsize = do let size = i32toi $ fromJust mtsize
                                  item i = Item mtname (Just $ Vector.fromList $ map itoi32 [i..100])
                                  items = map item [0..(size-1)]
                                  itemsv = Vector.fromList items 
                                  itemPack = ItemPack (Just itemsv) Nothing
                              putStrLn "getItems"
                              return itemPack

Notez qu'il n'existe pas de table de hachage envoyé. La première version donne le temps 12338.2 ms et la deuxième est 11698.7 ms, pas de speedup :(

Mise à jour 3:

J'ai signalé un problème à l'Épargne Jira

27voto

MdxBhmt Points 935

Tout le monde pointe qui est le coupable est les aubaines de la bibliothèque, mais je vais me concentrer sur votre code (et où je peux obtenir de l'aide de la vitesse)

En utilisant une version simplifiée de votre code, si vous calculez itemsv:

testfunc mtsize =  itemsv
  where size = i32toi $ fromJust mtsize
        item i = Item (Just $ Vector.fromList $ map itoi32 [i..100])
        items = map item [0..(size-1)]
        itemsv = Vector.fromList items 

Tout d'abord, vous avez beaucoup de données intermédiaires en cours de création en item i. En raison de paresse, ces petites et rapides pour calculer les vecteurs devient retardé bits de données, où nous avons pu l'avait tout de suite.

Avoir 2 soigneusement placé $!, qui représentent une évaluation rigoureuse :

 item i = Item (Just $! Vector.fromList $! map itoi32 [i..100])

Vous donnera une diminution de 25% au moment de l'exécution (pour la taille 1e5 et 1e6).

Mais il est de plus en plus problématique patron ici: vous générez une liste pour le convertir en un vecteur, en lieu et place de la construction du vecteur directement.

Regardez ces 2 dernières lignes, vous créez une liste -> carte une fonction -> transformer en un vecteur.

Ainsi, les vecteurs sont très similaires à la liste, vous pouvez faire quelque chose de similaire! Ainsi, vous aurez à générer un vecteur -> vecteur.carte et fait. Plus besoin de convertir une liste dans un vecteur, et cartographique sur le vecteur est généralement plus rapide qu'une liste!

De sorte que vous pouvez vous débarrasser de l' items et ré-écrire le suivant itemsv:

  itemsv = Vector.map item  $ Vector.enumFromN 0  (size-1)

La réapplication de la même logique d' item i, nous éliminons toutes les listes.

testfunc3 mtsize = itemsv
   where 
      size = i32toi $! fromJust mtsize
      item i = Item (Just $!  Vector.enumFromN (i::Int32) (100- (fromIntegral i)))
      itemsv = Vector.map item  $ Vector.enumFromN 0  (size-1)

Cela a une diminution de 50% pendant la période initiale d'exécution.

12voto

matthias krull Points 2459

Vous devriez jeter un coup d'œil aux méthodes de profilage Haskell pour trouver quelles ressources votre programme utilise / alloue et où.

Le chapitre sur le profilage dans le monde réel Haskell est un bon point de départ.

12voto

CoreyOConnor Points 151

C'est assez cohérent avec ce que user13251 dit: Le haskell mise en œuvre de l'épargne implique un grand nombre de petites lectures.

Par exemple: Dans Thirft.Le protocole.Binaire

readI32 p = do
    bs <- tReadAll (getTransport p) 4
    return $ Data.Binary.decode bs

Permet d'ignorer l'autre impair de bits et juste se concentrer sur cela pour le moment. Il dit: "pour lire un 32 bits int: lire 4 octets à partir de l'transports puis décoder ce paresseux bytestring."

La méthode de transport lit exactement 4 octets en utilisant le paresseux bytestring hGet. Le hGet va faire ce qui suit: allouer un tampon de 4 octets puis utilisez hGetBuf à remplir ce tampon. hGetBuf peut-être l'aide d'un tampon interne, dépend de la façon dont la Poignée a été initialisé.

Il y aurait peut-être certaines de mise en mémoire tampon. Même ainsi, cela signifie que l'Épargne pour haskell, est d'effectuer la lecture/décodage de cycle pour chaque entier individuellement. L'allocation d'une petite mémoire tampon à chaque fois. Ouch!

Je ne vois vraiment pas un moyen de résoudre ce problème sans la prudence de la bibliothèque en cours de modification à effectuer plus bytestring lit.

Puis, il y a d'autres bizarreries dans l'économie de la mise en œuvre: à l'Aide d'une des classes pour une structure de méthodes. Alors qu'elles se ressemblent et peuvent agir comme une structure de méthodes et sont mis en œuvre comme une structure de méthodes parfois: Ils ne devraient pas être traitées comme telles. Voir le "Existentielle Typeclass" antipattern:

Une étrange partie de la mise à l'essai:

  • générer un tableau d'Entiers seulement de changer immédiatement à Int32s pour aussitôt pack dans un Vecteur de Int32s. Générer le vecteur immédiatement serait suffisante et plus rapide.

Cependant, je soupçonne que ce n'est pas la source principale des problèmes de performance.

10voto

user13251 Points 131

Je ne vois aucune référence à la mémoire tampon dans le Haskell serveur. En C++, si vous n'avez pas de mémoire tampon, vous encourez un appel système pour chaque vecteur/élément de la liste. Je soupçonne la même chose se passe dans le Haskell serveur.

Je ne vois pas un tampon de transport en Haskell directement. Comme une expérience, vous pouvez changer à la fois le client et le serveur pour utiliser un encadré de transport. Haskell n'ont encadré de transport, et il est mis en mémoire tampon. Notez que cela va changer le fil de mise en page.

Comme une expérience distincte, vous souhaitez peut-être désactiver cette mise en mémoire tampon pour le C++ et de voir si les chiffres de performances sont comparables.

6voto

kvanberendonck Points 1254

L'implémentation Haskell du serveur d'épargne de base que vous utilisez utilise le threading en interne, mais vous ne l'avez pas compilée pour utiliser plusieurs cœurs.

Pour refaire le test en utilisant plusieurs cœurs, modifiez votre ligne de commande pour compiler le programme Haskell afin d'inclure -rtsopts et -threaded , puis exécutez le binaire final tel que ./Main -N4 & , où 4 est le nombre de cœurs à utiliser.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X