深入理解TVM:Object家族

​一、 Object简介

Object类在TVM里非常重要,基本上所有其他的类都直接或者间接继承自这个类,我做了一个简单的统计,基于这个基本类派生出来的类目前有三百多个(基于commit:fe25b9e7c,使用TypeContext类提供的Dump功能,可以把所有的类名和继承关系用文本的方式打印出来),比较靠顶层的部分类信息如下图:

调用Dump函数的代码如下:

#include <dmlc/logging.h>
#include <tvm/runtime/memory.h>
#include <tvm/runtime/object.h>
#include <tvm/runtime/registry.h>
​
int main(int argc, char** argv) {
  const string func_name = "runtime.DumpTypeTable";
  const tvm::runtime::PackedFunc* fp = 
      tvm::runtime::Registry::Get(func_name);
  ICHECK(fp != nullptr);
  (*fp)(0);
}

二、ObjectPtr、ObjectRef

ObjectPtr可以看作是指向Object的一个模拟指针的封装类,它的关键的数据成员和几个运算符重载接口如下:

// 关键数据成员,一个指向Object对象的指针
Object *data_{nullptr};
// 几个关键的运算符重载接口,用于获取指向Object的指针或者引用
T* get() const { return static_cast<T*>(data_); }
T* operator->() const { return get(); }
T& operator*() const { return *get(); }

ObjectRef也可以看作是指向Object的一个封装类(其实官方文档是这样介绍的:We use ObjectRef class to represent a reference to the Object, We can roughly view ObjectRef class as shared_ptr to the Object container.),它的关键数据成员和几个运算符重载接口如下:

// 关键数据成员
ObjectPtr<Object> data_;
// 关键运算符重载接口
const Object* get() const { return data_.get(); }
const Object* operator->() const { return get(); }

TVM里有个不成文的约定,所有以Node为结尾的类名都是继承自Object,不以Node结尾的类名都是继承自ObjectRef,例如:

// include/tvm/runtime/module.h
class Module : public ObjectRef {};
class ModuleNode : public Object {};

三、自定义继承类

主要有几个静态变量约定需要遵守,包括:

  • _type_key:类的全局唯一的字符串标识符
  • _type_index:类的全局唯一的uint32_t标识符
  • _type_child_slots:为子类预留的index个数
  • _type_final:表示是不是没有子类了,可以通过TVM_DECLARE_FINAL_OBJECT_INFO这个宏来设置
  • _type_child_slots_can_overflow:标识是不是可以超过_type_child_slots定义的数量

示例代码如下:

class ObjBase : public Object {
 public:
  // dynamically allocate slow
  static constexpr const uint32_t _type_index = TypeIndex::kDynamic;
  static constexpr const uint32_t _type_child_slots = 1000;
  static constexpr const char* _type_key = "test.ObjBase";
  TVM_DECLARE_BASE_OBJECT_INFO(ObjBase, Object);
};
// 定义过类之后要进行注册
TVM_REGISTER_OBJECT_TYPE(ObjBase);

四、RuntimeTypeIndex接口

在Object类的继承体系中,每个类都会有一个RuntimeTypeIndex的静态接口,返回一个能够标识这个类的全局unique id,Object类的这个值是TypeIndex::kRoot, 即下面列出的静态类型的第一个:

// include/tvm/runtime/object.h
struct TypeIndex {
  enum {
    kRoot = 0,
    kRuntimeModule = 1,
    kRuntimeNDArray = 2,
    kRuntimeString = 3,
    kRuntimeArray = 4,
    kRuntimeMap = 5,
    kRuntimeClosure,
    kRuntimeADT,
    kStaticIndexEnd,
    kDynamic = kStaticIndexEnd
  };
};

对于我们自己定义的扩展类,都要选择kDynamic类型,然后使用TVM_DECLARE_BASE_OBJECT_INFO宏来定义这个类的RuntimeTypeIndex接口,宏定义如下(代码做了简化):

