खोज…


तत्व-वार बीजीय अभिव्यक्तियों पर मूल अभिव्यक्ति टेम्पलेट


परिचय और प्रेरणा


अभिव्यक्ति टेम्पलेट (निम्नलिखित में ईटी के रूप में चिह्नित) एक शक्तिशाली टेम्पलेट मेटा-प्रोग्रामिंग तकनीक है, जिसका उपयोग कभी-कभी काफी महंगी अभिव्यक्तियों की गणना करने के लिए किया जाता है। यह विभिन्न डोमेन में व्यापक रूप से उपयोग किया जाता है, उदाहरण के लिए रैखिक बीजगणित पुस्तकालयों के कार्यान्वयन में।

इस उदाहरण के लिए, रैखिक बीजगणितीय संगणना के संदर्भ पर विचार करें। अधिक विशेष रूप से, गणना केवल तत्व-वार संचालन शामिल है । इस तरह की संगणना ईटी के सबसे बुनियादी अनुप्रयोग हैं, और वे एक अच्छे परिचय के रूप में कार्य करते हैं कि ईटी आंतरिक रूप से कैसे काम करता है।

आइए एक प्रेरक उदाहरण देखें। अभिव्यक्ति की गणना पर विचार करें:

Vector vec_1, vec_2, vec_3;

// Initializing vec_1, vec_2 and vec_3.

Vector result = vec_1 + vec_2*vec_3;

यहाँ सादगी के लिए, मैं यह मानूँगा कि वर्ग Vector और ऑपरेशन + (वेक्टर प्लस: एलिमेंट-वाइज प्लस ऑपरेशन) और ऑपरेशन * (यहाँ वेक्टर आंतरिक उत्पाद: तत्व-वार ऑपरेशन भी) दोनों को सही ढंग से लागू किया गया है, जैसा कि उन्हें गणितीय रूप से कैसा होना चाहिए।

ETs (या अन्य समान तकनीकों) का उपयोग किए बिना एक पारंपरिक कार्यान्वयन में, अंतिम result प्राप्त करने के लिए Vector इंस्टेंस के कम से कम पांच निर्माण होते हैं:

  1. vec_1 , vec_2 और vec_3 संगत तीन उदाहरण।
  2. एक अस्थायी Vector उदाहरण _tmp , _tmp = vec_2*vec_3; के परिणाम का प्रतिनिधित्व करता है _tmp = vec_2*vec_3;
  3. अंत में वापसी मूल्य अनुकूलन के उचित उपयोग के साथ, result में अंतिम result का निर्माण result = vec_1 + _tmp;

ईटी का उपयोग करने वाले कार्यान्वयन 2 में अस्थायी Vector _tmp के निर्माण को समाप्त कर सकते हैं, इस प्रकार Vector उदाहरणों के केवल चार निर्माणों को छोड़ सकते हैं। अधिक दिलचस्प बात, निम्नलिखित अभिव्यक्ति पर विचार करें जो अधिक जटिल है:

Vector result = vec_1 + (vec_2*vec3 + vec_1)*(vec_2 + vec_3*vec_1);

कुल मिलाकर Vector आवृत्तियों के चार निर्माण भी होंगे: vec_1, vec_2, vec_3 और result । दूसरे शब्दों में, इस उदाहरण में, जहां केवल तत्व-वार संचालन शामिल हैं , यह गारंटी है कि मध्यवर्ती गणना से कोई अस्थायी वस्तु नहीं बनाई जाएगी


ईटी कैसे काम करते हैं


मूल रूप से, किसी भी बीजीय संगणना के लिए ईटी में दो बिल्डिंग ब्लॉक होते हैं:

  1. शुद्ध बीजगणितीय अभिव्यक्तियाँ ( पीएई ): वे बीजगणितीय अभिव्यक्तियों के समीपता / सार हैं। एक शुद्ध बीजगणित वास्तविक गणना नहीं करता है, वे केवल गणना कार्य-प्रवाह के अमूर्त / मॉडलिंग हैं। एक पीएई इनपुट या किसी बीजीय अभिव्यक्तियों के आउटपुट का एक मॉडल हो सकता है। पीएई के उदाहरणों को अक्सर कॉपी करने के लिए सस्ता माना जाता है।
  2. आलसी मूल्यांकन : जो वास्तविक गणनाओं के कार्यान्वयन हैं। निम्नलिखित उदाहरण में, हम देखेंगे कि केवल तत्व-वार संचालन से संबंधित अभिव्यक्तियों के लिए, आलसी मूल्यांकन अंतिम परिणाम पर अनुक्रमित-पहुंच संचालन के अंदर वास्तविक संगणना को लागू कर सकते हैं, इस प्रकार मूल्यांकन-ऑन-डिमांड की एक योजना बनाते हैं: एक संगणना नहीं की जाती है केवल तभी यदि अंतिम परिणाम प्राप्त हो / के लिए कहा जाए।

