图4显示的是优化版本与基线版本的CPU IDLE对比情况。可以看到优化版本的CPU IDLE反而更高,CPU占用变少了。一个合理的解释是:当protobuf的messge数据量非常大时,其clear操作消耗的CPU比小message的析构和构造消耗的总的CPU还要多。
下面是Clear操作的代码。
void ReflectionOps::Clear(Message* message) { const Reflection* reflection = message->GetReflection(); vectorfields; reflection->ListFields(*message, &fields); for (int i = 0; i < fields.size(); i++) { reflection->ClearField(message, fields[i]); } reflection->MutableUnknownFields(message)->Clear();} //ClearField函数的实现void GeneratedMessageReflection::ClearField( Message* message, const FieldDescriptor* field) const { USAGE_CHECK_MESSAGE_TYPE(ClearField); if (field->is_extension()) { MutableExtensionSet(message)->ClearExtension(field->number()); } else if (!field->is_repeated()) { // 如果不是数组,也就是基础类型 if (HasBit(*message, field)) { ClearBit(message, field); // We need to set the field back to its default value. switch (field->cpp_type()) {#define CLEAR_TYPE(CPPTYPE, TYPE) case FieldDescriptor::CPPTYPE_##CPPTYPE: *MutableRaw (message, field) = field->default_value_##TYPE(); break; CLEAR_TYPE(INT32 , int32 ); // 对基础类型设置为默认值 CLEAR_TYPE(INT64 , int64 ); CLEAR_TYPE(UINT32, uint32); CLEAR_TYPE(UINT64, uint64); CLEAR_TYPE(FLOAT , float ); CLEAR_TYPE(DOUBLE, double); CLEAR_TYPE(BOOL , bool );#undef CLEAR_TYPE case FieldDescriptor::CPPTYPE_ENUM: // 处理枚举类型 *MutableRaw (message, field) = field->default_value_enum()->number(); break; case FieldDescriptor::CPPTYPE_STRING: { switch (field->options().ctype()) { default: // TODO(kenton): Support other string reps. case FieldOptions::STRING: const string* default_ptr = DefaultRaw (field); string** value = MutableRaw (message, field); if (*value != default_ptr) { if (field->has_default_value()) { // 如果有默认值,则设置为默认值 (*value)->assign(field->default_value_string()); } else { (*value)->clear(); // 否则设置清理数据 } } break; } break; } case FieldDescriptor::CPPTYPE_MESSAGE: (*MutableRaw (message, field))->Clear(); break; } } } else { switch (field->cpp_type()) {#define HANDLE_TYPE(UPPERCASE, LOWERCASE) case FieldDescriptor::CPPTYPE_##UPPERCASE : MutableRaw >(message, field)->Clear(); break HANDLE_TYPE( INT32, int32); HANDLE_TYPE( INT64, int64); HANDLE_TYPE(UINT32, uint32); HANDLE_TYPE(UINT64, uint64); HANDLE_TYPE(DOUBLE, double); HANDLE_TYPE( FLOAT, float); HANDLE_TYPE( BOOL, bool); HANDLE_TYPE( ENUM, int);#undef HANDLE_TYPE case FieldDescriptor::CPPTYPE_STRING: { switch (field->options().ctype()) { default: // TODO(kenton): Support other string reps. case FieldOptions::STRING: MutableRaw >(message, field)->Clear(); break; } break; } case FieldDescriptor::CPPTYPE_MESSAGE: { // We don't know which subclass of RepeatedPtrFieldBase the type is, // so we use RepeatedPtrFieldBase directly. MutableRaw (message, field) ->Clear >(); break; } } }}
通过上面的代码及图5可以看出,Clear操作采用了递归的方式对Message中的逐个字段都进行了处理。对于基础类型字段,代码会对每个字段都设置默认值。对于一个非常长大的Message来说,消耗的CPU会非常多。相对于这种情况,释放Message的内存并重新申请小的空间,所占用CPU资源反而更少一些。在这个Case中,经常出现Clear操作清理6、7M内存的情况。这样数据量的Clear操作与释放Message,再申请200K Message空间比起来,显然更消耗CPU资源。
7. 总结 protobuf的cache机制 protobuf message的clear()操作是存在cache机制的,它并不会释放申请的空间,这导致占用的空间越来越大。如果程序中protobuf message占用的空间变化很大,那么最好每次或定期进行清理。这样可以避免内存不断的上涨。这也是模块内存一直上涨的核心问题。 内存监控机制 需要对程序的各个模块添加合适的监控机制,这样当某个module的内存占用增加时,我们可以及时发现细节的问题,而不用从头排查。根据这次的排查经验,后面会主导在产品代码中添加线程/module级内存和cpu处理时间的监控,将监控再往”下”做一层。 UT在内存问题定位中的作用 在逐个对module进行排查时,UT验证比在测试环境中更高效,当然前提是这些module的UT能够比较容易的写出来。这也是使用先进框架的一个原因。对于验证环境代价高昂的模块,UT验证的效果更加明显。 百度MTC是业界领先的移动应用测试服务平台,为广大开发者在移动应用测试中面临的成本、技术和效率问题提供解决方案。同时分享行业领先的百度技术,作者来自百度员工和业界领袖等。 >>