c++ performance networking haskell thrift

La biblioteca Haskell Thrift 300 veces más lenta que C++ en la prueba de rendimiento



performance networking (5)

Estoy construyendo una aplicación que contiene dos componentes: el servidor escrito en Haskell y el cliente escrito en Qt (C ++). Estoy usando el ahorro para comunicarlos, y me pregunto por qué funciona tan lento.

Hice una prueba de rendimiento y aquí está el resultado en mi máquina

Resultados

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

¿Por qué Haskell es tan lento en esta prueba? ¿Cómo puedo mejorar su rendimiento?

Aquí están los archivos:

Archivos

performance.thrift

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) }

Main.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

Compilar y ejecutar

Genero archivos (usando thrift 0.9 disponible here ) con:

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

Compilar con

$ 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

Ejecute la prueba de Haskell:

$ ./Main& $ ./ItemStore_client

Ejecute la prueba de C ++:

$ ./ItemStore_server& $ ./ItemStore_client

Recuerde matar el servidor después de cada prueba

Actualizar

Método getVector editado para usar Vector.generate lugar de Vector.fromList , pero todavía no tiene efecto

Actualización 2

Debido a la sugerencia de @MdxBhmt, probé la función getItems siguiente manera:

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

que es estricto y ha mejorado la generación de vectores frente a su alternativa basada en mi implementación original:

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

Tenga en cuenta que no se envía HashMap. La primera versión da tiempo 12338.2 ms y la segunda es 11698.7 ms, sin aceleración :(

Actualización 3

Informé un problema a Thrift Jira

Actualización 4 por abhinav

Esto no es completamente científico, pero al usar GHC 7.8.3 con Thrift 0.9.2 y la versión de getItems de getItems , la discrepancia se reduce significativamente.

C++ server and C++ client: Sending 100 pings: 8.56 ms Transferring 1000000 size vector: 137.97 ms Recieved: 3906.25 kB Transferring 100000 items from server: 467.78 ms Transferring 100000 items to server: 207.59 ms Haskell server and C++ client: Sending 100 pings: 24.95 ms Recieved: 3906.25 kB Transferring 1000000 size vector: 378.60 ms Transferring 100000 items from server: 233.74 ms Transferring 100000 items to server: 913.07 ms

Se realizaron varias ejecuciones, reiniciando el servidor cada vez. Los resultados son reproducibles.

Tenga en cuenta que el código fuente de la pregunta original (con la implementación getItems de getItems ) no se compilará tal cual. Los siguientes cambios deberán realizarse:

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 getVector _ mtsize = do putStrLn "getVector" let size = i32toi $ fromJust mtsize return $ Vector.generate size itoi32


Debería echar un vistazo a los métodos de creación de perfiles de Haskell para encontrar qué recursos utiliza / asigna su programa y dónde.

El capítulo sobre profiling en Real World Haskell es un buen punto de partida.


Esto es bastante consistente con lo que dice el usuario13251: la implementación de ahorro de haskell implica una gran cantidad de lecturas pequeñas.

EG: En Thirft.Protocol.Binary

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

Vamos a ignorar los otros bits impares y solo enfocarnos en eso por ahora. Esto dice: "leer un int de 32 bits: leer 4 bytes del transporte y luego decodificar esta cadena de bytes perezosa".

El método de transporte lee exactamente 4 bytes utilizando lazy bytestring hGet. El hGet hará lo siguiente: asignar un búfer de 4 bytes y luego usar hGetBuf para llenar este búfer. hGetBuf podría estar usando un buffer interno, depende de cómo se inicializó el Handle.

Entonces podría haber algo de buffering. Aun así, esto significa Thrift for haskell está realizando el ciclo de lectura / decodificación para cada entero individualmente. Asignación de un pequeño búfer de memoria cada vez. ¡Ay!

Realmente no veo una manera de arreglar esto sin que la biblioteca Thrift se modifique para realizar lecturas de byteslas mayores.

Luego están las otras rarezas en la implementación del ahorro: Usar una clase para una estructura de métodos. Si bien se ven similares y pueden actuar como una estructura de métodos, e incluso se implementan como una estructura de métodos a veces: no deben tratarse como tales. Ver el antipatrón "Existencial Typeclass":

Una parte extraña de la implementación de la prueba:

  • generando una matriz de Ints solo para cambiarlos inmediatamente a Int32s solo para empacar inmediatamente en un Vector de Int32s. Generar el vector de inmediato sería suficiente y más rápido.

Aunque, sospecho, esta no es la principal fuente de problemas de rendimiento.


La implementación de Haskell del servidor de ahorro básico que está utilizando utiliza el enrutamiento interno, pero no compiló para usar varios núcleos.

Para volver a hacer la prueba usando múltiples núcleos, cambie su línea de comando para compilar el programa Haskell para incluir -rtsopts y -threaded , luego ejecute el binario final como ./Main -N4 & , donde 4 es la cantidad de núcleos a usar.


No veo ninguna referencia al almacenamiento en búfer en el servidor Haskell. En C ++, si no realiza un búfer, incurre en una llamada al sistema por cada vector / elemento de lista. Sospecho que lo mismo está sucediendo en el servidor Haskell.

No veo un transporte en búfer en Haskell directamente. Como experimento, es posible que desee cambiar tanto el cliente como el servidor para usar un transporte enmarcado. Haskell tiene un transporte enmarcado y está amortiguado. Tenga en cuenta que esto cambiará el diseño del cable.

Como un experimento separado, es posible que desee desactivar-buffering para C ++ y ver si los números de rendimiento son comparables.


Todos señalan que el culpable es la biblioteca de ahorro, pero me centraré en tu código (y donde puedo ayudar a conseguir algo de velocidad)

Usando una versión simplificada de tu código, donde puedes calcular 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

En primer lugar, tiene muchos datos intermedios creados en el item i . Debido a la pereza, aquellos pequeños y rápidos para calcular vectores se retrasan en trozos de datos, cuando podíamos tenerlos de inmediato.

¡Con 2 cuidadosamente colocados $! , que representan una evaluación estricta:

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

Le dará una disminución del 25% en el tiempo de ejecución (para el tamaño 1e5 y 1e6).

Pero hay un patrón más problemático aquí: generas una lista para convertirlo como un vector, en lugar de construir el vector directamente.

Mira esas 2 últimas líneas, creas una lista -> mapea una función -> transforma en un vector.

Bueno, los vectores son muy similares a la lista, ¡puedes hacer algo similar! Entonces tendrás que generar un vector -> vector.map sobre él y listo. Ya no es necesario convertir una lista en un vector, ¡y el mapeo en vectores suele ser más rápido que una lista!

Para que pueda deshacerse de los items y volver a escribir los siguientes itemsv :

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

Al volver a aplicar la misma lógica al item i , eliminamos todas las listas.

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)

Esto tiene una disminución del 50% sobre el tiempo de ejecución inicial.