तो, विशेष रूप से हम इस उदाहरण में ईटी को कैसे लागू करते हैं? चलो अब इसके माध्यम से चलते हैं।

हमेशा निम्नलिखित कोड स्निपेट पर विचार करें:

Vector vec_1, vec_2, vec_3;

// Initializing vec_1, vec_2 and vec_3.

Vector result = vec_1 + vec_2*vec_3;

परिणाम की गणना करने के लिए अभिव्यक्ति को दो उप-अभिव्यक्तियों में और विघटित किया जा सकता है:

  1. एक वेक्टर प्लस अभिव्यक्ति ( plus_expr के रूप में चिह्नित)
  2. एक वेक्टर आंतरिक उत्पाद अभिव्यक्ति ( इनरप्रोड_सेक्स के रूप में चिह्नित)।

ईटी क्या करते हैं:

  • प्रत्येक उप-अभिव्यक्ति को तुरंत गणना करने के बजाय, ETs एक ग्राफिकल संरचना का उपयोग करके पूरी अभिव्यक्ति को मॉडल करता है। ग्राफ में प्रत्येक नोड एक PAE का प्रतिनिधित्व करता है। नोड्स के किनारे का कनेक्शन वास्तविक गणना प्रवाह का प्रतिनिधित्व करता है। तो उपरोक्त अभिव्यक्ति के लिए, हम निम्नलिखित ग्राफ प्राप्त करते हैं:

           result = plus_expr( vec_1, innerprod_expr(vec_2, vec_3) )
              /   \
             /     \
            /       \
           /   innerprod_expr( vec_2, vec_3 )
          /         /  \
         /         /    \
        /         /      \
     vec_1     vec_2    vec_3
    
  • अंतिम गणना को चित्रण पदानुक्रम के माध्यम से देखते हुए कार्यान्वित किया जाता है : चूंकि हम केवल तत्व-वार संचालन से निपट रहे हैं, इसलिए result में प्रत्येक अनुक्रमित मूल्य की गणना स्वतंत्र रूप से की जा सकती है : result का अंतिम मूल्यांकन किसी तत्व के लिए किया जा सकता है। result के प्रत्येक तत्व का बुद्धिमान मूल्यांकन । दूसरे शब्दों में, result तत्व की गणना के बाद से, elem_res , vec_1 ( elem_1 ), vec_2 ( elem_2 ) और vec_3 ( elem_3 ) में संबंधित तत्वों का उपयोग करके व्यक्त किया जा सकता है:

    elem_res = elem_1 + elem_2*elem_3;
    

इसलिए मध्यवर्ती आंतरिक उत्पाद के परिणाम को संग्रहीत करने के लिए एक अस्थायी Vector बनाने की कोई आवश्यकता नहीं है: एक तत्व के लिए संपूर्ण गणना पूरी तरह से की जा सकती है, और अनुक्रमित-पहुंच ऑपरेशन के अंदर एन्कोड किया जा सकता है


यहां कार्रवाई में उदाहरण कोड हैं।


फ़ाइल vec.hh: std :: वेक्टर के लिए आवरण, जिसका उपयोग लॉग तब दिखाया जाता है जब एक निर्माण कहा जाता है।


#ifndef EXPR_VEC
# define EXPR_VEC

# include <vector>
# include <cassert>
# include <utility>
# include <iostream>
# include <algorithm>
# include <functional>

///
/// This is a wrapper for std::vector. It's only purpose is to print out a log when a
/// vector constructions in called.
/// It wraps the indexed access operator [] and the size() method, which are 
/// important for later ETs implementation.
///

