Я создаю приложение, которое содержит два компонента — сервер, написанный на 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 так медленно в этом тесте? Как я могу улучшить его производительность?
Вот файлы:
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 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"
#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;
}
#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 мс, а вторая — 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
Все указывают на то, что виновником является экономная библиотека, но я сосредоточусь на вашем коде (и где я могу помочь получить некоторую скорость)
Используя упрощенную версию вашего кода, где вы рассчитываете 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% меньше по сравнению с первоначальным временем выполнения.
Вы должны взглянуть на методы профилирования Haskell, чтобы найти, какие ресурсы использует и выделяет ваша программа и где.
Глава о профилирование в Реальный мир Haskell хорошая отправная точка.
Это вполне согласуется с тем, что говорит 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: использование классов для структуры методов. Хотя они выглядят одинаково и могут действовать как структура методов, а иногда даже реализуются как структура методов: их не следует рассматривать как таковые. См. «Шаблон экзистенциального типа»:
Одна странная часть реализации теста:
Хотя, я подозреваю, это не основной источник проблем с производительностью.
Я не вижу никаких ссылок на буферизацию на сервере Haskell. В C ++, если вы не буферизуете, вы выполняете один системный вызов для каждого элемента вектора / списка. Я подозреваю, что то же самое происходит на сервере Haskell.
Я не вижу буферизованного транспорта в Хаскеле напрямую. В качестве эксперимента вы можете захотеть изменить как клиента, так и сервер для использования транспорта в рамке. У Haskell есть каркасный транспорт, и он буферизируется. Обратите внимание, что это изменит расположение проводов.
В качестве отдельного эксперимента вы можете отключить -off-буферизацию для C ++ и посмотреть, сопоставимы ли показатели производительности.
Реализация Haskell базового Thrift-сервера, который вы используете, использует внутреннюю многопоточность, но вы не скомпилировали ее для использования нескольких ядер.
Чтобы снова выполнить тест с использованием нескольких ядер, измените командную строку для компиляции программы на Haskell, включив в нее -rtsopts
а также -threaded
, затем запустите последний двоичный файл, как ./Main -N4 &
где 4 — количество ядер для использования.