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

Posted

技术标签:

【中文标题】Haskell Thrift 库在性能测试中比 C++ 慢 300 倍【英文标题】:Haskell Thrift library 300x slower than C++ in performance test 【发布时间】:2013-10-31 00:47:43 【问题描述】:

我正在构建一个包含两个组件的应用程序 - 用 Haskell 编写的服务器和用 Qt (C++) 编写的客户端。我正在使用 Thrift 与他们交流,我想知道它为什么工作这么慢。

我做了一个性能测试,这是我机器上的结果

结果

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 在这个测试中这么慢?如何提高它的性能?

这里是文件:

文件

性能.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;

生成文件

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 可用here):

$ 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

运行 Haskell 测试:

$ ./Main& 
$ ./ItemStore_client

运行 C++ 测试:

$ ./ItemStore_server&
$ ./ItemStore_client

记得在每次测试后杀死服务器

更新

编辑getVector方法使用Vector.generate代替Vector.fromList,但仍然没有效果

更新 2

由于@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

这是严格的,并且与基于我的原始实现的替代方案相比,它改进了矢量生成:

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 ms,第二个是 11698.7 ms,没有加速:(

更新 3

我向Thrift Jira报告了一个问题

abhinav 更新 4

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

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

【问题讨论】:

那些宏 D: 只有 300 倍?回到那天我设法做指数减速。 ;) 我认为调用fromList 来创建向量可能会损失很多性能,而您可以使用Vector 的包函数之一来创建它们,例如generate。可能与地图相同 作为一个元评论:我认为所有“为什么我的 haskell 慢”问题都应该至少包括分析结果 如果 Haskell thrift 支持缓慢,我不会感到惊讶。当我有时间的时候,它就在我想研究的事情清单上。 【参考方案1】:

每个人都指出罪魁祸首是节俭库,但我将专注于您的代码(以及我可以帮助提高速度的地方)

使用简化版本的代码,计算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 中创建了许多中间数据。由于懒惰,那些小而快的计算向量变成了延迟的数据,而我们可以马上得到它们。

有2个精心放置的$!,代表严格的评估:

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

将使您的运行时间减少 25%(对于尺寸 1e5 和 1e6)。

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

看看最后两行,你创建了一个列表 -> 映射一个函数 -> 转换为一个向量。

好吧,vector 和 list 非常相似,你可以做类似的事情! 所以你必须在它上面生成一个vector -> vector.map 并完成。不再需要将列表转换为向量,并且在向量上映射通常比列表更快!

所以你可以去掉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)

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

【讨论】:

看看我更新的问题。虽然我认为主要问题是节俭协议的 Haskell 实现。看看getVectorping 方法——它们很简单,但比它们的C++ 实现慢得多。 @CoreyOConnor 是对的。由于简单的功能如此昂贵,但又如此简单,您必须在实际发送数据时付出代价。这也解释了为什么在更新到向量时没有得到加速,因为付出的代价取决于数据大小。您的代码几乎没有什么可做的。节俭需要小心谨慎。【参考方案2】:

您应该查看 Haskell 分析方法,以了解您的程序使用/分配了哪些资源以及在哪里。

Real World Haskell 中关于profiling 的章节是一个很好的起点。

【讨论】:

【参考方案3】:

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

EG:在 Thirft.Protocol.Binary 中

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

让我们忽略其他奇怪的部分,现在只关注它。这表示:“读取 32 位 int:从传输中读取 4 个字节,然后解码这个惰性字节串。”

传输方法使用惰性字节串 hGet 准确读取 4 个字节。 hGet 将执行以下操作:分配一个 4 字节的缓冲区,然后使用 hGetBuf 填充此缓冲区。 hGetBuf 可能正在使用内部缓冲区,这取决于 Handle 的初始化方式。

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

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

thrift 实现中还有其他奇怪的地方:使用类作为方法结构。虽然它们看起来相似并且可以像方法结构一样起作用,有时甚至可以作为方法结构实现:它们不应该被这样对待。参见“Existential Typeclass”反模式:

http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-typeclass/

测试实现的一个奇怪部分:

生成一个 Int 数组只是为了立即将它们更改为 Int32s 只是为了立即打包到 Int32s 的向量中。立即生成向量就足够了,而且速度更快。

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

【讨论】:

【参考方案4】:

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

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

作为一个单独的实验,您可能想要关闭 C++ 的缓冲并查看性能数据是否具有可比性。

【讨论】:

如何在 C++ 中关闭它?这是编译标志还是实现更改? 在 C++ 中关闭缓冲是一种实现更改。如果您使用 BufferedTransport、FramedTransport 或其关联的工厂,则您已启用缓冲。如果你不使用任何一个,那么你就没有缓冲。【参考方案5】:

您使用的基本 thrift 服务器的 Haskell 实现在内部使用线程,但您没有将其编译为使用多个内核。

要使用多核再次进行测试,请将编译 Haskell 程序的命令行更改为包含 -rtsopts-threaded,然后运行最终的二进制文件,如 ./Main -N4 &amp;,其中 4 是要使用的核数.

【讨论】:

我认为这与这个问题无关。如果 C++ 和 Haskell(都运行单核)在同一台机器上的速度下降如此之大,那么这不是解决这个问题的方法 我已经多次看到当-threaded 被省略时这样的减速,无论我是否使用多个内核运行。我已经开始为rtsSupportsBoundThreads 添加运行时测试,以查看我是否使用-threaded 编译。奇怪的是,减速非常依赖于平台。我已经在 Windows 和某些版本的 Ubuntu 中看到过它。可能与调度器有关。 这根本不是重点。 C++ 服务器(如果它使用线程..)使用操作系统线程,这将在所有内核上可用,而如果在没有 -threaded 的情况下编译,Haskell 使用的轻量级线程将被限制为单个内核。

以上是关于Haskell Thrift 库在性能测试中比 C++ 慢 300 倍的主要内容,如果未能解决你的问题,请参考以下文章

性能工具之Jmeter压测Thrift RPC服务

python thrift 服务端与客户端使用

thrift

Thrift介绍

Thrift远程过程调用

Thrift架构介绍