// std::vector wrapper.
template<typename ScalarType> class Vector
{
public:
  explicit Vector() { std::cout << "ctor called.\n"; };
  explicit Vector(int size): _vec(size) { std::cout << "ctor called.\n"; };
  explicit Vector(const std::vector<ScalarType> &vec): _vec(vec)
  { std::cout << "ctor called.\n"; };
  
  Vector(const Vector<ScalarType> & vec): _vec{vec()}
  { std::cout << "copy ctor called.\n"; };
  Vector(Vector<ScalarType> && vec): _vec(std::move(vec()))
  { std::cout << "move ctor called.\n"; };

  Vector<ScalarType> & operator=(const Vector<ScalarType> &) = default;
  Vector<ScalarType> & operator=(Vector<ScalarType> &&) = default;

  decltype(auto) operator[](int indx) { return _vec[indx]; }
  decltype(auto) operator[](int indx) const { return _vec[indx]; }

  decltype(auto) operator()() & { return (_vec); };        
  decltype(auto) operator()() const & { return (_vec); };  
  Vector<ScalarType> && operator()() && { return std::move(*this); }

  int size() const { return _vec.size(); }
  
private:
  std::vector<ScalarType> _vec;
};

///
/// These are conventional overloads of operator + (the vector plus operation)
/// and operator * (the vector inner product operation) without using the expression
/// templates. They are later used for bench-marking purpose.
///

// + (vector plus) operator.
template<typename ScalarType>
auto operator+(const Vector<ScalarType> &lhs, const Vector<ScalarType> &rhs)
{
  assert(lhs().size() == rhs().size() &&
         "error: ops plus -> lhs and rhs size mismatch.");
  
  std::vector<ScalarType> _vec;
  _vec.resize(lhs().size());
  std::transform(std::cbegin(lhs()), std::cend(lhs()),
                 std::cbegin(rhs()), std::begin(_vec),
                 std::plus<>());
  return Vector<ScalarType>(std::move(_vec));
}

// * (vector inner product) operator.
template<typename ScalarType>
auto operator*(const Vector<ScalarType> &lhs, const Vector<ScalarType> &rhs)
{
  assert(lhs().size() == rhs().size() &&
         "error: ops multiplies -> lhs and rhs size mismatch.");
  
  std::vector<ScalarType> _vec;
  _vec.resize(lhs().size());
  std::transform(std::cbegin(lhs()), std::cend(lhs()),
                 std::cbegin(rhs()), std::begin(_vec),
                 std::multiplies<>());
  return Vector<ScalarType>(std::move(_vec));
}

#endif //!EXPR_VEC

फ़ाइल expr.hh: तत्व-वार संचालन (वेक्टर प्लस और वेक्टर आंतरिक उत्पाद) के लिए अभिव्यक्ति टेम्पलेट्स का कार्यान्वयन


इसे खंडों में विभाजित करते हैं।

  1. धारा 1 सभी भावों के लिए एक आधार वर्ग को लागू करता है। यह क्यूरेटिकली रिकरिंग टेम्प्लेट पैटर्न ( CRTP ) को नियोजित करता है
  2. धारा 2 पहले पीएई को लागू करता है: एक टर्मिनल , जो गणना के लिए वास्तविक इनपुट मूल्य वाले इनपुट डेटा संरचना का सिर्फ एक आवरण (कॉन्स्ट रेफरेंस) है।
  3. धारा 3 दूसरे पीएई को लागू करती है: बाइनरी_ओपरेशन , जो एक वर्ग टेम्पलेट है जिसे बाद में वेक्टर_प्लस और वेक्टर_इनरोड्रोड के लिए उपयोग किया जाता है। यह ऑपरेशन के प्रकार , बाएं हाथ की ओर PAE और दाएं हाथ की ओर PAE द्वारा पैरामीट्रिज्ड है। वास्तविक गणना अनुक्रमित-पहुंच ऑपरेटर में एन्कोडेड है।
  4. धारा 4 तत्व-वार ऑपरेशन के रूप में वेक्टर_प्लस और वेक्टर_इनरप्रोड संचालन को परिभाषित करता है। यह भी पीएई के लिए ऑपरेटर + और * ओवरलोड करता है: जैसे कि ये दोनों ऑपरेशन भी पीएई को वापस करते हैं।
#ifndef EXPR_EXPR
# define EXPR_EXPR
      

/// Fwd declaration.
template<typename> class Vector;

namespace expr
{


/// -----------------------------------------
///
/// Section 1.
///
/// The first section is a base class template for all kinds of expression. It         
/// employs the Curiously Recurring Template Pattern, which enables its instantiation 
/// to any kind of expression structure inheriting from it.
///
/// -----------------------------------------


