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.