// include/tvm/runtime/object.h
#define TVM_DECLARE_BASE_OBJECT_INFO(TypeName, ParentType)       \
  static uint32_t RuntimeTypeIndex() {                           \
    if (TypeName::_type_index != TypeIndex::kDynamic) {          \
      return TypeName::_type_index;                              \
    }                                                            \
    return _GetOrAllocRuntimeTypeIndex();                        \
  }  
  static uint32_t _GetOrAllocRuntimeTypeIndex() {                \
    static uint32_t tindex = Object::GetOrAllocRuntimeTypeIndex( \
        TypeName::_type_key,                                     \
        TypeName::_type_index,                                   \
        ParentType::_GetOrAllocRuntimeTypeIndex(),               \
        TypeName::_type_child_slots,                             \
        TypeName::_type_child_slots_can_overflow);               \
    return tindex;                                               \
  }

五、type_index分配算法

Object继承体系的每个类的内部都会使用_type_index这个uint32_t的static变量来唯一标识当前这个类,这个值也是上面说到的RuntimeTypeIndex这个接口的返回值,_type_index的值通过Object::GetOrAllocRuntimeTypeIndex这个接口来分配,具体的分配算法会用到下面的数据结构:

// src/runtime/object.cc
struct TypeInfo {
  uint32_t index{0};
  uint32_t parent_index{0};
​
  uint32_t num_slots{0};
  uint32_t allocated_slots{0};
  bool child_slots_can_overflow{true};
  std::string name;
  size_t name_hash{0};
};
class TypeContext {
  // 略去所有实现,只列出用到的关键数据结构
  std::mutex mutex_;
  std::atomic<uint32_t> type_counter_{TypeIndex::kStaticIndexEnd};
  std::vector<TypeInfo> type_table_;
  std::unordered_map<std::string, uint32_t> type_key2index_;
};

TypeContext是个单例,具体的分配算法就是在父类预留的_type_child_slots范围内确定当前类的_type_index,然后更新type_table_这个vector,它的下标同时也是具体分配到的type_index。

六、type_index应用

可以用于IsInstance这个辅助函数的加速,内部实现就是直接判断子类的_type_index的值是不是在父类预留的_type_child_slots范围之内,具体代码如下(代码做了简化):

// include/tvm/runtime/object.h
template <typename TargetType>
inline bool Object::IsInstance() const {
  if (std::is_same<TargetType, Object>::value) return true;
​
  if (TargetType::_type_final) {
    return type_index_ == TargetType::RuntimeTypeIndex();
  } else {
    // quick check using type_index
    uint32_t begin = TargetType::RuntimeTypeIndex();
    if (TargetType::_type_child_slots != 0) {
      uint32_t end = begin + TargetType::_type_child_slots;
      if (type_index_ >= begin && type_index_ < end) return true;
    } else {
      if (type_index_ == begin) return true;
    }
    if (!TargetType::_type_child_slots_can_overflow) return false;
    if (type_index_ < TargetType::RuntimeTypeIndex()) return false;
​​
    // slow check using type hierarchy
    return DerivedFrom(TargetType::RuntimeTypeIndex());
  }
}

七、最后

列一下Object继承体系中有最多子类的类_type_key的相关信息统计,这都是TVM里面比较重要的类,后续的文章还会继续研究相关细节:

  • BaseExpr(12):
    • parent=runtime.Object
    • num_child_slots=62
    • num_children=62
  • PrimExpr(13)
    • parent=BaseExpr
    • num_child_slots=38
    • num_children=38
  • RelayExpr(52)
    • parent=BaseExpr
    • num_child_slots=22
    • num_children=22
  • Type(110)
    • parent=runtime.Object
    • num_child_slots=14
    • num_children=14
  • Attrs(125)
    • parent=runtime.Object
    • num_child_slots=0
    • num_children=125
  • Operation(149)
    • parent=runtime.Object
    • num_child_slots=0
    • num_children=7
  • tir.Stmt(176)
    • parent=runtime.Object
    • num_child_slots=15
    • num_children=17
  • DFPatternNode(343)
    • parent=runtime.Object
    • num_child_slots=0
    • num_children=16

