Protobuf NewCallback回调拷贝开销问题及优化方案咨询
问题1:拷贝次数的理解是否正确
你的计数是完全正确的,我们可以拆解传值场景下shared_ptr的完整拷贝流程:
- 调用
NewCallback时,值传递的形参触发第一次拷贝 NewCallback内部调用MethodClosure2构造函数,值传递的构造函数形参触发第二次拷贝- 构造函数初始化列表给成员变量
arg1_/arg2_赋值,触发第三次拷贝 Run方法调用目标函数时,将成员变量传入值传递的目标形参,触发第四次拷贝
如果使用std::move传入右值,第一步会替换为移动构造,剩下三次仍然是拷贝,和你描述的3次拷贝+1次移动的结果一致。
问题2:是否需要关注该开销,是否要只用基础类型/裸指针规避
是否需要关注完全取决于你的业务场景:
- 若你的服务异步RPC调用QPS低于1000,这个开销占比极低,完全不需要额外处理
- 若你服务的异步调用QPS过万,你实测的1万次拷贝耗时1秒的开销会占相当大的CPU比例,必须优化
不推荐为了规避拷贝只用裸指针:异步场景下回调的执行时机不确定,裸指针的生命周期极难管理,野指针导致的崩溃问题比性能问题影响严重得多,shared_ptr的安全收益远大于拷贝开销的损失,不需要因噎废食。
问题3:是否可以通过std::move/std::forward优化
完全可以,通过完美转发改造接口可以把拷贝次数降到最低,优化后的参考实现如下:
改造NewCallback接口,支持万能引用传参
// 改用可变参数模板+万能引用适配任意参数个数的方法 template <typename Class, typename Method, typename... Args> inline Closure* NewCallback(Class* object, Method method, Args&&... args) { return new internal::MethodClosure<Class, Method, std::decay_t<Args>...>( object, method, true, std::forward<Args>(args)...); }
改造MethodClosure实现,减少不必要拷贝
template <typename Class, typename Method, typename... Args> class MethodClosure : public Closure { public: template <typename... FwdArgs> MethodClosure(Class* object, Method method, bool self_deleting, FwdArgs&&... args) : object_(object), method_(method), self_deleting_(self_deleting), args_(std::forward<FwdArgs>(args)...) {} void Run() override { bool needs_delete = self_deleting_; // 把tuple存储的参数移动到目标函数,避免拷贝 std::apply([this](auto&&... unpacked_args) { (object_->*method_)(std::move(unpacked_args)...); }, args_); if (needs_delete) delete this; } private: Class* object_; Method method_; bool self_deleting_; // 用tuple存储任意个数、任意类型的参数 std::tuple<std::decay_t<Args>...> args_; };
改造完成后:
- 传入左值参数时,全程只有1次拷贝(参数存入tuple时触发)
- 传入
std::move的右值参数时,全程只有1次移动,没有任何拷贝
相比原来的实现性能提升非常明显,同时完全保留了shared_ptr的生命周期安全特性。
内容的提问来源于stack exchange,提问作者user9429062
相关产品推荐
相关产品推荐

