当我在多线程中解析Google协议缓冲区的消息时,为什么会这么慢?

时间:2015-08-03 12:57:02

标签: multithreading protocol-buffers

我尝试从通过调用SerializeToString生成的二进制文件中解析许多Google协议缓冲区消息。我首先通过调用new函数将所有Bytes加载到堆内存中。我还有两个数组来存储堆内存中消息的字节开始地址和消息的字节数。 然后我开始通过调用ParseFromString来解析消息。我希望通过使用多线程来加快该过程。 在每个线程中,我传递地址数组的起始索引和结束索引以及字节计数数组。

在父进程中。主要代码是:

struct ParsePara
{
    char* str_buffer;
    size_t* buffer_offset;
    size_t* binary_string_length_array;
    size_t start_idx;
    size_t end_idx;
    Flight_Ticket_Info* ticket_info_buffer_array;
};

//Flight_Ticket_Info is class of message
//offset_size is the count of message
ticket_array = new Flight_Ticket_Info[offset_size];
const int max_thread_count = 6;
pthread_t pthread_id_vec[max_thread_count];

CTimer thread_cost;
thread_cost.start();

vector<ParsePara*> para_vec;
const size_t each_count = ceil(float(offset_size) / max_thread_count);
for (size_t k = 0;k < max_thread_count;k++)
{   
    size_t start_idx = each_count * k;
    size_t end_idx = each_count * (k+1);

    if (start_idx >= offset_size)
        break;

    if (end_idx >= offset_size)
        end_idx = offset_size;

    ParsePara* cand_para_ptr = new ParsePara();

    if (!cand_para_ptr)
    {   
        _ERROR_EXIT(0,"[Malloc memory fail.]");
    }   

    cand_para_ptr->str_buffer = m_valdata;//heap memory for storing Bytes of message
    cand_para_ptr->buffer_offset = offset_array;//begin address of each message
    cand_para_ptr->start_idx = start_idx;
    cand_para_ptr->end_idx = end_idx;
    cand_para_ptr->ticket_info_buffer_array = ticket_array;//array to store message
    cand_para_ptr->binary_string_length_array = binary_length_array;//Bytes count of each message

    para_vec.push_back(cand_para_ptr);
}   

for(size_t k = 0 ;k < para_vec.size();k++)
{
    int ret = pthread_create(&pthread_id_vec[k],NULL,parserFlightTicketForMultiThread,para_vec[k]);

    if (0 != ret)
    {
        _ERROR_EXIT(0,"[Error] [create thread fail]");
    }
}

for (size_t k = 0;k < para_vec.size();k++)
{
    pthread_join(pthread_id_vec[k],NULL);
}

在每个线程中,线程函数是:

    void* parserFlightTicketForMultiThread(void* void_para_ptr)
{
    ParsePara* para_ptr = (ParsePara*) void_para_ptr;

    parserFlightTicketForMany(para_ptr->str_buffer,para_ptr->ticket_info_buffer_array,para_ptr->buffer_offset,
            para_ptr->start_idx,para_ptr->end_idx,para_ptr->binary_string_length_array);
}

void parserFlightTicketForMany(const char* str_buffer,Flight_Ticket_Info* ticket_info_buffer_array,
        size_t* buffer_offset,const size_t start_idx,const size_t end_idx,size_t* binary_string_length_array)
{
    printf("start_idx:%d,end_idx:%d\n",start_idx,end_idx);
    for (size_t k = start_idx;k < end_idx;k++)
    {
        if (k % 100000 == 0)
            cout << k << endl;

        size_t cand_offset = buffer_offset[k];
        size_t binary_length = binary_string_length_array[k];
    ticket_info_buffer_array[k].ParseFromString(string(&str_buffer[cand_offset],binary_length-1));
    }
    printf("done %ld %ld\n",start_idx,end_idx);
}

但是多线程成本不止一个线程。 一个线程的成本是:40455623ms 我的电脑是8芯,六线程成本是:131586865ms

任何人都可以帮助我吗?谢谢!

1 个答案:

答案 0 :(得分:1)

一些可能的问题 - 您必须尝试确定哪个:

  • Protobuf解析速度通常受内存带宽而非CPU时间的限制,特别是对于大输入数据集。在这种情况下,由于所有内核都与主内存共享带宽,因此更多线程无法提供帮助。实际上,让多个核心争夺内存带宽可能会使整体操作变慢。请注意,内存的最大消费者不是输入字节,而是解析的数据对象 - 即解析的输出 - 比数据库大很多倍。编码数据。要改进此问题,请考虑编写解析循环,以便在解析之后立即对每个消息进行完全处理,然后再转到文本消息。这样,您不需要分配k个protobuf对象,而只需要为每个线程分配一个protobuf对象,并重复使用相同的对象进行解析。这样,对象(可能)将保留在核心的私有L1缓存中,并避免消耗内存带宽;只有输入字节将通过主总线读取。
  • 如何将数据加载到RAM中?您是read()成了一个大型数组还是mmap()?在后一种情况下,数据是懒惰地从磁盘中读取的 - 在您实际尝试解析它之前它不会发生。即使在read()情况下,也可能是数据已被换出,产生类似的效果。无论哪种方式,您的线程现在不只是争取内存带宽,而是磁盘带宽,当然 更慢。有六个线程读取大文件的不同部分肯定会比一个线程读取整个文件更慢,因为操作系统优化顺序访问。
  • Protobuf在解析期间分配内存。许多内存分配器在分配新内存时会锁定。由于你所有的线程都在一个紧密的循环中分配吨和吨的对象,他们将争夺这个锁。确保使用的是线程友好的内存分配器,例如Google的tcmalloc。请注意,在parse-consume循环中重复使用相同的protobuf对象而不是分配大量不同的对象在这里也会有很大的帮助,因为protobuf对象会自动重用子对象的内存。
  • 您的代码中可能存在错误,并且在多线程时可能没有按照您的预期执行操作。例如,错误可能导致所有线程处理相同的数据而不是不同的数据,并且可能是他们选择的数据恰好更大。当您运行单线程与多线程时,请确保测试代码的结果完全相同。

简而言之,如果您希望多个内核能够使代码更快,那么您不仅要考虑每个内核正在做什么,还要考虑每个内核的数据进出,以及内核需要多少内容对彼此。理想情况下,您希望每个核心都可以独立运行而无需与任何人或任何人交谈;然后你获得最大的并行度。当然,这通常是不可能的,但是你越接近那就越好。

BTW,随机优化:

ParseFromString(string(&str_buffer[cand_offset],binary_length-1))

将其替换为:

ParseFromArray(&str_buffer[cand_offset],binary_length-1)

std::string创建会复制数据,从而浪费时间(和内存带宽)。 (但这并不能解释为什么线程很慢。)