這文章主要解釋了表達式模板的工做原理(也是mshadow的主要原理),文章的前半部分是翻譯自exp-template/README.md。咱們會解釋它爲何會影響編譯代碼的性能,表達式模板也是C++
矩陣運算庫的用到的主要技巧,好比Eigen,GSL,boost.uBLAS。html
在開始以前,咱們先考一個問題,假如更新的規則以下:(這裏是爲了達到解釋的目的,一般更新規則是這樣的:weight += - eta * (grad + lambda * weight)
)c++
weight = - eta * (grad + lambda * weight);
這裏權重與梯度都是長度爲n
的向量。當你選擇C++
做爲你的編程語言時,我想你主要考慮是效率。下面這個很重要而且用在大多數C/C++
程序中:git
這裏是一個例子:github
void UpdateWeight (const float *grad, float eta, float lambda, int n, float *weight) { for (int i = 0; i < n; ++i) { weight[i] = - eta * (grad[i] + lambda * weight[i]); } }
這個函數用了預先分配的梯度、權重空間來計算,寫這樣的一個函數十分簡單,然而當咱們要重複這樣寫的時候會十分煩惱。因此問題是若是咱們寫成如下的樣子,能獲得和上面代碼同樣的性能嗎?算法
void UpdateWeight (const Vec& grad, float eta, float lambda, Vec& weight) { weight = -eta * (grad + lambda * weight); }
答案是能夠的,但這不是最顯然的答案。express
讓咱們首先來看一下最直接的解決方法:運算符重載。編程
// Naive solution for vector operation overloading struct Vec { int len; float* dptr; Vec(int len) : len(len) { dptr = new float[len]; } Vec(const Vec& src) : len(src.len) { dptr = new float[len]; memcpy(dptr, src.dptr, sizeof(float)*len ); } ~Vec(void) { delete [] dptr; } }; inline Vec operator+(const Vec &lhs, const Vec &rhs) { Vec res(lhs.len); for (int i = 0; i < lhs.len; ++i) { res.dptr[i] = lhs.dptr[i] + rhs.dptr[i]; } return res; }
若是咱們用一樣的方式增長更多的運算符重載,咱們就能夠獲得咱們的想要的直接寫等式而不是循環的方法。然而,這種方法明顯是低效的,由於中間有臨時內存被分配與釋放,因此咱們能作得更好。數組
有一種更高效的選擇是:咱們能夠僅重載運算符+=,-=
,這兩個運算符是不用分配臨時內存的,可是這會限制咱們寫等式。等會咱們將會討論爲何咱們須要表達式模板,雖然C++11
在本教程的末尾提供了移動賦值運算符和右值引用。數據結構
在作運算符+
時,爲何咱們要分配臨時內存呢?這是由於咱們不知道將在運算符+
中分配的目標,不然咱們能夠直接將結果存入到目標中,而不是放在臨時變量中。app
可是若是咱們知道目標呢?這個結代碼的實如今exp_lazy.cpp中:
// Example Lazy evaluation code // for simplicity, we use struct and make all members public #include <cstdio> struct Vec; // expression structure holds the expression struct BinaryAddExp { const Vec &lhs; const Vec &rhs; BinaryAddExp(const Vec &lhs, const Vec &rhs) : lhs(lhs), rhs(rhs) {} }; // no constructor and destructor to allocate and de-allocate memory, // allocation done by user struct Vec { int len; float* dptr; Vec(void) {} Vec(float *dptr, int len) : len(len), dptr(dptr) {} // here is where evaluation happens inline Vec &operator=(const BinaryAddExp &src) { for (int i = 0; i < len; ++i) { dptr[i] = src.lhs.dptr[i] + src.rhs.dptr[i]; } return *this; } }; // no evaluation happens here inline BinaryAddExp operator+(const Vec &lhs, const Vec &rhs) { return BinaryAddExp(lhs, rhs); } const int n = 3; int main(void) { float sa[n] = {1, 2, 3}; float sb[n] = {2, 3, 4}; float sc[n] = {3, 4, 5}; Vec A(sa, n), B(sb, n), C(sc, n); // run expression A = B + C; for (int i = 0; i < n; ++i) { printf("%d:%f==%f+%f\n", i, A.dptr[i], B.dptr[i], C.dptr[i]); } return 0; }
咱們實現的思想是在運算符+
並無直接的計算,而是返回一個表達的對象(像抽象語法樹),當咱們重載運算符=
時,咱們就能夠知道目標和全部的操做時,這樣咱們就能夠直接計算並且不須要臨時變量。一樣地,咱們定義DotDxp
和在運算符=
上定義延遲計算,並將矩陣(向量)的乘法定向的BLAS庫上計算。
使用延遲計算,咱們能夠很好地避免了臨時變量的分配,可是代碼的擴展能力被限制了:
A=B+C
,不能寫出更出的表達式了。=
來計算每個等式。這裏咱們實現了一個魔法模板程序來解決這兩個問題,代碼(exp_template.cpp)以下,代碼雖然有點長,但能夠容許你寫更多的等式。
// Example code, expression template, and more length equations // for simplicity, we use struct and make all members public #include <cstdio> // this is expression, all expressions must inheritate it, // and put their type in subtype template<typename SubType> struct Exp { // returns const reference of the actual type of this expression inline const SubType& self(void) const { return *static_cast<const SubType*>(this); } }; // binary add expression // note how it is inheritates from Exp // and put its own type into the template argument template<typename TLhs, typename TRhs> struct BinaryAddExp: public Exp<BinaryAddExp<TLhs, TRhs> > { const TLhs &lhs; const TRhs &rhs; BinaryAddExp(const TLhs& lhs, const TRhs& rhs) : lhs(lhs), rhs(rhs) {} // evaluation function, evaluate this expression at position i inline float Eval(int i) const { return lhs.Eval(i) + rhs.Eval(i); } }; // no constructor and destructor to allocate // and de-allocate memory, allocation done by user struct Vec: public Exp<Vec> { int len; float* dptr; Vec(void) {} Vec(float *dptr, int len) :len(len), dptr(dptr) {} // here is where evaluation happens template<typename EType> inline Vec& operator= (const Exp<EType>& src_) { const EType &src = src_.self(); for (int i = 0; i < len; ++i) { dptr[i] = src.Eval(i); } return *this; } // evaluation function, evaluate this expression at position i inline float Eval(int i) const { return dptr[i]; } }; // template add, works for any expressions template<typename TLhs, typename TRhs> inline BinaryAddExp<TLhs, TRhs> operator+(const Exp<TLhs> &lhs, const Exp<TRhs> &rhs) { return BinaryAddExp<TLhs, TRhs>(lhs.self(), rhs.self()); } const int n = 3; int main(void) { float sa[n] = {1, 2, 3}; float sb[n] = {2, 3, 4}; float sc[n] = {3, 4, 5}; Vec A(sa, n), B(sb, n), C(sc, n); // run expression, this expression is longer:) A = B + C + C; for (int i = 0; i < n; ++i) { printf("%d:%f == %f + %f + %f\n", i, A.dptr[i], B.dptr[i], C.dptr[i], C.dptr[i]); } return 0; }
關鍵的思想是模板Exp<SubType>
將派生的類做爲模板參數,這樣就能夠將這個模板的自身經過self()
轉換成SubTpye
(就是派生類)。BinaryAddExp
如今是一個模板類,能夠將表達式複合在一塊兒,就像一個複合模式的模板版本同樣。計算經過函數Eval完成,它在BinaryAddExp
中以遞歸的方式完成。
=
調用src.Eval(i)
會在編譯時被編譯成B.dptr[i] + C.dptr[i] + C.dptr[i]
。經過上面的例子,模板編程編譯時能夠強大地更程序更加靈活,最後的例子比較接近mshadow了,能夠請容許用戶使用雙目運算符(exp_template_op.cpp)。
// Example code, expression template // with binary operator definition and extension // for simplicity, we use struct and make all members public #include <cstdio> // this is expression, all expressions must inheritate it, // and put their type in subtype template<typename SubType> struct Exp{ // returns const reference of the actual type of this expression inline const SubType& self(void) const { return *static_cast<const SubType*>(this); } }; // binary operators struct mul{ inline static float Map(float a, float b) { return a * b; } }; // binary add expression // note how it is inheritates from Exp // and put its own type into the template argument template<typename OP, typename TLhs, typename TRhs> struct BinaryMapExp: public Exp<BinaryMapExp<OP, TLhs, TRhs> >{ const TLhs& lhs; const TRhs& rhs; BinaryMapExp(const TLhs& lhs, const TRhs& rhs) :lhs(lhs), rhs(rhs) {} // evaluation function, evaluate this expression at position i inline float Eval(int i) const { return OP::Map(lhs.Eval(i), rhs.Eval(i)); } }; // no constructor and destructor to allocate and de-allocate memory // allocation done by user struct Vec: public Exp<Vec>{ int len; float* dptr; Vec(void) {} Vec(float *dptr, int len) : len(len), dptr(dptr) {} // here is where evaluation happens template<typename EType> inline Vec& operator=(const Exp<EType>& src_) { const EType &src = src_.self(); for (int i = 0; i < len; ++i) { dptr[i] = src.Eval(i); } return *this; } // evaluation function, evaluate this expression at position i inline float Eval(int i) const { return dptr[i]; } }; // template binary operation, works for any expressions template<typename OP, typename TLhs, typename TRhs> inline BinaryMapExp<OP, TLhs, TRhs> F(const Exp<TLhs>& lhs, const Exp<TRhs>& rhs) { return BinaryMapExp<OP, TLhs, TRhs>(lhs.self(), rhs.self()); } template<typename TLhs, typename TRhs> inline BinaryMapExp<mul, TLhs, TRhs> operator*(const Exp<TLhs>& lhs, const Exp<TRhs>& rhs) { return F<mul>(lhs, rhs); } // user defined operation struct maximum{ inline static float Map(float a, float b) { return a > b ? a : b; } }; const int n = 3; int main(void) { float sa[n] = {1, 2, 3}; float sb[n] = {2, 3, 4}; float sc[n] = {3, 4, 5}; Vec A(sa, n), B(sb, n), C(sc, n); // run expression, this expression is longer:) A = B * F<maximum>(C, B); for (int i = 0; i < n; ++i) { printf("%d:%f == %f * max(%f, %f)\n", i, A.dptr[i], B.dptr[i], C.dptr[i], B.dptr[i]); } return 0; }
到這裏爲止,你應該明白它工做的基本思想:
因此在編寫機器學習代碼時寫表達式,並將精力集中在重要的算法部分上。
在Mshadow的表達式模板用到的上面咱們介紹的關鍵思想,但有幾個小的不一樣點:
Plan
類用來替代Exp
類的計算函數Eval
,用來計算結果。Plan
中放置較少的變量,例如,當咱們評估數據時,咱們不須要數組長度。A = dot(B.T(), C)
的運算,一樣延遲表達是不須要分配臨時內存的。C++11
:在C ++ 11
中,移動構造函數能夠用來保存重複的分配內存,這樣就省去了一些須要的表達模板。而後,仍然要分配最少一次的空間。
dst = A+B+C
,dst
並無包括賦值前所分配的空間。Mshadow採用了表達式模板加強了c++矩陣庫的性能,四個主要的數據類型的繼承關係以下:
Tensor --> TRValue --> RValueExp --> Exp
能夠看到基類是Exp,除了一些基本的數據類型(如int
、float
等),其它的數據類型都是繼承於Exp,Exp的設計特殊之處在於能夠將它的派生類做爲模板參數,這樣就能夠將這個模板的自身經過self()
(返回的是一個不可修改的實例)或者ptrself()
(返回的是一個可修改的指針)轉換成SubTpye
(就是派生類)
template<typename SubType, typename DType, int exp_type> struct Exp { public: /*! \return subtype instance of current class */ inline const SubType& self(void) const { return *static_cast<const SubType*>(this); } /*! \return reference of subtype instance of current class */ inline SubType* ptrself(void) { return static_cast<SubType*>(this); } };
RValueExp
僅定義了一些賦值函數、重載運算符等,要注意的是它將表達式的類型寫成默認的kRValue=0,後面全部的數據定義時表達類型都是這個,真正改變表達式類型的是運算符,好比dot
、一些重載的運算符。
template<typename Container, typename DType> class RValueExp: public Exp<Container, DType, type::kRValue> { //... }
Eval(i,j)
中所定義,這個能從輸入的表達式中抽出結果(i,j)
並和輸出到特定位置中。dot
。namespace type { // type expression type are defined as bitmask // subtype relationshop kRValue < kMapper < kMapper < kComplex /*! * \brief this expression directly correspnds to a data class, * can be used to assign data */ const int kRValue = 0; /*! * \brief expression contains element-wise tensor operations, * map a expression to same shape */ const int kMapper = 1; /*! * \brief expression that can be chained with other expressiones * Usually it have function Eval(i,j) defined, which pulls the result (i, j) from input * expression and output the result at certain position. */ const int kChainer = 3; /*! \brief othercase: e.g dot product */ const int kComplex = 7; } // namespace type
TRValue
並無什麼實現的內容,但它是全部可能Teson
的超類:
template<typename Container, typename Device, int dimension, typename DType> struct TRValue: public expr::RValueExp<Container, DType> { };
Tensor
是咱們的計算基本數據類型,在mshadow中,除了基本類型,其它數據結果最終於都要在回到Tensor
中計算,包括繼承它了類TensorContainer
和用於圖模型中更抽象靈活的數據結構TBlob
。
template<typename Device, int dimension, typename DType MSHADOW_DEFAULT_DTYPE> struct Tensor: public TRValue<Tensor<Device, dimension, DType>, Device, dimension, DType> { public: //-------------------------------- // struct memembers //-------------------------------- /*! \brief whether current type lies in cpu */ static const bool kDevCPU = Device::kDevCPU; /*! \brief dimension of subtype */ static const int kSubdim = dimension - 1; //-------------------------------- // struct memembers //-------------------------------- /*! \brief pointer to the data */ DType *dptr_; /*! \brief shape of the tensor */ Shape<dimension> shape_; /*! * \brief storing the stride information in x dimension * this is used to deal with pitch allocation in gpu or sse(align x dimension to 64bit) for efficiency */ index_t stride_; /*! * \brief stream where the computation lies * stream is a device dependency concept where each computation */ Stream<Device> *stream_; //-------------------------------- // functions //-------------------------------- ... }
TensorContainer
繼承自Tensor
,最大的區別是TensorContainer
在建立對象的時候會分配相應的空間,使用時相對比較方便。
template<typename Device, int dimension, typename DType = default_real_t> class TensorContainer: public Tensor<Device, dimension, DType> { }
對於Tensor
來講,它的Shape
是不能改變的,但對於深度學習的圖模型來講,這顯然是不合適的。爲了適應這個需求,設計了用於圖模型中更抽象靈活的數據結構TBlob
。經過TBlob
的成員函數能獲取它在儲存的數據並轉於相應的Tensor
。
class TBlob { public: /*! \brief pointer to the data */ void *dptr_; /*! \brief shape of the tensor */ TShape shape_; /*! * \brief storing the stride information in x dimension */ index_t stride_; /*! \brief device mask of the corresponding device */ int dev_mask_; /*! \brief type flag of the tensor blob */ int type_flag_; ... }
表達式和對應的計算是分開的,並且用了延遲計算的思想,到賦值運算時會一塊兒計算。新構建一個運算符的表達式有以下步驟:
Exp
,內建構造函數存儲相應的數據。Plan
類,生成表達類的Plan
對象,內建Eval
函數計算Plan
對象的值。MakePlan
類,用來生成該表達類的Plan
類。ExpInfo
類,用來存儲該表達類的相關信息。下面咱們用basic.cpp內的一段函數來講明上面的過程及原理:
40 TensorContainer<cpu, 2> lhs(Shape2(2, 3)), rhs(Shape2(2, 3)), ret(Shape2(2,2)); 41 lhs = 1.0; 42 rhs = 1.0; 43 ret = implicit_dot(lhs, rhs.T());
TensorContainer
變量分配了相應的空間。lhs = 1.0
爲例說明(過程有省略,由於調用堆棧比較深)。
1
已是一個完整的對象了,不會再有操做,因此直接調用賦值運算符=
=
兩邊的變量,左邊個是經常使用的類型double
或者float
(看編譯器),右邊是TensorContainer
對象,因此調用Tensor_container.cpp中的函數:inline Tensor<Device, dimension, DType> &operator=(DType s) { return this->__assign(s); }
__assign
在父類RValueExp
(在文件Expresion.h)中定義了,查看數據類型,調用的是如下函數,其中saveto是一個運算符(也能夠說成操做符)。要注意的是,這個函數內有一個操做scalar<DType>(s)
,這個操做將類型從DTpye
轉變成ScalarExp<Dtype>
,並且運算符的類型變成了KMapper
。另外,this->ptrself()
指的是lhs
,scalar<DType>(s)
則是1
。inline Container &__assign(DType s) { ExpEngine<sv::saveto, Container, DType>::Eval(this->ptrself(), scalar<DType>(s)); return *(this->ptrself()); }
saveto
:template<typename E> inline static void Eval(RV *dst, const Exp<E, DType, type::kMapper> &exp) { MapExp<SV>(dst, exp); }
MapExp
、Map
函數:template<typename Saver, typename R, int dim, typename DType, typename E, int etype> inline void MapExp(TRValue<R, cpu, dim, DType> *dst, const expr::Exp<E, DType, etype> &exp) { ... MapExpCPUEngine<expr::PacketCheck<E, MSHADOW_DEFAULT_PACKET>::kPass,Saver, R, dim, DType, E, etype> ::Map(dst->ptrself(), exp); } template<bool pass_check, typename Saver, typename R, int dim, typename DType, typename E, int etype> struct MapExpCPUEngine { inline static void Map(TRValue<R, cpu, dim, DType> *dst, const expr::Exp<E, DType, etype> &exp) { MapPlan<Saver>(dst, MakePlan(exp.self())); } };
MapPlan
有兩次調用,先是調用裏面的MapPlan
,函數在Expr_engine-inl.h中,而且獲得Scalar<DType>
對象的Plan
對象。而後再調用Tensor_cpu-inl.h的MapPlan
template<typename DType> inline Plan<ScalarExp<DType>, DType> MakePlan(const ScalarExp<DType> &e) { return Plan<ScalarExp<DType>, DType>(e.scalar_); } template<typename Saver, typename R, int dim, typename DType, typename E> inline void MapPlan(TRValue<R, cpu, dim, DType> *dst, const expr::Plan<E, DType> &plan) { Shape<2> shape = expr::ShapeCheck<dim, R>::Check(dst->self()).FlatTo2D(); expr::Plan<R, DType> dplan = expr::MakePlan(dst->self()); #if (MSHADOW_USE_CUDA == 0) #pragma omp parallel for #endif // temp remove openmp, as default setting throttles CPU for (openmp_index_t y = 0; y < shape[0]; ++y) { for (index_t x = 0; x < shape[1]; ++x) { // trust your compiler! -_- they will optimize it Saver::template Save<DType>(dplan.REval(y, x), plan.Eval(y, x)); } } }
plan.Eval(y, x)
與dplan.REval(y, x)
,兩個函數都在Expr_engine-inl.h中:// scalar template<typename DType> class Plan<ScalarExp<DType>, DType> { public: explicit Plan(DType scalar) : scalar_(scalar) {} MSHADOW_XINLINE DType Eval(index_t y, index_t x) const { return scalar_; } private: DType scalar_; }; // tensor plan template <typename Device, int dim, typename DType> class Plan<Tensor<Device, dim, DType>, DType> { public: explicit Plan(const Tensor<Device, dim, DType> &t) : dptr_(t.dptr_), stride_(t.stride_) {} // for RValue, the return type should be reference MSHADOW_XINLINE DType &REval(index_t y, index_t x) { return dptr_[y * stride_ + x]; } // const evaluation MSHADOW_XINLINE const DType &Eval(index_t y, index_t x) const { return dptr_[y * stride_ + x]; } private: DType *dptr_; index_t stride_; };
Saver::template Save<DType>(dplan.REval(y, x), plan.Eval(y, x))
,這個Save操做就是咱們以前的saveto
,調用在Base.h函數:/*! \brief save to saver: = */ struct saveto { /*! \brief save b to a using save method */ template<typename DType> MSHADOW_XINLINE static void Save(DType &a, DType b) { // NOLINT(*) a = b; } ... };
到這裏,基本的賦值操做就完成了,你會發現用表達式模板是十不直接的操做。
ret = implicit_dot(lhs, rhs.T())
:implicit_dot
能夠當作一個獨立的操做符,若是想寫一個新的操做符,能夠參考Implicit_gemn.h。這個表達式複雜的緣由是有三個運算符——.T()
、implicit_dot
和ret
,要用到遞歸運算了。不管表達式多麼的複雜,咱們只要記住,除了賦值運算(其實它也沒有Plan
類)外一個表達式的結果能夠用Plan.Eval
來得到的。下面是一些這行代碼運算的基本過程:
rhs.T()
這個操做在Expression.h中,返回一個TransposeExp
表達式(注意的是inline
的函數會在編譯中展開,這裏爲了方便理解,只是說了調用)。這裏要注意的是TransposeExp
表達式的類型是kChainer
。inline const TransposeExp<Container, DType> T(void) const { return TransposeExp<Container, DType>(this->self()); }
implicit_dot(lhs, rhs.T())
函數在Implicit_gemn.h中,返回一個ImplicitGEMMExp
表達式。這裏要注意的是implicit_dot
表達式的類型是kChainer
。template<typename LhsExp, typename RhsExp, typename DType, int e1, int e2> inline ImplicitGEMMExp<LhsExp, RhsExp, DType> implicit_dot(const Exp<LhsExp, DType, e1> &lhs, const Exp<RhsExp, DType, e2> &rhs) { TypeCheckPass<ExpInfo<LhsExp>::kDim == 2 && ExpInfo<RhsExp>::kDim == 2> ::Error_Expression_Does_Not_Meet_Dimension_Req(); return ImplicitGEMMExp<LhsExp, RhsExp, DType>(lhs.self(), rhs.self()); }
=
與上面因此的步驟有相同之處,只是不一樣的類型,調用的重載函數是不同的。上面的調用的第一個MapPlan
是調用Implicit_gemn.h中的,返回的是一個ImplicitGEMMExp
的Plan
類,第二個則是同樣的。template<typename LhsExp, typename RhsExp, typename DType> inline Plan<ImplicitGEMMExp<LhsExp, RhsExp, DType>, DType> MakePlan(const ImplicitGEMMExp<LhsExp, RhsExp, DType> &exp) { return Plan<ImplicitGEMMExp<LhsExp, RhsExp, DType>, DType>(exp); }
Saver::template Save<DType>(dplan.REval(y, x), plan.Eval(y, x))
時,對於dplan.REval(y, x)
和Save
咱們已經說過了,此次要說的是Plan.Eval(y, x)
,來看下是若是進行遞歸調用的。這個函數在Implicit_gemn.h中:template<typename LhsExp, typename RhsExp, typename DType> struct Plan<ImplicitGEMMExp<LhsExp, RhsExp, DType>, DType> { public: explicit Plan(const ImplicitGEMMExp<LhsExp, RhsExp, DType> &e) : lhs_(MakePlan(e.lhs_)), rhs_(MakePlan(e.rhs_)), prod_size_(e.prod_size_), prod_size_lower_align_(packet::LowerAlign<DType, MSHADOW_DEFAULT_PACKET>(e.prod_size_)) { } MSHADOW_XINLINE DType Eval(index_t y, index_t x) const { typedef packet::Packet<DType> Packet; Packet sum = Packet::Fill(0); const size_t packetSize = Packet::Size(); DType lhs_temp[packetSize], rhs_temp[packetSize]; for (index_t i = 0; i < prod_size_lower_align_; i += packetSize) { // unroll for (index_t j = 0; j < packetSize; ++j) { lhs_temp[j] = lhs_.Eval(y, i + j); } for (index_t j = 0; j < packetSize; ++j) { rhs_temp[j] = rhs_.Eval(i + j, x); } sum = sum + Packet::LoadUnAligned(lhs_temp) * Packet::LoadUnAligned(rhs_temp); } DType ret_result = sum.Sum(); for (index_t i = prod_size_lower_align_; i < prod_size_; ++i) { ret_result += lhs_.Eval(y, i) * rhs_.Eval(i, x); } return ret_result; } private: expr::Plan<LhsExp, DType> lhs_; expr::Plan<RhsExp, DType> rhs_; const index_t prod_size_; const index_t prod_size_lower_align_; };在進行計算中,要獲得
lhs_
與rhs_
表達式的值,而這兩個表達式也是Plan
類,以前咱們說過:只要記得Plan.Eval
是獲得表達式的值就好了。因此當調用rhs_.Eval
一直遞歸到計算獲得值爲止,因此rhs_.Eval
最後獲得的ret = implicit_dot(lhs, rhs.T())
中rhs.T()
的值。
對於傳統的一些操做符+
、-
、*
、/
等,會被統一到TernaryMapExp
(三目)、BinaryMapExp
(雙目)、UnaryMapExp
(單目)中,參考加文件Expression.h,它的相關類MakePlan
與Plan
則定義在Exp_engine-inl.h。
gpu
的編程有它的一些限制,但設計的類和上而是差的多的。Tensor
)讀寫。【防止爬蟲轉載而致使的格式問題——連接】:http://www.cnblogs.com/heguanyou/p/7545344.html