  /// Base class for all expressions.
  template<typename Expr> class expr_base
  {
  public:
    const Expr& self() const { return static_cast<const Expr&>(*this); }
    Expr& self() { return static_cast<Expr&>(*this); }

  protected:
    explicit expr_base() {};
    int size() const { return self().size_impl(); }
    auto operator[](int indx) const { return self().at_impl(indx); }
    auto operator()() const { return self()(); };
  };
  

/// -----------------------------------------
///
/// The following section 2 & 3 are abstractions of pure algebraic expressions (PAE).
/// Any PAE can be converted to a real object instance using operator(): it is in 
/// this conversion process, where the real computations are done.

///
/// Section 2. Terminal
///
/// A terminal is an abstraction wrapping a const reference to the Vector data 
/// structure. It inherits from expr_base, therefore providing a unified interface
/// wrapping a Vector into a PAE.
///
/// It provides the size() method, indexed access through at_impl() and a conversion
/// to referenced object through () operator.
/// 
/// It might no be necessary for user defined data structures to have a terminal 
/// wrapper, since user defined structure can inherit expr_base, therefore eliminates
/// the need to provide such terminal wrapper. 
///
/// -----------------------------------------


  /// Generic wrapper for underlying data structure.
  template<typename DataType> class terminal: expr_base<terminal<DataType>>
  {
  public:
    using base_type = expr_base<terminal<DataType>>;
    using base_type::size;
    using base_type::operator[];
    friend base_type;
    
    explicit terminal(const DataType &val): _val(val) {}
    int size_impl() const { return _val.size(); };
    auto at_impl(int indx) const { return _val[indx]; };
    decltype(auto) operator()() const { return (_val); }
    
  private:
    const DataType &_val;
  };


/// -----------------------------------------
///
/// Section 3. Binary operation expression.
///
/// This is a PAE abstraction of any binary expression. Similarly it inherits from 
/// expr_base.
///
/// It provides the size() method, indexed access through at_impl() and a conversion
/// to referenced object through () operator. Each call to the at_impl() method is
/// a element wise computation.
/// 
/// -----------------------------------------


  /// Generic wrapper for binary operations (that are element-wise).
  template<typename Ops, typename lExpr, typename rExpr>
  class binary_ops: public expr_base<binary_ops<Ops,lExpr,rExpr>>
  {
  public:
    using base_type = expr_base<binary_ops<Ops,lExpr,rExpr>>;
    using base_type::size;
    using base_type::operator[];
    friend base_type;
    
    explicit binary_ops(const Ops &ops, const lExpr &lxpr, const rExpr &rxpr)
      : _ops(ops), _lxpr(lxpr), _rxpr(rxpr) {};
    int size_impl() const { return _lxpr.size(); };

    /// This does the element-wise computation for index indx.
    auto at_impl(int indx) const { return _ops(_lxpr[indx], _rxpr[indx]); };

    /// Conversion from arbitrary expr to concrete data type. It evaluates
    /// element-wise computations for all indices.
    template<typename DataType> operator DataType()
    {
      DataType _vec(size());
      for(int _ind = 0; _ind < _vec.size(); ++_ind)
        _vec[_ind] = (*this)[_ind];
      return _vec;
    }
    
  private: /// Ops and expr are assumed cheap to copy.
    Ops   _ops;
    lExpr _lxpr;
    rExpr _rxpr;
  };


/// -----------------------------------------
/// Section 4.
///
/// The following two structs defines algebraic operations on PAEs: here only vector 
/// plus and vector inner product are implemented. 
///
/// First, some element-wise operations are defined : in other words, vec_plus and 
/// vec_prod acts on elements in Vectors, but not whole Vectors. 
///
/// Then, operator + & * are overloaded on PAEs, such that: + & * operations on PAEs         
/// also return PAEs.
///
/// -----------------------------------------


  /// Element-wise plus operation.
  struct vec_plus_t
  {
    constexpr explicit vec_plus_t() = default; 
    template<typename LType, typename RType>
    auto operator()(const LType &lhs, const RType &rhs) const
    { return lhs+rhs; }
  };
  
  /// Element-wise inner product operation.
  struct vec_prod_t
  {
    constexpr explicit vec_prod_t() = default; 
    template<typename LType, typename RType>
    auto operator()(const LType &lhs, const RType &rhs) const
    { return lhs*rhs; }
  };
  
  /// Constant plus and inner product operator objects.
  constexpr vec_plus_t vec_plus{};
  constexpr vec_prod_t vec_prod{};
  
  /// Plus operator overload on expressions: return binary expression.
  template<typename lExpr, typename rExpr>
  auto operator+(const lExpr &lhs, const rExpr &rhs)
  { return binary_ops<vec_plus_t,lExpr,rExpr>(vec_plus,lhs,rhs); }
  
