作者:guoling,来自微信客户端团队
除了显而易见的减少实例化类型的数量(实际业务场景下其实大部分减不了),「本文主要是提供适用于一些具体场景、可实际操作的优化策略以减少C++模板代码的大小。」
主要包括:
如果模板函数中有一部分代码与模板参数无关,那么可以将这部分代码提取出来,放到一个非模板函数中。这样,这部分代码只需要生成一次,而不是在每个模板实例中都生成一次。
为了方便讨论,后续的例子基于这个场景:我们提供一个集中的 Service 单例管理器。以下是大体的框架:
// 所有 Service 需要实现的接口class BaseService {public: virtual ~BaseService() = default; virtual void onServiceInit() = 0; …… std::string contextName{};};// 所有 Service 单例的统一管理中心class ServiceCenter {public: explicit ServiceCenter(const std::string& name) : _contextName(name) {} template<typename T> std::shared_ptr<T> getService() { …… } private: std::unordered_map<std::string, std::shared_ptr<BaseService>> _serviceMap = {}; std::string _contextName; std::recursive_mutex _mutex;};
例如,在我们的例子中,getService() 函数最简单的版本可能长这样,显然,一大部分代码是与模板参数无关的,可以提取出来:
class ServiceCenter {public: template<typename T> std::shared_ptr<T> getService() { auto const key = typeid(T).name(); std::lock_guard<std::recursive_mutex> lock(_mutex); auto const itr = _serviceMap.find(key); if (itr == _serviceMap.end()) { return nullptr; } auto service = itr->second; return std::dynamic_pointer_cast<T>(service); }};
我们抽出一个非模板的函数getService(const std::string &key),将加锁、查询 map 这些逻辑都挪进去,优化后:
class ServiceCenter {public: std::shared_ptr<BaseService> getService(const std::string &key) { std::lock_guard<std::recursive_mutex> lock(_mutex); auto const itr = _serviceMap.find(key); if (itr == _serviceMap.end()) { return nullptr; } return itr->second; } template<typename T> std::shared_ptr<T> getService() { auto const key = typeid(T).name(); auto service = getService(key); return std::dynamic_pointer_cast<T>(service); }};
例如,getService()函数不但要管查询,还要按需创建新实例、初始化、以及各种异常处理,有3行代码都用了类型T:
class ServiceCenter {public: template<typename T> std::shared_ptr<T> getService() { std::lock_guard<std::recursive_mutex> lock(_mutex); auto const key = typeid(T).name(); auto const service = getService(key); if (service == nullptr) { auto const tService = std::make_shared<T>(); tService->contextName = _contextName; setService(key, tService); tService->onServiceInit(); return tService; } else { auto const tService = std::dynamic_pointer_cast<T>(service); if (tService == nullptr) { aerror("ServiceCenter", "tService is null"); return nullptr; } return tService; } } void setService(const std::string &key, const std::shared_ptr<AffBaseService> &service) {....}};
这种情况我们可以将需要用到类型T的地方进行封装,抽象出一套协议。具体来说,用到T的地方分别有:
使用最常见的多态(也可用 C 函数指针数组std::function<>传参等)来组织这 3 个抽象接口:
class ServiceTypeHelperBase {public: virtual ~ServiceTypeHelperBase() = default; virtual const char* getTypeName() const = 0; virtual std::shared_ptr<BaseService> newInstance() const = 0; virtual std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const = 0;}; template<typename T>class ServiceTypeHelper : public ServiceTypeHelperBase { const char* getTypeName() const override { return typeid(T).name(); } std::shared_ptr<BaseService> newInstance() const override { return std::make_shared<T>(); } std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const override { return std::dynamic_pointer_cast<T>(service); }};
然后就可以抽出非模板的函数getService(const ServiceTypeHelperBase* helper)。可见到,得益于合理的抽象,新函数跟没优化之前的getService()几乎一模一样:
class ServiceCenter {public: std::shared_ptr<void> getService(const ServiceTypeHelperBase* helper) { std::lock_guard<std::recursive_mutex> lock(_mutex); auto const key = helper->getTypeName(); auto const service = getService(key); if (service == nullptr) { auto const tService = helper->newInstance(); tService->contextName = _contextName; setService(key, tService); tService->onServiceInit(); return helper->castToOriginType(tService); } else { auto const tService = helper->castToOriginType(service); if (tService == nullptr) { aerror("ServiceCenter", "tService is null"); return nullptr; } return tService; } } template<typename T> std::shared_ptr<T> getService() { ServiceTypeHelper<T> helper; auto service = getService(&helper); return std::static_pointer_cast<T>(service); }};
注意,抽象出来的接口必须「足够精简,避免换个地方写模板函数」;太复杂的话,起不到瘦身效果。例如下面这样,就是不够精简的抽象:
// 反面例子,不要这样做template<typename T>class ServiceTypeHelper : public ServiceTypeHelperBase { const char* getTypeName() const override { return typeid(T).name(); } std::shared_ptr<BaseService> newInstance() const override { auto const tService = std::make_shared<T>(); tService->contextName = _contextName; setService(key, tService); tService->onServiceInit(); return tService; } std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const override { auto const tService = std::dynamic_pointer_cast<T>(service); if (tService == nullptr) { aerror("ServiceCenter", "tService is null"); return nullptr; } return tService; }};
❝
特别注意:这里的基类指「非模板基类」,或者「模板参数比子类少的基类」;否则只是换个地方写模板类,起不到瘦身效果。
❞
编译器每实例化一个模板类,会将类的所有部分都复制一份,包括非模板成员变量、模板成员变量、非模板函数、模板函数。尤其是「非模板成员变量和非模板函数,也会复制生成一份」,即使它们没有用到模板信息。这是很多人都会忽视的地方。因此,将通用部分提取到基类,避免编译器重复生成同样的代码,就成了瘦身的有效手段。
为了方便讨论,依然以 ServiceCenter 来举例。假设我们每个大业务(朋友圈、视频号等)都有自己的一套 XXXBaseService,定制了一些大业务相关的通用处理逻辑,因此他们希望有业务专门的 XXXServiceCenter,大概是这么一个架构:
// 业务相关的 BaseService 协议class BussinessBaseService : public BaseService {public: BussinessBaseService(const std::string& name) : _bussinessName(name) {} virtual void onBussinessEnter() = 0; virtual void onBussinessExit() = 0; ……protected: const std::string _bussinessName;};// 业务相关的 ServiceCentertemplate <typename BaseService_t>class ServiceCenter {public: explicit ServiceCenter(); public: void setService(const std::string &key, const std::shared_ptr<BaseService> &service); std::shared_ptr<BaseService> getService(const std::string &key); template<typename T> std::shared_ptr<T> getService() { static_assert(std::is_base_of<BaseService_t, T>::value, "Wrong Service Type"); …… }……private: std::unordered_map<std::string, std::shared_ptr<BaseService>> _serviceMap = {}; std::string _bussinessName; std::string _contextName; std::recursive_mutex _mutex;};// 例如朋友圈业务 Service 基类class TLBussinessServiceBase : public BussinessBaseService { TLBussinessServiceBase(const std::string& name) : BussinessBaseService(name) {} void onBussinessEnter() override { /*some common logic*/ } void onBussinessExit() override { /*some common logic*/ } ……};// 朋友圈 ServiceCenter,要求所有 Service 都继承自 TLBussinessServiceBasestatic ServiceCenter<TLBussinessServiceBase> g_tlServiceCenter;
这个是不言自明的机械操作:
class BaseServiceCenter {public: void setService(const std::string &key, const std::shared_ptr<BaseService> &service); std::shared_ptr<BaseService> getService(const std::string &key); protected: std::unordered_map<std::string, std::shared_ptr<BaseService>> _serviceMap = {}; std::string _contextName; std::recursive_mutex _mutex;}; template <typename BaseService_t>class ServiceCenter : BaseServiceCenter {public: template<typename T> std::shared_ptr<T> getService() { …… }……private: std::string _bussinessName;};
我们先来看新版getService(),跟之前的有点不一样,Service 构造函数多一个参数:
template <typename BaseService_t>class ServiceCenter : BaseServiceCenter {public: template<typename T> std::shared_ptr<T> getService() { static_assert(std::is_base_of<BaseService_t, T>::value, "Wrong Service Type"); std::lock_guard<std::recursive_mutex> lock(_mutex); auto const name = typeid(T).name(); auto const service = getService(name); if (service == nullptr) { auto const tService = std::make_shared<T>(_bussinessName); // 这里不一样 tService->contextName = _contextName; setService(name, tService); tService->onServiceInit(); return tService; } else { auto const tService = std::dynamic_pointer_cast<T>(service); if (tService == nullptr) { aerror("ServiceCenter", "tService is null"); return nullptr; } return tService; } }};
因此我们抽象出来的 XXXHelper::newInstance() 也需要多一个参数:
class BussinessServiceTypeHelperBase : public ServiceTypeHelperBase {public: std::shared_ptr<BaseService> newInstance() const override { return nullptr; } virtual std::shared_ptr<BussinessBaseService> newInstance(const std::string& name) const = 0;};template<typename T>class BussinessServiceTypeHelper : public BussinessServiceTypeHelperBase { const char* getTypeName() const override { return typeid(T).name(); } std::shared_ptr<BussinessBaseService> newInstance(const std::string& name) const override { return std::make_shared<T>(name); } std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const override { return std::dynamic_pointer_cast<T>(service); }};
然后就可以类似 1.2 那样抽出共有逻辑,并挪到基类:
class BaseServiceCenter {public: std::shared_ptr<void> getService(const BussinessServiceTypeHelperBase* helper, const std::string& bussinessName) { std::lock_guard<std::recursive_mutex> lock(_mutex); auto const key = helper->getTypeName(); auto const service = getService(key); if (service == nullptr) { auto const tService = helper->newInstance(bussinessName); tService->contextName = _contextName; setService(key, tService); tService->onServiceInit(); return helper->castToOriginType(tService); } else { auto const tService = helper->castToOriginType(service); if (tService == nullptr) { aerror("ServiceCenter", "tService is null"); return nullptr; } return tService; } }};template <typename BaseService_t>class ServiceCenter : BaseServiceCenter {public: template<typename T> std::shared_ptr<T> getService() { static_assert(std::is_base_of<BaseService_t, T>::value, "Wrong Service Type"); BussinessServiceTypeHelper<T> helper; auto service = getService(&helper, _bussinessName); return std::static_pointer_cast<T>(service); }};
如果基类也有模板参数,那么应尽量使基类的模板参数比子类少,并把子类的共用部分挪到基类。例如,假设现在有如下子类和基类,T 的实例个数是 n,U 的实例个数是 m,那么子类的每个成员变量和成员函数都会「生成 n*m 份」;如果把子类里只与 T 相关的成员挪到基类,那么这些成员「只会生成 n 份」,少了一个数量级。更详细的分析可参考 Effective C++ 44:将参数无关代码重构到模板外去。
template<typename T, typename U>class Pair {public: std::string getFirstTypeName() const { auto typeName = typeid(T).name(); int status; auto demangledName = abi::__cxa_demangle(typeName, 0, 0, &status); if (!demangledName) { return typeName; } std::string result = demangledName; free(demangledName); return result; } T first; U second;};
上面的例子里,getFirstTypeName()明显跟参数U无关,因此可抽出一个基类,并把该函数挪过去:
template<typename T>class PairLeft {public: std::string getTypeName() const { auto typeName = typeid(T).name(); int status; auto demangledName = abi::__cxa_demangle(typeName, 0, 0, &status); if (!demangledName) { return typeName; } std::string result = demangledName; free(demangledName); return result; }};template<typename T, typename U>class Pair : private PairLeft<T> { using Base = PairLeft<T>;public: std::string getFirstTypeName() const { return Base::getTypeName(); }};
❝
还可以进一步将函数 PairLeft::getTypeName() 抽离出模板无关的部分,放到一个非模板基类里。具体可参考前文,不再赘述。
❞
不要为了用模板而用模板。这里举一个具体的反面例子(github上某开源库):一个提供了类似上述 ServiceCenter 功能的库,它侧重点是控制反转 Inversion of Control,亮点是对构造函数依赖的参数的自动查找和构建,以及类型映射。我们学习一个库,除了学它好的地方,也要看到它不好的地方,引以为鉴。
场景1:基类 RegistrationDescriptorBase<TDescriptor, TDescriptorInfo>有两个模板参数,仔细看代码,会发现压根没在基类用过。而这个会导致非常严重的代码膨胀,每个<TDescriptor, TDescriptorInfo>组合就会生成一套全新的基类。
场景2:工具类 EnforceBaseOf<TDescriptorInfo, TBase, T>里面的TDescriptorInfo参数在判断T是否继承自TBase时,完全没用,不知为何要加这么一个参数。
这个形容词我想了很久,没找一个合适的词去形容,因为实在太震撼。具体是这个类AutowireableConstructorRegistrationDescriptor:
template <class TDescriptorInfo>class AutowireableConstructorRegistrationDescriptor : public RegistrationDescriptorBase< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >, public RegistrationDescriptorOperations::As< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >, public RegistrationDescriptorOperations::OnActivated< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >, public RegistrationDescriptorOperations::SingleInstance< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >, ……{ ……};
以及这个类RegistrationDescriptorInfo:
template< class T, InstanceLifetimes::InstanceLifetime Lifetime = InstanceLifetimes::Transient, class TSelfRegistrationTag = Tags::NotSelfRegistered, class TFallbackRegistrationTag = Tags::DefaultRegistration, class TRegisteredBases = MetaMap<>, class TDependencies = MetaMap<>>struct RegistrationDescriptorInfo{ typedef T InstanceType; typedef std::integral_constant< InstanceLifetimes::InstanceLifetime, Lifetime > InstanceLifetime; typedef TSelfRegistrationTag SelfRegistrationTag; typedef TFallbackRegistrationTag FallbackRegistrationTag; typedef TRegisteredBases RegisteredBases; typedef TDependencies Dependencies; struct SingleInstance { typedef RegistrationDescriptorInfo < InstanceType, InstanceLifetimes::Persistent, SelfRegistrationTag, FallbackRegistrationTag, RegisteredBases, Dependencies > Type; }; template <class TBase> struct RegisterBase { typedef RegistrationDescriptorInfo < InstanceType, InstanceLifetime::value, SelfRegistrationTag, FallbackRegistrationTag, typename MetaInsert< RegisteredBases, MetaPair< TBase, MetaIdentity< TBase > > >::Type, Dependencies > Type; }; template <class TBase> struct IsBaseRegistered : MetaContains< RegisteredBases, TBase > { }; ……};
这两个模板类组合起来,可以提供类似下面这样的语法:
auto contain_builder = Hypodermic::ContainerBuilder();contain_builder.registerType<TServiceImp>() .template as<TServiceInterface>() .template as<TServiceBase>() .singleInstance() .onActivated( [](Hypodermic::ComponentContext &ctx, const std::shared_ptr<TServiceImp> &service) { // some onActive logic } );
看起来挺干爽的,有什么问题?
问题如此严重,那要怎么优化?回头看作者的用心,大概或许应该是防止用户出错。例如映射的基类并不是基类、重复映射同一个基类、重复设置singleInstance,等等「可以在编译期发现的错误」。如果抛开这个大设定,其实所有配置信息都可以用一个 POD (Plain Old Data) 结构体来记录,最后再一次性生效。在 POD 结构体的基础上,我们再来看哪些是可以零成本在编译期完成的错误检查:
其他接口如named(), asSelf(), with()等等,也都可参考上面做法,分别在编译期/运行时解决,不再赘述。
通常来说,多用组合少用继承是设计模式上的好建议,它能提高灵活性、减低耦合度、增强代码复用、减少继承层次。但其实它还有瘦身意义上的好处。假设我们有一个模板类 GraphicObject,它有两个模板参数:Shape 表示形状类型,Color 表示颜色类型。
template<typename Shape, typename Color>class GraphicObject {public: // ...};
如果我们有很多不同的 Shape 和 Color 类型,那么 GraphicObject 的每种组合都会生成一个新的模板实例,这可能会导致生成的代码量非常大。
为了减少模板实例化的大小,我们可以将 Shape 和 Color 类型的处理逻辑分离出来,使它们成为 GraphicObject 的成员,而不是模板参数。这样,GraphicObject 就不再需要为每种 Shape 和 Color 类型的组合生成一个新的模板实例,从而减少了模板实例化的大小。
class ShapeBase {public: // ...};class ColorBase {public: // ...};class GraphicObject {public: GraphicObject(std::shared_ptr<ShapeBase> shape, std::shared_ptr<ColorBase> color) : shape_(shape), color_(color) {} // ...private: std::shared_ptr<ShapeBase> shape_; std::shared_ptr<ColorBase> color_;};
模板函数中的对象会在每个模板实例中都生成一份,因此应该避免在模板函数中使用大型对象。如果必须使用大型对象,可以考虑使用指针或引用,或者将对象移动到函数外部。
Linux 平台下(包括Android),可用 nm 打印出每个符号的大小并按大小排序:
nm --print-size --size-sort xxx_binary
在 macOS/iOS 平台下,可通过生成 LinkMapFile.txt 来进行分析。
❝
需要注意的是,现代编译器能够对不同编译单元的相同模板函数进行去重,所以需要对最终链接产物(可执行文件 / 动态库)进行测量,不要对单个 .o .a 文件进行分析。
❞
上述描述的策略目前正逐步应用到微信客户端内进行优化,目前的优化效果是:「将有24个 Service 的代码库从14M瘦身到11M,减少体积22%,效果非常明显。」
总的来说,优化C++模板代码的关键是减少每个模板实例的大小,本文描述的优化策略可以帮助我们提高编译速度,减小生成的二进制文件大小,同时保持代码的可读性和可维护性,完整总结如下:
作者:guoling
来源:微信公众号:微信客户端技术团队
出处
:https://mp.weixin.qq.com/s/aRqKGoVNcf8yzRhIHbmoBA