Haskellで書かれたサーバーとQt(C++)で書かれたクライアントの2つのコンポーネントを含むアプリケーションを構築しています。私はそれらを伝えるために倹約を使用しています、そしてなぜそれがとても遅いのか疑問に思います。
パフォーマンステストを行いました。これが私のマシンでの結果です
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がこのテストでとても遅いのはなぜですか?どうすればパフォーマンスを向上させることができますか?
ファイルは次のとおりです。
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)
}
{-# 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"
#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;
}
#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を使用して ここ ):
$ 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
を使用しましたが、それでも効果はありません
@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ミリ秒の時間を与え、2番目のバージョンは11698.7ミリ秒で、スピードアップはありません:(
問題を Thrift Jira に報告しました
これは完全に非科学的ですが、GHC7.8.3とThrift0.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
誰もが原因はスリフトライブラリであると指摘していますが、私はあなたのコードに焦点を当てます(そして私がある程度のスピードを上げるのを助けることができる場所)
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の場合)。
ただし、ここにはもっと問題のあるパターンがあります。ベクトルを直接作成する代わりに、リストを生成してベクトルとして変換します。
最後の2行を見て、リストを作成し、関数をマップし、ベクトルに変換します。
さて、ベクトルはリストに非常に似ています、あなたは似たようなことをすることができます!したがって、その上に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%減少します。
これは、user13251の発言とかなり一致しています。thriftのhaskell実装は、多数の小さな読み取りを意味します。
EG:Thirft.Protocol.Binaryで
readI32 p = do
bs <- tReadAll (getTransport p) 4
return $ Data.Binary.decode bs
他の奇数ビットを無視して、今はそれに焦点を合わせましょう。これは、「32ビット整数を読み取るには:トランスポートから4バイトを読み取り、この遅延バイト文字列をデコードします」と言います。
トランスポートメソッドは、遅延バイト文字列hGetを使用して正確に4バイトを読み取ります。 hGetは次のことを行います。4バイトのバッファーを割り当ててから、hGetBufを使用してこのバッファーを埋めます。 hGetBufは、ハンドルがどのように初期化されたかに応じて、内部バッファーを使用している可能性があります。
したがって、someバッファリングがある可能性があります。それでも、これは、HaskellのThriftが各整数の読み取り/デコードサイクルを個別に実行していることを意味します。毎回小さなメモリバッファを割り当てます。痛い!
より大きなバイト文字列の読み取りを実行するようにThriftライブラリを変更せずに、これを修正する方法は実際にはわかりません。
次に、thriftの実装には他にも奇妙な点があります。メソッドの構造にクラスを使用することです。それらは似ており、メソッドの構造のように機能し、メソッドの構造として実装されることもありますが、そのように扱われるべきではありません。 「ExistentialTypeclass」アンチパターンを参照してください。
テスト実装の奇妙な部分:
とはいえ、これはパフォーマンスの問題の主な原因ではないと思います。
Haskellのプロファイリング方法を調べて、プログラムが使用/割り当てているリソースと場所を見つける必要があります。
プロファイリング in Real World Haskell の章は良い出発点です。
Haskellサーバーにバッファリングへの参照がありません。 C++では、バッファリングしないと、ベクター/リスト要素ごとに1つのシステムコールが発生します。 Haskellサーバーでも同じことが起こっているのではないかと思います。
Haskellにバッファリングされたトランスポートが直接表示されません。実験として、フレーム化されたトランスポートを使用するようにクライアントとサーバーの両方を変更することをお勧めします。 Haskellにはフレーム化されたトランスポートがあり、バッファリングされています。これにより、ワイヤのレイアウトが変更されることに注意してください。
別の実験として、C++のバッファリングをオフにして、パフォーマンスの数値が同等かどうかを確認することをお勧めします。
使用している基本的なThriftサーバーのHaskell実装は、内部でスレッド化を使用していますが、複数のコアを使用するようにコンパイルしていません。
複数のコアを使用してテストを再度実行するには、Haskellプログラムをコンパイルするためのコマンドラインを変更して-rtsopts
と-threaded
を含め、./Main -N4 &
のような最終バイナリを実行します。4は使用するコア。