Szablony wyrażeń i C ++ 11

Spójrzmy na jedną szczególną korzyść szablonów wyrażeń: ET można użyć do uniknięcia tymczasowych wektorów wielkości w pamięci, które występują w przeciążonych operatorach, takich jak:

template<typename T>
std::vector<T> operator+(const std::vector<T>& a, const std::vector<T>& b)
{
  std::vector<T> tmp;   // vector-sized temporary
  for_each(...);
  return tmp;
}

W C ++ 11 instrukcja return tej funkcji stosuje semantykę ruchu. Brak kopii wektora. To wygrana.

Jeśli jednak patrzę na proste wyrażenie, takie jak

d = a + b + c;

Widzę, że powyższa funkcja jest wywoływana dwukrotnie (dla obuoperator+), podczas gdy ostateczne przypisanie można wykonać za pomocą semantyki ruchu.

W sumie wykonywane są 2 pętle. Znaczy, że zgasiłem tymczasowy i przeczytałem go od razu. W przypadku dużych wektorów wypada z pamięci podręcznej. To gorsze niż szablony wypowiedzi. Mogą zrobić wszystko w jednej pętli. ET mogą wykonać powyższy kod równoważny:

for(int i=0 ; i < vec_length ; ++i)
  d[i] = a[i] + b[i] + c[i];

Zastanawiałem się, czy lambdy wraz z semantyką ruchu lub jakąkolwiek inną nową funkcją mogą działać tak dobrze, jak ET. jakieś pomysły?

Edytować:

Zasadniczo, używając techniki ET kompilator buduje drzewo analizujące, które przypomina wyrażenie algebraiczne z jego systemem typów. To drzewo składa się z węzłów wewnętrznych i węzłów liści. Węzły wewnętrzne reprezentują operacje (dodawanie, mnożenie itp.), A węzły liści reprezentują odwołania do obiektów danych.

Starałem się myśleć o całym procesie obliczeniowym w sposób podobny do maszyny stosu: Wykonaj operację ze stosu operacji i pobierz kolejne argumenty ze stosu argumentów i oceń operację. Umieść wynik z powrotem na stosie, czekając na operację.

Aby reprezentować te dwa różne obiekty (stos operacji i stos liści danych), spakowałem razemstd::tuple dla operacji istd::tuple dla danych pozostawia sięstd::pair<>. Początkowo użyłemstd:vector ale skutkowało to narzutem na środowisko wykonawcze.

Cały proces przebiega w dwóch fazach: inicjalizacja maszyny stosowej, w której inicjowany jest stos operacji i argumentów. I faza oceny, która jest wyzwalana przez przypisanie sparowanych kontenerów do wektora.

Stworzyłem klasęVec który posiada prywatnyarray<int,5> (ładunek) i który zawiera przeciążony operator przypisania, który przyjmuje „wyrażenie”.

Globalnyoperator* jest przeciążony dla wszystkich kombinacjiVec oraz „wyrażenie”, aby umożliwić poprawną obsługę również w przypadku, gdy mamy więcej niż tylkoa*b. (Zauważ, że przeniosłem się na ten edukacyjny przykład do mnożenia - zasadniczo po to, by szybko zauważyćimull w asemblerze.)

Najpierw przed rozpoczęciem ewaluacji następuje „wyodrębnienie” wartości z zaangażowanychVec obiekty i inicjowanie stosu argumentów. Było to konieczne, aby nie mieć różnych rodzajów obiektów leżących wokół: wektorów indeksowalnych i wyników nieindeksowalnych. Oto coExtractor jest dla. Znowu fajna rzecz: używane są szablony Variadic, które w tym przypadku nie powodują narzutu w czasie wykonywania (wszystko to odbywa się w czasie kompilacji).

Całość działa. Wyrażenie jest ładnie oceniane (dodałem też dodatek, ale tutaj jest to pasujące do kodu). Poniżej możesz zobaczyć wyjście asemblera. Po prostu surowa kompilacja, dokładnie taka, jaka ma być: w zgodzie z techniką ET.

Wynik. Nowe funkcje języka C ++ 11 oferują zmienne szablony, które (wraz z meta-programowaniem szablonów) otwierają obszar obliczania czasu kompilacji. Pokazałem tutaj, w jaki sposób korzyści płynące z szablonów różnicowych można wykorzystać do stworzenia kodu tak dobrego, jak w tradycyjnej technice ET.

#include<algorithm>
#include<iostream>
#include<vector>
#include<tuple>
#include<utility>
#include<array>



template<typename Target,typename Tuple, int N, bool end>
struct Extractor {
  template < typename ... Args >
  static Target index(int i,const Tuple& t, Args && ... args)
  {
    return Extractor<Target, Tuple,  N+1, 
             std::tuple_size<Tuple>::value == N+1>::
      index(i, t , std::forward<Args>(args)..., std::get<N>(t).vec[i] );
  }
};

template < typename Target, typename Tuple, int N >
struct Extractor<Target,Tuple,N,true>
{
    template < typename ... Args >
    static Target index(int i,Tuple const& t, 
            Args && ... args) { 
      return Target(std::forward<Args>(args)...); }
};