  /// Inner prod operator overload on expressions: return binary expression.
  template<typename lExpr, typename rExpr>
  auto operator*(const lExpr &lhs, const rExpr &rhs)
  { return binary_ops<vec_prod_t,lExpr,rExpr>(vec_prod,lhs,rhs); }
  
} //!expr


#endif //!EXPR_EXPR

फ़ाइल main.cc: src फ़ाइल का परीक्षण करें


# include <chrono>
# include <iomanip>
# include <iostream>
# include "vec.hh"
# include "expr.hh"
# include "boost/core/demangle.hpp"


int main()
{
  using dtype = float;
  constexpr int size = 5e7;
  
  std::vector<dtype> _vec1(size);
  std::vector<dtype> _vec2(size);
  std::vector<dtype> _vec3(size);

  // ... Initialize vectors' contents.

  Vector<dtype> vec1(std::move(_vec1));
  Vector<dtype> vec2(std::move(_vec2));
  Vector<dtype> vec3(std::move(_vec3));

  unsigned long start_ms_no_ets =
    std::chrono::duration_cast<std::chrono::milliseconds>
    (std::chrono::system_clock::now().time_since_epoch()).count();
  std::cout << "\nNo-ETs evaluation starts.\n";
  
  Vector<dtype> result_no_ets = vec1 + (vec2*vec3);
  
  unsigned long stop_ms_no_ets =
    std::chrono::duration_cast<std::chrono::milliseconds>
    (std::chrono::system_clock::now().time_since_epoch()).count();
  std::cout << std::setprecision(6) << std::fixed
            << "No-ETs. Time eclapses: " << (stop_ms_no_ets-start_ms_no_ets)/1000.0
            << " s.\n" << std::endl;
  
  unsigned long start_ms_ets =
    std::chrono::duration_cast<std::chrono::milliseconds>
    (std::chrono::system_clock::now().time_since_epoch()).count();
  std::cout << "Evaluation using ETs starts.\n";
  
  expr::terminal<Vector<dtype>> vec4(vec1);
  expr::terminal<Vector<dtype>> vec5(vec2);
  expr::terminal<Vector<dtype>> vec6(vec3);
  
  Vector<dtype> result_ets = (vec4 + vec5*vec6);
  
  unsigned long stop_ms_ets =
    std::chrono::duration_cast<std::chrono::milliseconds>
    (std::chrono::system_clock::now().time_since_epoch()).count();
  std::cout << std::setprecision(6) << std::fixed
            << "With ETs. Time eclapses: " << (stop_ms_ets-start_ms_ets)/1000.0
            << " s.\n" << std::endl;
  
  auto ets_ret_type = (vec4 + vec5*vec6);
  std::cout << "\nETs result's type:\n";
  std::cout << boost::core::demangle( typeid(decltype(ets_ret_type)).name() ) << '\n'; 

  return 0;
}

जब जीसीसी 5.3 का उपयोग करते हुए -O3 -std=c++14 साथ संकलित किया जाता है तो यहां एक संभावित आउटपुट होता है:

ctor called.
ctor called.
ctor called.

No-ETs evaluation starts.
ctor called.
ctor called.
No-ETs. Time eclapses: 0.571000 s.

Evaluation using ETs starts.
ctor called.
With ETs. Time eclapses: 0.164000 s.


ETs result's type:
expr::binary_ops<expr::vec_plus_t, expr::terminal<Vector<float> >, expr::binary_ops<expr::vec_prod_t, expr::terminal<Vector<float> >, expr::terminal<Vector<float> > > >

अवलोकन हैं:

  • ETs का उपयोग इस मामले में महत्वपूर्ण प्रदर्शन को बढ़ावा देता है (> 3x)।
  • अस्थायी वेक्टर ऑब्जेक्ट का निर्माण समाप्त हो गया है। जैसा कि ETs मामले में, ctor को केवल एक बार कहा जाता है।
  • बूस्ट :: रूपांतरण से पहले ईटीएस रिटर्न के प्रकार की कल्पना करने के लिए डेमो का उपयोग किया गया था: यह स्पष्ट रूप से उसी अभिव्यक्ति ग्राफ का निर्माण करता है जो ऊपर दिखाया गया है।

