tensorflow variant源码分析

tensorflow variant基本语义

通过分析tf.variant的源码可以知道它其实是一个any语义,即这个类型可以被任意类型赋值,它的主要目的就是做彻底的类型擦除。

这个名字取得有迷惑性,它和标准库和boost库中的variant语义是不一样的,而是和c++17中的std::any对应的。

tf.variant用法

可以从tf的测试代码中知道它的基本用法,用法很简单:

从这个测试代码中可以看到tf.variant和std::any/boost.any用法是差不多的。

tf.variant内部有一个unique_ptr指针, 默认为nullptr,所以没有赋值的时候总是返回nullptr。

Int(42)赋值给variant之后,就可以通过get(x)来获得Int指针了,接着就可以得到其实际的Int值了;因为这个Variant是any语义,所以任意类型比如Tensor也可以复制给它,取值方法也是类似的,先取T指针,接着调用value得到初始值。

注意,这里的get中类型必须和赋值时候的类型一致,不一致的时候会返回nullptr(除了void,传void时会返回void*指针);

any和variant比较

之前介绍过vairant,知道它也是用来做类型擦除的,不过variant擦除的类型是有限个的,必须要事先指定,它只能实现部分的类型擦除。

而any也是实现类型擦除的,它可以代表任何类型,不需要像variant那样需要事先确定擦除的类型,看起来更方便更强大了。

事实上这个any并不是变得更强大了,它虽然能彻底擦除类型,但是,在取值的时候需要知道准确的类型,这反倒不如variant方便了,variant在取值的时候通过一个visitor是不需要知道具体类型的。

二者的本质区别是类型擦除方式不一样,variant是通过栈上的一个定长内存块去保存赋值对象的,variant中所有的类型共用这块buffer,而any是通过派生,在堆内存上创建一个实际的对象,并且每次赋值都会析构之前的对象,重新在堆内存上重新创建一个新对象。

所以从性能上说variant的效率是高于any的,二者效率大约相差4倍左右。 附一个测试代码:

所以能用variant就不用any,除非真的需要擦除所有的类型,或者是为了追求代码更简单不太在意性能的时候可以用any,大部分情况下用variant擦除部分类型就可以满足需求了。

any的一个典型应用场景就是http服务器中的session,这个session是保存用户在服务器端的数据的,这个数据类型可能是字符串,数值,或者用户自定义的对象,在这种情况下无法定义一个variant,这时候用any是合适的。

tf.variant的实现

tf.variant的实现思路

tf.variant的实现思路实际上就是any的实现思路,any对象内部有一个内部基类的unique_ptr,这个基类有一个带模版参数的派生类。

这个派生模版类是泛型的,用它代表所有类型。

当给any对象赋值的时候,我们就创建这个派生类对象,这个对象保存了实际的类型。

在后面取值的时候我们就可以将这个基类向下转型为某个具体的派生类对象了,这时需要判断当前保存的对象类型和传入的类型是否一致,不一致则返回nullptr。

any

tf.variant的源码实现

1.内部定义一个抽象类和一个派生模版类

2.定义一个成员变量,内部接口类的指针–std::unique_ptr< ValueInterface > value_;

3.赋值时创建派生类对象

赋值的时候对赋值类型做了限定,必须为非Variant的,并且有拷贝构造函的类型,因为any内部会重新创建这个对象,之后赋值给基类指针value_,同时实现了类型擦除。

4.取值

取值的时候会判断是否初始化以及类型是否匹配,满足条件就转成实际的类型,否则返回nullptr。

这个代码的实现还是比较简单的,就是实现了一个基本的any语义,还有一些自己扩展的部分,比如encode和decode之类的。

总结

tf.variant本质上就是一个any,用来做类型擦除,像c#,java中的object那样代表一个通用类型。

在tensorflow中很多时候是作为一个函数参数,有些这样代码:

目前还没进一步分析tf的代码,我猜测这样做的目的可能是为了统一的接口以及灵活性,接口可以保持通用不变,具体实现将因具体类型不同而各有不同,方便扩展。

variant原理和应用

variant原理和应用

variant语义

variant是一个泛化的、类型安全的union。可以保存类型不同的对象,它的特点是多类型单值。

