Библиотека Haskell Thrift в 300 раз медленнее, чем C ++, в тесте производительности

Я создаю приложение, которое содержит два компонента — сервер, написанный на Haskell, и клиент, написанный на Qt (C ++). Я использую бережливость, чтобы общаться с ними, и мне интересно, почему это работает так медленно.

Я сделал тест производительности, и вот результат на моей машине

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 так медленно в этом тесте? Как я могу улучшить его производительность?

Вот файлы:

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 ItemStorei32toi :: 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_ENDlong 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_ENDcout << "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

Запустите тест 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 мс, а вторая — 11698,7 мс без ускорения 🙁

Я сообщил о проблеме Бережливость джира

Это совершенно ненаучно, но с использованием 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’s 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

42

Решение

Все указывают на то, что виновником является экономная библиотека, но я сосредоточусь на вашем коде (и где я могу помочь получить некоторую скорость)

Используя упрощенную версию вашего кода, где вы рассчитываете 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.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% меньше по сравнению с первоначальным временем выполнения.

27

Другие решения

Вы должны взглянуть на методы профилирования Haskell, чтобы найти, какие ресурсы использует и выделяет ваша программа и где.

Глава о профилирование в Реальный мир Haskell хорошая отправная точка.

12

Это вполне согласуется с тем, что говорит user13251: реализация thrift на haskell подразумевает большое количество небольших операций чтения.

Например: в Thirft.Protocol.Binary

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

Давайте проигнорируем другие нечетные биты и сосредоточимся только на этом. Это говорит: «прочитать 32-битное целое число: прочитать 4 байта из транспорта, а затем декодировать эту ленивую строку теста».

Транспортный метод считывает ровно 4 байта, используя ленивый байт-строку hGet. HGet сделает следующее: выделит буфер в 4 байта, затем использует hGetBuf для заполнения этого буфера. hGetBuf может использовать внутренний буфер, в зависимости от того, как был инициализирован дескриптор.

Так что может быть немного буферизация. Тем не менее, это означает, что Thrift для haskell выполняет цикл чтения / декодирования для каждого целого числа отдельно. Выделение небольшого буфера памяти каждый раз. Ой!

На самом деле я не вижу способа исправить это без модификации библиотеки Thrift для выполнения больших чтений из строки байтов.

Тогда есть другие странности в реализации Thrift: использование классов для структуры методов. Хотя они выглядят одинаково и могут действовать как структура методов, а иногда даже реализуются как структура методов: их не следует рассматривать как таковые. См. «Шаблон экзистенциального типа»:

Одна странная часть реализации теста:

  • генерирование массива Ints только для немедленного изменения их на Int32 только для немедленной упаковки в вектор Int32. Генерирование вектора немедленно будет достаточно и быстрее.

Хотя, я подозреваю, это не основной источник проблем с производительностью.

12

Я не вижу никаких ссылок на буферизацию на сервере Haskell. В C ++, если вы не буферизуете, вы выполняете один системный вызов для каждого элемента вектора / списка. Я подозреваю, что то же самое происходит на сервере Haskell.

Я не вижу буферизованного транспорта в Хаскеле напрямую. В качестве эксперимента вы можете захотеть изменить как клиента, так и сервер для использования транспорта в рамке. У Haskell есть каркасный транспорт, и он буферизируется. Обратите внимание, что это изменит расположение проводов.

В качестве отдельного эксперимента вы можете отключить -off-буферизацию для C ++ и посмотреть, сопоставимы ли показатели производительности.

10

Реализация Haskell базового Thrift-сервера, который вы используете, использует внутреннюю многопоточность, но вы не скомпилировали ее для использования нескольких ядер.

Чтобы снова выполнить тест с использованием нескольких ядер, измените командную строку для компиляции программы на Haskell, включив в нее -rtsopts а также -threaded, затем запустите последний двоичный файл, как ./Main -N4 &где 4 — количество ядер для использования.

6