Подтвердить что ты не робот

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

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

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

Результаты

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 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 [email protected]

ItemStore_server: ItemStore_server.o $(GEN_OBJ) 
    $(CXX) $^ -o [email protected] -L/usr/local/lib -lthrift -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H

ItemStore_client: ItemStore_client.o $(GEN_OBJ)
    $(CXX) $^ -o [email protected] -L/usr/local/lib -lthrift -DHAVE_INTTYPES_H -DHAVE_NETINET_IN_H

clean:
    $(RM) *.o ItemStore_server ItemStore_client

Скомпилировать и запустить

Я сгенерировал файлы (используя экономный 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

Выполнить тест С++:

$ ./ItemStore_server&
$ ./ItemStore_client

Не забудьте убить сервер после каждого теста

Update

Отредактированный способ 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 мс, а вторая - 11698,7 мс, без ускорения: (

Обновление 3

Я сообщил о проблеме Thrift Jira

Обновление 4 с помощью abhinav

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

Ответ 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.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% по сравнению с начальным временем выполнения.

Ответ 2

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

Глава profiling в Real World Haskell является хорошей отправной точкой.

Ответ 3

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

EG: В Thirft.Protocol.Binary

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

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

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

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

Я действительно не вижу способа исправить это, если библиотека Thrift не будет изменена, чтобы выполнять более крупные байтовые сообщения.

Тогда в реализации бережливости есть другие странности: использование классов для структуры методов. Хотя они выглядят одинаково и могут действовать как структура методов и даже реализуются как структура методов: они не должны рассматриваться как таковые. См. "Неисправность экзистенциального типа":

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

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

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

Ответ 4

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

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

В качестве отдельного эксперимента вы можете захотеть повернуть -off-buffering для С++ и посмотреть, сопоставимы ли показатели производительности.

Ответ 5

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

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