ड्रा-बैक और कैविएट


  • ईटी का एक स्पष्ट नुकसान सीखने की अवस्था, कार्यान्वयन की जटिलता और कोड-रखरखाव कठिनाई है। उपरोक्त उदाहरण में, जहां केवल तत्व-वार संचालन पर विचार किया जाता है, कार्यान्वयन में पहले से ही बड़ी मात्रा में बॉयलरप्लेट शामिल हैं, वास्तविक दुनिया में अकेले जाने दें, जहां प्रत्येक गणना और तत्व-वार स्वतंत्रता में अधिक जटिल बीजीय अभिव्यक्तियाँ होती हैं, जो अब पकड़ में नहीं आती हैं (उदाहरण के लिए गुणन ), कठिनाई घातीय होगी।

  • टिकट उपयोग का एक और चेतावनी है कि वे के साथ अच्छी तरह से खेलते हैं auto कीवर्ड। जैसा कि ऊपर उल्लेख किया गया है, पीएई अनिवार्य रूप से परदे के पीछे हैं: और परदे के पीछे मूल रूप से auto साथ अच्छा नहीं खेलते हैं। निम्नलिखित उदाहरण पर विचार करें:

     auto result = ...;                // Some expensive expression: 
                                       // auto returns the expr graph, 
                                       // NOT the computed value.
     for(auto i = 0; i < 100; ++i)
         ScalrType value = result* ... // Some other expensive computations using result.
    

लूप के लिए प्रत्येक पुनरावृत्ति में, परिणाम का पुनर्मूल्यांकन किया जाएगा , क्योंकि लूप के लिए गणना किए गए मान के बजाय अभिव्यक्ति ग्राफ़ को पास किया गया है।


मौजूदा पुस्तकालय जो ईटी को लागू कर रहे हैं


  • बढ़ावा :: प्रोटो एक शक्तिशाली पुस्तकालय है जो आपको अपने स्वयं के नियमों और व्याकरण को अपने स्वयं के भावों के लिए परिभाषित करता है और ईटी का उपयोग करके निष्पादित करता है।
  • Eigen रैखिक बीजगणित के लिए एक पुस्तकालय है जो ETs का उपयोग करते हुए कुशलतापूर्वक विभिन्न बीजीय संगणनाओं को कार्यान्वित करता है।

एक मूल उदाहरण अभिव्यक्ति टेम्पलेट्स दिखाता है

एक अभिव्यक्ति टेम्पलेट एक संकलन-समय अनुकूलन तकनीक है जिसका उपयोग ज्यादातर वैज्ञानिक कंप्यूटिंग में किया जाता है। यह मुख्य उद्देश्य है अनावश्यक टेंपरेरी से बचना और एकल पास (आमतौर पर संख्यात्मक समुच्चय पर संचालन करते समय) का उपयोग करके लूप गणना को अनुकूलित करना। संख्यात्मक Array या Matrix प्रकारों को लागू करते समय भोले ऑपरेटर ओवरलोडिंग की अक्षमताओं को दरकिनार करने के लिए शुरू में अभिव्यक्ति टेम्पलेट्स तैयार किए गए थे। बज़्ने स्ट्रॉस्ट्रुप द्वारा अभिव्यक्ति टेम्पलेट्स के लिए एक समान शब्दावली पेश की गई है, जो उन्हें अपनी पुस्तक "सी ++ प्रोग्रामिंग लैंग्वेज" के नवीनतम संस्करण में "फ्यूज्ड संचालन" कहते हैं।

वास्तव में अभिव्यक्ति टेम्पलेट्स में गोता लगाने से पहले, आपको यह समझना चाहिए कि आपको पहली जगह में उनकी आवश्यकता क्यों है। इसे समझने के लिए, नीचे दिए गए बहुत ही सरल मैट्रिक्स वर्ग पर विचार करें:

template <typename T, std::size_t COL, std::size_t ROW>
class Matrix {
public:
    using value_type = T;

    Matrix() : values(COL * ROW) {}

    static size_t cols() { return COL; }
    static size_t rows() { return ROW; }

    const T& operator()(size_t x, size_t y) const { return values[y * COL + x]; }
    T& operator()(size_t x, size_t y) { return values[y * COL + x]; }

private:
    std::vector<T> values;
};

template <typename T, std::size_t COL, std::size_t ROW>
Matrix<T, COL, ROW>
operator+(const Matrix<T, COL, ROW>& lhs, const Matrix<T, COL, ROW>& rhs)
{
    Matrix<T, COL, ROW> result;

    for (size_t y = 0; y != lhs.rows(); ++y) {
        for (size_t x = 0; x != lhs.cols(); ++x) {
            result(x, y) = lhs(x, y) + rhs(x, y);
        }
    }
    return result;
}