基本用法

以c++17中的variant为例(boost中的variant和标准库的用法几乎一样),我们定义一个这种的variant:

std::variant<int, double, char> v;

这个variant可以用来存储int, double和char三种类型的数据,所以我们可以这样赋值:

可以看到类型的值可以赋给同一个variant,如果将一个非int, double和char的类型赋值给v的话则在会出现一个编译期错误,所以variant是类型安全的,variant只允许将定义时指定的那些类型赋值给它。注意,重新赋值的时候之前的对象会自动析构掉。

接下来看如何取值:

通过std::get(v)就可以获取对应的值了,不过取值的时候需要知道当前的variant具体是什么类型,如果类型不对则会抛异常。

这种需要传具体类型的访问variant的方法有局限性,有时候我们不能确切知道当前的具体类型,这种情况下该如何取值呢?
以boost库提供的访问vairant方法为例:

这种访问方法需要定义一个函数对象对每个类型的访问都写一个重载函数,如果找不到对应的重载函数则会报一个编译期错误。

这种方法更常用,因为它不需要知道具体的类型。

还可以用C++11写一个支持lambda的visitor,避免写一个函数对象,用起来可以更简单。

实现原理

虽然很多库自己实现了variant,但其实现原理是类似的,实现variant主要分为下面几步:

1.variant内部定义一个足够大的缓冲区。

足够大的意思是这个缓冲区可以用来创建variant定义时的那些类型,具体多大呢,需要借助模版元方法遍历所有的类型找出其中size最大的那个类型,找到这个最大的size之后我们就可以定义一个栈上的char数组了,这里还需要考虑内存对齐的问题。

2.通过placement new创建某一个类型的对象。

这个对象就在内部的栈上缓冲区中创建的,所有的类型共用这块缓冲区,和union类似。具体创建对象的时候用的是placement new,一个是出于性能考虑,一个出于便于回收之前的对象考虑,因为重新赋值的时候需要将之前的对象析构掉。

3.variant赋值。

赋值时需要保存当前类型的type_index, 便于后面取值的时候判断需要取值的类型和当前类型是否一致。

4.variant的析构。

析构的时候要根据type_index遍历所有的类型找到当前的类型然后调用该类型的析构函数去析构。

通过这个思路就可以实现一个基本的variant,如果需要支持更多功能的时候还需要增加一些代码,比如支持嵌套的variant之类,支持lambda访问等等。

具体的实现可以参考我的github上C++11实现的基本功能的variant:https://github.com/qicosmos/cosmos/blob/master/Variant.hpp.

应用场景

我们一般在什么场景下需要用到variant呢?varaint一般用于类型擦除,比如我们需要访问一个某个值,这个值的类型是有限个的,这时可以用variant来表示这个“可能是多个类型的值”,避免针对具体类型去写代码,可以消除强制转换,或者把复杂问题简化,比如一些异构类型没办法抽象泛化的时候,用variant泛化是很方便的。

一个典型的应用场景就是数据库表访问的场景,数据库表字段的类型是有限多个的,我们完全可以用一个variant来表示数据库字段,接着就可以像通用类型那样去访问数据表字段了,而不必关注具体的类型,把一个复杂问题变得很简单了。

如何引入到我们的工程

boost库中已经有variant了,c++17中也引入了,如果我们的编译器还只支持C++11,则可以直接用boost中的variant,boost中的variant是header only的,直接包含头文件既可以,这里涉及到另外一个问题就是如何引入boost库。

boost库是一个广泛使用具有工业强度的c++库,很多C++新标准里的新特性都是来自于boost,使用里面已经有的库,可以避免重复造轮子,节省开发测试时间,而且质量是有保证的。

那么如何引入boost呢,引入boost有两种方式:

1.直接安装

可以查看当前源中有哪个boost库, 然后直接安装:

安装之后直接在工程中引用boost头文件既可以,以variant为例,使用variant时直接include即可:

2.编译安装

从boost.org官网下载一个版本,解压之后在boost目录执行两个命令就可以了

总结

variant作为一个类型安全的union,可以帮助我们做类型擦除,从而可以避免强制转换,便于编写更加泛化通用的代码,化繁为简。