template < typename ... Vs > 
std::tuple<typename std::remove_reference<Vs>::type::type_t...>
extract(int i , const std::tuple<Vs...>& tpl)
{
  return Extractor<std::tuple<typename std::remove_reference<Vs>::type::type_t...>,
           std::tuple<Vs...>, 0,
           std::tuple_size<std::tuple<Vs...> >::value == 0>::index(i,tpl);
}


struct Vec {
  std::array<int,5> vec;
  typedef int type_t;

  template<typename... OPs,typename... VALs>
  Vec& operator=(const std::pair< std::tuple<VALs...> , std::tuple<OPs...> >& e) {
    for( int i = 0 ; i < vec.size() ; ++i ) {
      vec[i] = eval( extract(i,e.first) , e.second );
    }
  }
};




template<int OpPos,int ValPos, bool end>
struct StackMachine {
  template<typename... OPs,typename... VALs>
  static void eval_pos( std::tuple<VALs...>& vals , const std::tuple<OPs...> & ops )
  {
    std::get<ValPos+1>( vals ) =
      std::get<OpPos>(ops).apply( std::get<ValPos>( vals ) , 
                  std::get<ValPos+1>( vals ) );
    StackMachine<OpPos+1,ValPos+1,sizeof...(OPs) == OpPos+1>::eval_pos(vals,ops);
  }
};

template<int OpPos,int ValPos>
struct StackMachine<OpPos,ValPos,true> {
  template<typename... OPs,typename... VALs>
  static void eval_pos( std::tuple<VALs...>& vals , 
            const std::tuple<OPs...> & ops )
  {}
};



template<typename... OPs,typename... VALs>
int eval( const std::tuple<VALs...>& vals , const std::tuple<OPs...> & ops )
{
  StackMachine<0,0,false>::eval_pos(const_cast<std::tuple<VALs...>&>(vals),ops);
  return std::get<sizeof...(OPs)>(vals);
}




struct OpMul {
  static int apply(const int& lhs,const int& rhs)  {
    return lhs*rhs;
  }
};

std::pair< std::tuple< const Vec&, const Vec& > , std::tuple<OpMul> >
operator*(const Vec& lhs,const Vec& rhs)
{
  return std::make_pair( std::tuple< const Vec&, const Vec& >( lhs , rhs ) , 
             std::tuple<OpMul>( OpMul() ) );
}

template<typename... OPs,typename... VALs>
std::pair< std::tuple< const Vec&, VALs... > , std::tuple<OPs...,OpMul> >
operator*(const Vec& lhs,const std::pair< std::tuple< VALs... > , std::tuple<OPs...> >& rhs)
{
  return std::make_pair( std::tuple_cat( rhs.first , std::tuple< const Vec& >(lhs)  ) , 
             std::tuple_cat( rhs.second , std::tuple<OpMul>( OpMul() )  ) );
}

template<typename... OPs,typename... VALs>
std::pair< std::tuple< const Vec&, VALs... > , std::tuple<OPs...,OpMul> >
operator*(const std::pair< std::tuple< VALs... > , std::tuple<OPs...> >& lhs,
      const Vec& rhs)
{
  return std::make_pair( std::tuple_cat( lhs.first , std::tuple< const Vec& >(rhs)  ) , 
             std::tuple_cat( lhs.second , std::tuple<OpMul>( OpMul() ) ) );
}

int main()
{
  Vec d,c,b,a;


  for( int i = 0 ; i < d.vec.size() ; ++i ) {
    a.vec[i] = 10+i;
    b.vec[i] = 20+i;
    c.vec[i] = 30+i;
    d.vec[i] = 0;
  }

  d = a * b * c * a;

  for( int i = 0 ; i < d.vec.size() ; ++i ) 
    std::cout << d.vec[i] << std::endl;
}

Generowany za pomocą asemblerag++-4.6 -O3 (Musiałem umieścić zależność od runtime w inicjalizacji wektora, aby kompilator nie obliczał całej rzeczy w czasie kompilacji i rzeczywiście widziszimull instacje.)

imull   %esi, %edx
imull   32(%rsp), %edx
imull   %edx, %esi
movl    68(%rsp), %edx
imull   %ecx, %edx
movl    %esi, (%rsp)
imull   36(%rsp), %edx
imull   %ecx, %edx
movl    104(%rsp), %ecx
movl    %edx, 4(%rsp)
movl    72(%rsp), %edx
imull   %ecx, %edx
imull   40(%rsp), %edx
imull   %ecx, %edx
movl    108(%rsp), %ecx
movl    %edx, 8(%rsp)
movl    76(%rsp), %edx
imull   %ecx, %edx
imull   44(%rsp), %edx
imull   %ecx, %edx
movl    112(%rsp), %ecx
movl    %edx, 12(%rsp)
movl    80(%rsp), %edx
imull   %ecx, %edx
imull   %eax, %edx
imull   %ecx, %edx
movl    %edx, 16(%rsp)

questionAnswers(2)

yourAnswerToTheQuestion