पिछली कक्षा की परिभाषा को देखते हुए, अब आप मैट्रिक्स अभिव्यक्ति लिख सकते हैं जैसे:

const std::size_t cols = 2000;
const std::size_t rows = 1000;

Matrix<double, cols, rows> a, b, c;

// initialize a, b & c
for (std::size_t y = 0; y != rows; ++y) {
    for (std::size_t x = 0; x != cols; ++x) {
        a(x, y) = 1.0;
        b(x, y) = 2.0;
        c(x, y) = 3.0;
    }
}  

Matrix<double, cols, rows> d = a + b + c;  // d(x, y) = 6 

जैसा कि ऊपर दिखाया गया है, operator+() को अधिभार देने में सक्षम होने के नाते operator+() आपको एक अंकन प्रदान करता है जो परिपक्वता के लिए प्राकृतिक गणितीय संकेतन की नकल करता है।

दुर्भाग्य से, पिछले कार्यान्वयन समान "हाथ से तैयार किए गए" संस्करण की तुलना में अत्यधिक अक्षम है।

यह समझने के लिए कि, आपको क्या विचार करना है जब आप एक अभिव्यक्ति लिखते हैं जैसे कि Matrix d = a + b + c । यह वास्तव में ((a + b) + c) या operator+(operator+(a, b), c) फैलता है। दूसरे शब्दों में, operator+() अंदर लूप को दो बार निष्पादित किया जाता है, जबकि इसे आसानी से एक पास में किया जा सकता था। इससे 2 टेम्परेरी भी बनाए जा रहे हैं, जो प्रदर्शन को और खराब करते हैं। संक्षेप में, अपने गणितीय समकक्ष के एक अंकन का उपयोग करने के लिए लचीलेपन को जोड़कर, आपने Matrix वर्ग को भी अत्यधिक अक्षम बना दिया है।

उदाहरण के लिए, ऑपरेटर ओवरलोडिंग के बिना, आप एक एकल पास का उपयोग करके कहीं अधिक कुशल मैट्रिक्स योग लागू कर सकते हैं:

template<typename T, std::size_t COL, std::size_t ROW>
Matrix<T, COL, ROW> add3(const Matrix<T, COL, ROW>& a,
                         const Matrix<T, COL, ROW>& b,
                         const Matrix<T, COL, ROW>& c)
{
    Matrix<T, COL, ROW> result;
    for (size_t y = 0; y != ROW; ++y) {
        for (size_t x = 0; x != COL; ++x) {
            result(x, y) = a(x, y) + b(x, y) + c(x, y);
        }
    }
    return result;
}

पिछले उदाहरण के अपने नुकसान हैं क्योंकि यह मैट्रिक्स वर्ग के लिए कहीं अधिक जटिल इंटरफ़ेस बनाता है (आपको Matrix::add2() , Matrix::AddMultiply() और इतने पर) जैसे तरीकों पर विचार करना होगा।

इसके बजाय आइए हम एक कदम पीछे लेते हैं और देखते हैं कि कैसे हम अधिक कुशल तरीके से प्रदर्शन करने के लिए ऑपरेटर ओवरलोडिंग को अनुकूलित कर सकते हैं

समस्या इस तथ्य से उपजी है कि अभिव्यक्ति Matrix d = a + b + c का मूल्यांकन "उत्सुकता से" होने से पहले किया गया है ताकि आपको संपूर्ण अभिव्यक्ति पेड़ बनाने का अवसर मिले। दूसरे शब्दों में, जो आप वास्तव में प्राप्त करना चाहते हैं, वह है एक पास में a + b + c का मूल्यांकन करना और केवल एक बार जब आपको वास्तव में परिणाम व्यक्त करने की आवश्यकता होती है तो d

अभिव्यक्ति टेम्पलेट्स के पीछे यह मुख्य विचार है: दो मैट्रिक्स उदाहरणों को जोड़ने के परिणामस्वरूप operator+() का तुरंत मूल्यांकन करने के बजाय, यह संपूर्ण अभिव्यक्ति ट्री के निर्माण के बाद भविष्य के मूल्यांकन के लिए "अभिव्यक्ति टेम्पलेट" लौटाएगा।

उदाहरण के लिए, 2 प्रकारों के योग के लिए एक अभिव्यक्ति टेम्पलेट के लिए यहां एक संभावित कार्यान्वयन है:

