β

JsonCpp使用优化(二)

Untitled RSS Feed 2764 阅读

经过上次的优化之后,我发现在服务器端json格式的性能还是太差了。我们服务一般的latency也就30ms,json格式化就要用10ms+,难于忍受。使用下面这段代码跟踪性能问题。

void test4()
{
    int doc_count = 40; 
    int outer_field_count = 80; 

    Json::Value common_info;

    int64_t start_time = getCurrentTime();

    Json::Value auc_info;

    for(size_t i=0; i<doc_count; ++i)
    {   
        for( size_t j=0 ; j<outer_field_count; ++j )
        {   
            auc_info[j].setValue(Json::StaticString(str));
        }   
        common_info.append(auc_info);
    }   

    if (! common_info.isNull())
    {   
        Json::Value auction_info;
        auction_info["auction_info"] = common_info;

        Json::Value res;
        res["main"] = auction_info;
    }   

    int64_t end_time = getCurrentTime();

    cout << "setValue time: " << end_time - start_time << endl;
}

这个json输出有三层,res/auction_info/common_info,使用ltrace跟踪了一下程序的运行,发现有大量的malloc、memcpy和free,如下所示。

:13:36.437544 [0x406d5f]     strlen("abcdefghijklmnopqrstuvwxyz") = 26 <0.000017>
:13:36.437698 [0x406d2b]     malloc(27)        = 0x562a9c0 <0.000016>
:13:36.437799 [0x406d3c]     memcpy(0x562a9c0, "abcdefghijklmnopqrstuvwxyz", 26) = 0x562a9c0 <0.000016>

在阅读了一下jsoncpp的代码之后,我发现这个拷贝是在json_value.cpp下面这个函数做的。这个地方居然没有根据allocated_的情况来决定是否做拷贝。如果allocated_=false的话,显然是不需要做拷贝的。

Value::Value( const Value &other )
   : type_( other.type_ )
   , comments_( 0 )
# ifdef JSON_VALUE_USE_INTERNAL_MAP
   , itemIsUsed_( 0 )
#endif
{
   switch ( type_ )
   {
   case nullValue:
   case intValue:
   case uintValue:
   case realValue:
   case booleanValue:
      value_ = other.value_;
      break;
   case stringValue:
      if ( other.value_.string_ )
      {    
         value_.string_ = valueAllocator()->duplicateStringValue( other.value_.string_ );
         allocated_ = true;
      }    
      else 
         value_.string_ = 0; 
      break;

于是我把代码稍微改了一下,如果allocated_=false,那么就不拷贝了。

Value::Value( const Value &other )
   : type_( other.type_ )
   , comments_( 0 )
# ifdef JSON_VALUE_USE_INTERNAL_MAP
   , itemIsUsed_( 0 )
#endif
{
   switch ( type_ )
   {
   case nullValue:
   case intValue:
   case uintValue:
   case realValue:
   case booleanValue:
      value_ = other.value_;
      break;
   case stringValue:
      if ( other.allocated_ )
      {    
          if ( other.value_.string_ )
          {    
             value_.string_ = valueAllocator()->duplicateStringValue( other.value_.string_ );
             allocated_ = true;
          }    
          else 
             value_.string_ = 0; 
      }else
      {    
            value_.string_ = other.value_.string_;
            allocated_ = false;
      }    

      break;

测试一下性能,结果如下。测试环境性能提升很明显。线上高压力的环境下,延时能从12ms降低到10ms,只是略有提升而已。

old version: 2729
new version: 1585

JsonCpp现在主要的瓶颈是下面两个原因导致的。JsonCpp作为一个输出格式,在构造和解释两个环节,性能都是问题。它之所以能蓬勃发展起来,主要是因为自描述、个头小、解释简单这几个优点吧。

1、构造和析构大量的Value对象,使用ltrace可以看到大量的operator new和operator delete。上面这个例子new和delete加起来有19875次,如果平均每次需要10ns,那么加起来就是200us。这个问题需要做一个对象池来避免。

2、FastWriter/StyledWriter的write方法都是很耗时的,时间主要消耗在两个地方,第一个是string的append,40*80=3200个Value,就需要调用1.2W+次append(每个value要做一次append,前后还得加上一个双引号,然后把它们加到document_里面)。如果每次append耗时在20ns,那么加起来就是240us左右。第二个是对特殊字符进行处理,会调用strpbrk对每个字符串进行检查,看看是不是包含了特殊字符,这个也需要调用3200次。

从ltrace跟踪的结果可以看到排在前面两位的分别是operator new和append。

 setValue time: 2838771
 % time     seconds  usecs/call     calls      function
 ------ ----------- ----------- --------- --------------------
.41    2.144498         215      9938 operator new(unsigned long)
.90    0.179447          13     12894 std::string::append(char const*, unsigned long)
.43    0.141332          14      9938 operator delete(void*)
.71    0.044593          13      3202 strpbrk
.71    0.044567          13      3204 std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char     const*, std::allocator<char> const&)
.68    0.043697          13      3209 strlen

JsonCpp有些接口设计得有问题的,比如这个valueToQuotedString,应该把document_当做引用传递进去,而不是传一个std::string出去,外面还得继续append这个字符串。我测试了一下,仅修改这个接口就能减少1ms的开销。

case stringValue:
      document_ += valueToQuotedString( value.asCString() );

把valueToQuotedString接口改成下面这样,可以兼容之前旧的使用方式。

std::string JSON_API valueToQuotedString( const char *value, std::string* document = NULL );

另外一个比较消耗性能的接口是FastWriter::writeValue。它在处理map类型时性能很差,时间主要消耗在两个地方,第一个是构造members这个std::map,涉及到大量的拷贝;另外一个是value[name],要在map中定位key。

case objectValue:
  {
     Value::Members members( value.getMemberNames() );
     document_ += "{";
     for ( Value::Members::iterator it = members.begin(); 
           it != members.end(); 
           ++it )
     {
        const std::string &name = *it;
        if ( it != members.begin() )
           document_ += ",";
        document_ += valueToQuotedString( name.c_str() );
        document_ += yamlCompatiblityEnabled_ ? ": " 
                                              : ":";
        writeValue( value[name] );
     }
     document_ += "}";
  }
  break;
}

我把这个接口稍微修改了一下。

case objectValue:
  {
     document_ += "{";

     Value::ValueMap& value_map = value.getValueMap();

     Value::ValueMap::iterator it = value_map.begin();

     for ( Value::ValueMap::iterator it = value_map.begin(); 
           it != value_map.end();
           ++it )
     {
        if ( it != value_map.begin() )
           document_ += ",";
        const char* name = it->first.c_str();

        valueToQuotedString( name, &document_ );
        document_ += yamlCompatiblityEnabled_ ? ": " 
                                              : ":";
        writeValue( it->second );
     }

     document_ += "}";
  }
  break;
}
作者:Untitled RSS Feed
Untitled RSS Feed
原文地址:JsonCpp使用优化(二), 感谢原作者分享。