再宣传一下自己的公众号哈,欢迎大家关注、留言、提建议,stay hungry,stay foolish!

http://weixin.qq.com/r/axO6osnEwZS_rY3B90Z5 (二维码自动识别)

之前已经写了一篇《深入理解TVM:Object家族》,因为Object抽象在TVM里太过基础和重要,本文再深入学习和挖掘一些细节

一、Object、ObjectPtr、ObjectRef关系

前文已经对这三个类做了大概的介绍,本文我又画了一个更直观的图来展示它们之间的关系,这样更好理解:

图1

在具体的使用当中,Object机制有两套继承体系,一套继承自Object,用于表示实际的类,一套继承自ObjectRef,它就像是指向实际类对象的智能指针,用于操作实际的类对象,虽然实际上没有shared_ptr,但我们可以用下图来帮助理解这两套继承体系:

图2

前文《具有共享所有权的智能指针(一)具有共享所有权的智能指针(二)》已经仔细分析过shared_ptr,如果拿Object、ObjectPtr、ObjectRef这三个类和shared_ptr类比的话:

  • Object相当于控制块,可以通过引用计数ref_counter_来控制对象的生命周期,对象的析构函数也可以通过deleter_这个函数指针指定
  • Object的子类的除去Object基类的部分相当于数据块,里面保存有类的真实数据
  • ObjectRef就像是shared_ptr这个wrapper,自身不包含实际数据,但是可以操作实际的数据
  • ObjectPtr的作用在使用的角度有点类似ObjectRef,不同的是数据类型,ObjectPtr<T>是一个模板,下面还会详细讲ObjectPtr

二、Object对象的构造和析构

前文《深入理解TVM:内存分配器》中详细讲了make_object和make_inplace_array_object这两个helper function,在TVM中,make_object通常被用来创建Object系列类对象,make_inplace_array_object通常用来创建数组类对象(数组是Object系列类类型,数组元素是ObjectRef系列类类型),最底层依然是使用标准库的new/delete创建和释放对象,并且还可以方便的扩展出新的内存管理器

我觉得还有一个做法可以用来定制化构造析构Object对象,就是直接在Object类中重载operatornew/delete函数,这个之前在《内存管理:new and delete》中讲过,重载代码大概如下:

class Foo {
  static void *operator new(size_t size) {
    。。。
    void *p = ::operator new(size);
    。。。
    return p;
  }

  static void operator delete(void *ptr) {
    。。。
    ::operator delete(ptr);
    。。。
  }
}

三、需不需要ObjectPtr类

前面图1展示了既然Object、ObjectPtr、ObjectRef的关系,ObjectRef hold指向ObjectPtr的指针,ObjectPtr hold指向Object的指针,能不能像图2一样,ObjectRef直接hold指向Object的指针呢?

先看一下ObjectPtr对象主要是从哪被创建来的,《深入理解TVM:内存分配器》中讲了make_object这个helper function,它是TVM中创建Object对象的一个主要方法(还有一个创建Object数组对象的make_inplace_array_object,原理相同,不再单独说):

template <typename T, typename... Args>
inline ObjectPtr<T> make_object(Args&&... args) {
  return SimpleObjAllocator().make_object<T>(std::forward<Args>(args)...);
}

在TVM code base中,make_object通常被用来在ObjectRef的子类构造函数中被调用来创建Object子类对象,如下所示,在Tensor类的构造函数中使用make_object创建TensorNode对象:

Tensor::Tensor(Array<PrimExpr> shape, DataType dtype, 
    Operation op, int value_index) {
  auto n = make_object<TensorNode>();
  n->shape = std::move(shape);
  n->dtype = dtype;
  n->op = op;
  n->value_index = value_index;
  data_ = std::move(n);
}

上面代码中的data_变量,根据前面图1,是ObjectPtr<Object>类型,而临时变量n的类型是ObjectPtr<TensorNode>,最后给data_的赋值操作,调用了ObjectPtr类的下面这个构造函数来做转换:

// 注意这里的data_已经不是上面说的那个data_,
// 这里data_的类型是Object*
template <typename T> class ObjectPtr {
public:
  template <typename Y>
  ObjectPtr(ObjectPtr<Y>&& other) : data_(other.data_) {
    static_assert(std::is_base_of<T, Y>::value, "error");
    other.data_ = nullptr;
  }
private:
  Object* data_{nullptr};
};

上面是在使用ObjectPtr的情况下,从上到下的一个示例。

如果TVM的实现没有ObjectPtr,我们首先需要改变一下ObjectRef的实现,最主要的是把原来的ObjectRef中的ObjectPtr<Object>类型的数据成员改成Object *类型的数据成员,还有在相关的构造函数和析构函数中调用Object中的IncRef()函数和DecRef()函数用来更新引用计数,如下所示(代码很不完整,只为表达这里说的最主要的意思):

class ObjectRef {
public:
  ObjectRef(Object *data) {
    data_ = data;
    data_->IncRef(); 
  }
  ObjectRef(const ObjectRef& other) {
    data_ = other.get();
    data_->IncRef(); 
  }
  ~ObjectRef() { data_->IncRef(); }
  ObjectRef& operator=(const ObjectRef& other) {
    data_->DecRef();
    data_ = other.get();
    data_->IncRef();
  }
  
  const Object* get() const { return data_.get(); }
  const Object* operator->() const { return get(); }
  
private:
  Object *data_{nullptr};
};

然后需要把make_object改成类似下面这样,直接返回raw pointer:

template <typename T, typename... Args>
inline T* make_object(Args&&... args) {
  return SimpleObjAllocator().make_object<T>(std::forward<Args>(args)...);
}

然后在前面的Tensor构造函数的例子中,改成下面这样:

Tensor::Tensor(Array<PrimExpr> shape, DataType dtype, 
    Operation op, int value_index) {
  data_ = make_object<TensorNode>();
  data_->shape = std::move(shape);
  data_->dtype = dtype;
  data_->op = op;
  data_->value_index = value_index;
}

根据上面的分析,感觉ObjectPtr是可以去掉的,不使用ObjectPtr的代码更加清晰,但也有可能我对TVM的理解不全面不深入,可能ObjectPtr是不能去掉的,大家可以留言一起讨论。

顺便再头脑风暴一下ObjectRef的是不是也可以被替换掉,前面已经基本分析了ObjectRef的作用,它有点像shared_ptr,单从实现的角度上来看,如果不用ObjectRef这个类体系,我觉得也是可以的,直接用标准库的智能指针来管理Object类体系对象,从作用上来看,使用一个Tensor对象,和使用一个shared_ptr<TensorNode>对象,可以实现相同的功能,但是整个TVM的code都是构建在ObjectRef之上的,先不说替换成智能指针之后有可能带来的性能损失,单是实现这种替代方案基本上都算是整个代码库重构了,所以ObjectRef没有改变的必要性。

四、Summary and Reference

我觉得Object机制最主要的优点有两个:

  1. 统一抽象:这个很好理解,所有的类都继承自Object,简化了很多接口设计,整个系统的可扩展性很好
  2. 性能优势:这个重点说下,我感觉这个优势主要来自于Object机制中的下面两个实现
  • 侵入式的智能指针:Object机制中对于对象生命周期的管理实际上是一种侵入式的智能指针,虽然没有shared_ptr强大,但是通过对源码的分析,应该比shared_ptr快,之前《内存管理:几个简单测试(二)》中也有对shared_ptr的测试,shared_ptr的开销确实挺大的,对于侵入式的智能指针,以后还会再抽时间单独总结
  • 简单的RTTI:Object机制中使用一个整数值type_index_来标识当前的类类型,继承体系中的所有类的type_index_值可以简单认为在编译时就已经确定好,这显然比使用C++虚函数机制中的RTTI开销要小

本文参考了下面这些资料:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值