template <typename LHS, typename RHS>
class MatrixSum
{
public:
    using value_type = typename LHS::value_type;

    MatrixSum(const LHS& lhs, const RHS& rhs) : rhs(rhs), lhs(lhs) {}
    
    value_type operator() (int x, int y) const  {
        return lhs(x, y) + rhs(x, y);
    }
private:
    const LHS& lhs;
    const RHS& rhs;
};

और यहाँ operator+() का अद्यतन संस्करण है

template <typename LHS, typename RHS>
MatrixSum<LHS, RHS> operator+(const LHS& lhs, const LHS& rhs) {
    return MatrixSum<LHS, RHS>(lhs, rhs);
}

जैसा कि आप देख सकते हैं, operator+() अब 2 मैट्रिक्स इंस्टेंस (जो एक और मैट्रिक्स उदाहरण होगा) को जोड़ने के परिणाम का "उत्सुक मूल्यांकन" नहीं करता है, बल्कि इसके अलावा एक अभिव्यक्ति टेम्पलेट जो अतिरिक्त ऑपरेशन का प्रतिनिधित्व करता है। ध्यान रखने के लिए सबसे महत्वपूर्ण बिंदु यह है कि अभिव्यक्ति का मूल्यांकन अभी तक नहीं किया गया है। यह केवल अपने ऑपरेंड के संदर्भ रखता है।

वास्तव में, MatrixSum<> अभिव्यक्ति टेम्पलेट को निम्न प्रकार से MatrixSum<> से आपको कुछ भी नहीं रोकता है:

MatrixSum<Matrix<double>, Matrix<double> > SumAB(a, b);

आप हालांकि बाद के चरण में हो सकते हैं, जब आपको वास्तव में योग के परिणाम की आवश्यकता होती है, तो अभिव्यक्ति का मूल्यांकन करें d = a + b निम्नानुसार है:

for (std::size_t y = 0; y != a.rows(); ++y) {
    for (std::size_t x = 0; x != a.cols(); ++x) {
        d(x, y) = SumAB(x, y);
    }
}

जैसा कि आप देख सकते हैं, एक अभिव्यक्ति टेम्पलेट का उपयोग करने का एक और लाभ यह है कि आप मूल रूप से a और b के योग का मूल्यांकन करने और इसे एक ही पास में d को असाइन करने में कामयाब रहे हैं।

इसके अलावा, कई अभिव्यक्ति टेम्पलेट्स के संयोजन से कुछ भी नहीं रोकता है। उदाहरण के लिए, a + b + c का परिणाम निम्न अभिव्यक्ति टेम्पलेट में होगा:

MatrixSum<MatrixSum<Matrix<double>, Matrix<double> >, Matrix<double> > SumABC(SumAB, c);

और यहाँ फिर से आप एकल पास का उपयोग करके अंतिम परिणाम का मूल्यांकन कर सकते हैं:

for (std::size_t y = 0; y != a.rows(); ++y) {
    for (std::size_t x = 0; x != a.cols(); ++x) {
        d(x, y) = SumABC(x, y);
    }
}

अंत में, पहेली का अंतिम टुकड़ा वास्तव में अपनी अभिव्यक्ति टेम्पलेट को Matrix क्लास में प्लग करना है। यह अनिवार्य रूप से Matrix::operator=() लिए एक कार्यान्वयन प्रदान करके प्राप्त किया जाता है, जो अभिव्यक्ति टेम्पलेट को एक तर्क के रूप में लेता है और इसे एक पास में मूल्यांकन करता है, जैसा कि आपने पहले "मैन्युअल रूप से" किया था:

template <typename T, std::size_t COL, std::size_t ROW>
class Matrix {
public:
    using value_type = T;

    Matrix() : values(COL * ROW) {}

    static size_t cols() { return COL; }
    static size_t rows() { return ROW; }

    const T& operator()(size_t x, size_t y) const { return values[y * COL + x]; }
    T& operator()(size_t x, size_t y) { return values[y * COL + x]; }

    template <typename E>
    Matrix<T, COL, ROW>& operator=(const E& expression) {
        for (std::size_t y = 0; y != rows(); ++y) {
            for (std::size_t x = 0; x != cols(); ++x) {
                values[y * COL + x] = expression(x, y);
            }
        }
        return *this;
    }

private:
    std::vector<T> values;
};


Modified text is an extract of the original Stack Overflow Documentation
के तहत लाइसेंस प्राप्त है CC BY-SA 3.0
से संबद्ध नहीं है Stack Overflow