Haskell Thrift库在性能testing中比C ++慢300倍

我正在构build一个包含两个组件的应用程序 – 用Haskell编写的服务器和用Qt(C ++)编写的客户端。 我正在用节俭的方式来沟通他们,我想知道为什么这么慢。

我做了性能testing,这是我的机器上的结果

结果

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 

为什么Haskell在这个testing中如此缓慢? 我怎样才能改善它的performance?

这里是文件:

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 

编译并运行

我生成文件(使用thrift 0.9 在这里可用)与:

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

编译

 $ 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 

运行Haskelltesting:

 $ ./Main& $ ./ItemStore_client 

运行C ++testing:

 $ ./ItemStore_server& $ ./ItemStore_client 

记住在每次testing后都要杀死服务器

更新

编辑的getVector方法使用Vector.generate而不是Vector.fromList ,但仍然没有效果

更新2

由于@MdxBhmt的build议,我testing了getItems函数,如下所示:

 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 

这是严格的,已经改进了vector代与基于我原来的实现的替代:

 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 

注意没有发送HashMap。 第一个版本给出时间12338.2毫秒,第二个是11698.7毫秒,没有加速:(

更新3

我向Thrift Jira报告了一个问题

由abhinav更新4

这是完全不科学的,但是使用GHC 7.8.3与Thrift 0.9.2和@MdxBhmt版本的getItems ,差异显着减less。

 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 

执行多次执行,每次都重新启动服务器。 结果是可重现的。

请注意,原始问题的源代码(使用@ MdxBhmt的getItems实现)将不会按原样编译。 必须做出以下更改:

 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 

大家都在指出,罪魁祸首就是节俭图书馆,但我会专注于你的代码(在哪里我可以帮助获得一些速度)

使用代码的简化版本,计算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 

首先,您在item i中创build了许多中间数据。 由于懒惰,那些小而快的计算向量就会变成延迟的数据,当我们马上拥有它们的时候。

有2仔细放置$! 代表严格的评估:

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

运行时会减less25%(对于1e5和1e6)。

但是这里存在一个更有问题的模式:您生成一个列表,将其转换为一个向量,而不是直接构build向量。

看看最后2行,你创build一个列表 – >映射一个函数 – >变换成一个向量。

那么,载体是非常相似的列表,你可以做类似的事情! 所以你必须生成一个vector – > vector.map来完成。 不再需要将列表转换为vector,并且在vector上映射通常比列表快!

所以你可以摆脱items并重新编写以下itemsv

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

重新应用相同的逻辑item i ,我们消除所有名单。

 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) 

这比初始运行时减less了50%。

你应该看看Haskell分析方法来find你的程序使用/分配的资源和位置。

在真实世界Haskell 分析的一章是一个很好的起点。

这与user13251所说的相当一致:节点的haskell实现意味着大量的小读取。

EG:在Thirft.Protocol.Binary

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

让我们忽略其他一些奇怪的东西,现在就关注这个。 这就是说:“读取32位int:从传输中读取4个字节,然后解码这个懒惰的字节串”。

transport方法使用lazy bytestring hGet读取正好4个字节。 hGet将执行以下操作:分配4个字节的缓冲区,然后使用hGetBuf填充此缓冲区。 hGetBuf可能使用内部缓冲区,取决于Handle是如何初始化的。

所以可能会有一些缓冲。 即使如此,这意味着Thrift for Haskell将为每个整数单独执行读取/解码周期。 每次分配一个小内存缓冲区。 哎哟!

如果没有修改Thrift库来执行更大的字节串读取,我真的没有办法解决这个问题。

然后在节俭实现中还有其他一些古怪的东西:使用类来构造方法。 虽然它们看起来相似,可以像一个方法结构,甚至有时被实施为一种方法的结构:它们不应该被当作这样的对待。 参见“存在型Typeclass”反模式:

testing实施的一个奇怪的部分:

  • 生成一个Ints数组只是立即将它们更改为Int32s只是立即打包到Int32s的向量。 立即生成vector将是足够和快速的。

不过,我怀疑这不是性能问题的主要来源。

我没有看到在Haskell服务器缓冲的任何参考。 在C ++中,如果您不缓冲,则会为每个向量/列表元素引发一次系统调用。 我怀疑在Haskell服务器中发生了同样的事情。

我没有直接在Haskell中看到缓冲的传输。 作为一个实验,您可能想要更改客户端和服务器以使用帧传输。 Haskell确实有一个框架的运输,它被缓冲。 请注意,这将改变导线布局。

作为一个单独的实验,您可能需要closuresC ++缓冲区,并查看性能数据是否可比。

您所使用的基本的节俭服务器的Haskell实现在内部使用线程,但是您没有编译它以使用多个内核。

要使用多个内核再次执行testing,请更改用于编译Haskell程序的命令行,以包含-rtsopts-rtsopts ,然后运行最终二进制文件,如./Main -N4 & ,其中4是要使用的内核数。