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