Zaawansowane CPP/Wykład 8: Metaprogramowanie: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Gracja (dyskusja | edycje)
Nie podano opisu zmian
 
Nie podano opisu zmian
Linia 1: Linia 1:
==7.1 Metaprogramowanie==


Ogólnie rzecz biorąc metaprogramowanie oznacza pisanie programów, które
piszą programy lub pisania programu, który pisze się sam. W naszym
kontekście będzie to oznaczało wykonywanie obliczeń za pomocą
szablonów, przy czym obliczenia te są wykonywane podczas kompilacji.
Podstawą do tych obliczeń jest rekurencyjna konkretyzacja szablonów.
Taką metodą można generować w czasie kompilacji całkiem skomplikowane
fragmenty kodu, stąd określenie metaprogramowanie.  Przykłady takich
"metaszablonów" poznaliśmy już na wykładzie o funkcjach typów. W
szczególności działanie na listach typów to właśnie przykłady
metaprogramowania.  W tym wykładzie przyjrzymy się dokładniej temu
zagadnieniu i przeanalizujemy kolejne przykłady.
==7.2 Potęgi==
Zaczniemy od bardzo prostego przykładu (zob. D. Vandervoorde, N. Josuttis:''"C++ Szablony, Vademecum profesjonalisty"'', rozdz. 17).
Napiszemy szablon który ma za zadanie obliczać potęgi liczby 3.
Ponieważ w programowaniu za pomocą szablonów musimy posługiwać się
rekurencją to zaczynamy od sformułowania problemu w sposób
rekurencyjny. To akurat jest bardzo proste:
{{przyklad|7.1||
<center><math>3^N=3*3^{N-1},\quad 3^0 = 1</math>
</center>
}}
Za pomocą szablonów implementujemy to tak ([http://osilek.mimuw.edu.pl/images/9/91/Pow.cpp Źródło:Pow.cpp]):
   
template<int  N> struct Pow3 { <br>
&nbsp; enum {val<nowiki>=</nowiki>3*Pow3<N-1>::val}; <br>
};<br>
template<> struct Pow3<0> {<br>
&nbsp; enum {val<nowiki>=</nowiki>1}; <br>
};
teraz możemy już użyć w programie np. wyrażenie:
&nbsp;i=Pow3<4>::val;
Podstawową zaletą metaprogramowania i głównym powodem jego używania
jest fakt, że to wyrażenie jest obliczane w czasie kompilacji i efekt
jest taki sam jak podstawienia:
&nbsp;i<nowiki>=</nowiki>81;
Można też zastosować szablon funkcji:
template<int N> int pow3() {<br>
&nbsp; return 3*pow3<N-1>();<br>
};<br>
template<> int pow3<0>() {return 1;}<br>
cout<<pow3<4>()<<endl;
<span style="color:red">{mod07/code/powf.cpp}{powf.cpp
/* {mod07/code/powf.cpp}{powf.cpp}*/}</span>.
Nietrudno jest uogólnić powyższy kod tak aby wyliczał potęgi dowolnej
liczby:
template<int K,int N> struct Pow { enum <br>
&nbsp;{val<nowiki>=</nowiki>K*Pow<K,N-1>::val}; };<br>
template<int K> struct Pow<K,0> {<br>
&nbsp;enum {val<nowiki>=</nowiki>1};<br>
};
<span style="color:red">{mod07/code/pow.cpp}{pow.cpp}</span>
/* {mod07/code/pow.cpp}{pow.cpp}*/
Tutaj już nie można wykorzystać szablonu funkcji, bo nie zezwala on na
specjalizację częściową.<br>
Ograniczeniem dla takich obliczeń jest implementacja kompilatora,
przede wszystkim założony limit głebokości rekurencyjnego
konkretyzowania szablonów. Dla kompilatora g++ jest on ustawiany za
pomocą opcji i defaultowo wynosi 500, dlatego już konkretyzacja <tt>Pow<1,500>::val</tt> się nie powiedzie. 
Konkretyzacja szablonów wymaga też pamięci i może się zdarzyć, że
kompilator wyczerpie limit pamięci lub czasu. <br> &nbsp;Kolejne ograniczenie to konieczność rachunków na liczbach całkowitych.
Wiąże się to z faktem, że tylko stałe całkowitoliczbowe mogą być
parametrami szablonów.
==7.3 Ciąg Fibonacciego==
Po opanowaniu powyższych przykładów obliczanie wyrazów ciągu
Fibonacciego jest prostym zadaniem. Przytoczymy je jednak tutaj, aby
zaprezentować pewną bardzo sympatyczną cechę metaprogramowania za
pomocą szablonów. Ciąg Fibonacciego jest definiowany rekurencyjnie:
{{przyklad|7.2||
<center><math>f_n=f_{n-1}+f_{n-2},\quad f_1=f_2=1</math>
</center>
}}
więc jego implementacja jest natychmiastowa:
template<int N> struct Fibonacci {<br>
&nbsp;enum {val <nowiki>=</nowiki> Fibonacci<N-1>::val+Fibonacci<N-2>::val};<br>
};<br>
template<> struct Fibonacci<1> {<br>
&nbsp;enum {val <nowiki>=</nowiki> 1};<br>
};<br>
template<> struct Fibonacci<2> {<br>
&nbsp;enum {val <nowiki>=</nowiki> 1};<br>
};
<span style="color:red">{mod07/code/fibonacci_template.cpp}{fibonaccitemplate.cpp}</span>
/*{mod07/code/fibonacci_template.cpp}{fibonaccitemplate.cpp}*/
Przykład ten nie wart byłby może i wspomnienia gdyby nie fakt, że
rekurencyjna implementacja ciągu Fibonacciego jest bardzo nieefektywna.
Jeśli zaimplementujemy ją w zwykłym kodzie
int fibonacci(int n) {<br>
&nbsp;if(n<nowiki>=</nowiki><nowiki>=</nowiki>1) return 1;<br>
&nbsp;if(n<nowiki>=</nowiki><nowiki>=</nowiki>2) return 1;<br>
&nbsp;return fibonacci(n-1)+fibonacci(n-2);<br>
}
<span style="color:red">{mod07/code/fibonacci.cpp}{fibonacci.cpp}</span>
/*{mod07/code/fibonacci.cpp}{fibonacci.cpp}*/
to obliczanie <tt>fibonacci(45)</tt> zajmie np.na moim komputerze ok. 8
sekund.  Tymczasem szablon kompiluje sie poniżej jednej sekundy!  Skąd
taka różnica? Czyżby kompilator był bardziej wydajny niż generowany
przez niego kod? W przypadku zwykłego kodu długi czas wykonania bierze
się z ogromnej liczby wywołań funkcji <tt>fibonacci</tt>. Liczba ta rośnie
wykładniczo z <tt>n</tt> i większość czasu jest marnowana na wielokrotne
wywoływanie funkcji z tymi samymi argumentami.
&nbsp;W przypadku użycia metaprogramu szablony konkretyzowane są tylko raz.
Więc jeśli już raz policzymy np. <tt>Fibonacci<25>::val</tt> to kolejne
żądanie nie spowoduje już rozwinięcia rekurencyjnego, a tylko
podstawienie istniejącej wartości. Jak widzieliśmy zysk jest ogromny.
Takie pamiętanie wyników raz wywołanych funkcji nazywane jest też
programowaniem dynamicznym. Jedynym znanym mi językiem, który
bezpośrednio wspiera taki mechanizm jest <tt>Mathematica</tt>.
==7.4 Pierwiastek kwadratowy==
Rozważymy teraz trudniejszy przykład szablonu obliczającego
pierwiastek kwadratowy ([1] rozdz. 17), choć tak naprawdę
ten kod jest bardziej uniwersalny i, jak zobaczymy, łatwo za jego pomocą
zaimplementować inne funkcje. Ponieważ jesteśmy ograniczeni do
arytmetyki liczb całkowitych, tak naprawdę nie liczymy pierwiastka z
<tt>n</tt> ale jego przybliżenie: największą liczbę całkowitą <tt>k</tt>, taką że <tt>k*k<<nowiki>=</nowiki>n</tt>.
W tym celu zastosujemy algorytm przeszukiwania binarnego:
int sqrt(int n,int low, int high) {<br>
&nbsp;if(low<nowiki>=</nowiki><nowiki>=</nowiki>high) return low;<br>
&nbsp;int mid<nowiki>=</nowiki>(low+high+1)/2;<br>
&nbsp;if(mid*mid > n ) <br>
&nbsp;&nbsp;&nbsp;return sqrt(n,low,mid-1);<br>
&nbsp;else<br>
&nbsp;&nbsp;&nbsp;return sqrt(n,mid,high);<br>
}
co już łatwo przetłumaczyć na szablon:
template<int N,int L<nowiki>=</nowiki>1,int H<nowiki>=</nowiki>N> struct Sqrt{<br>
&nbsp;enum  {mid<nowiki>=</nowiki>(L+H+1)/2};<br>
&nbsp;enum  {res<nowiki>=</nowiki> (mid*mid> N)? (int)Sqrt<N,L,mid-1>::res :<br>
&nbsp;&nbsp;(int)Sqrt<N,mid,H>::res};<br>
};<br>
template<int N,int L> struct Sqrt<N,L,L> {<br>
&nbsp;enum {res<nowiki>=</nowiki>L};<br>
};
/* {mod07/code/sqrt.cpp}{sqrt.cpp}*/
Łatwo sprawdzić, że kod ten działa poprawnie. Niestety posiada on
istotną wadę.  W trakcie konkretyzacji szablonu konkretyzowane są oba
szablony występujące w wyrażeniu warunkowym, nawet ten, który nie będzie
potem używany.  Tak więc wykonywana jest duża liczba konkretyzacji, z
których tylko ułamek jest potrzebny (zob. rysunek). Jakie to obciążenie dla
kompilatora to łatwo sprawdzić kompilując kod, w którym wywołujemy
<tt>Sqrt<10000></tt>.  Ja nie doczekałem się na koniec kompilacji.
Na szczęście istnieje rozwiązanie - należy użyć szablonu <tt>If_then_else</tt>
(zob. [[Zaawansowane CPP/Wykład 5: Funkcje typów i inne sztuczki|rozdział 5.2.1]]):
template<int N,int L<nowiki>=</nowiki>1,int H<nowiki>=</nowiki>N> struct Sqrt{<br>
&nbsp;enum  {mid<nowiki>=</nowiki>(L+H+1)/2};<br>
&nbsp;typedef typename If_then_else<<br>
&nbsp;&nbsp;(mid*mid> N),<br>
&nbsp;&nbsp;Sqrt<N,L,mid-1>,<br>
&nbsp;&nbsp;Sqrt<N,mid,H> >::Result tmp;<br>
&nbsp;enum  {res<nowiki>=</nowiki> tmp::res};<br>
};<br>
template<int N,int L> struct Sqrt<N,L,L> {<br>
&nbsp;enum {res<nowiki>=</nowiki>L};<br>
};
/* {mod07/code/sqrt_ifte.cpp}{sqrtifte.cpp}*/
Ten kod powoduje już tylko konkretyzację rzeczywiście wymaganych
szablonów, a tych jest dużo mniej: rzędu <math>O(\log N)</math>.  Tym razem
kompilacja wyrażenia <tt>Sqrt<10000></tt> powiedzie się bez trudu.
==7.5 Pow(x)==
Jak dotąd używaliśmy metaprogramowania do wyliczania wartości stałych.
Teraz postaramy się wygenerować funkcje. Zacznijmy od pytania: po co?
Pomijając syndrom Mount Everestu (wchodzę na niego bo jest), to
głównym powodem jest nadzieja uzyskania bardziej wydajnego kodu.
Weźmy dalej za przykład liczenie potęgi, tym razem dowolnej liczby
zmiennoprzecinkowej:
double pow_int(double x,int n) {<br>
double res<nowiki>=</nowiki>1.0;<br>
for(int i<nowiki>=</nowiki>0;i<n;++i)<br>
&nbsp;res*<nowiki>=</nowiki>x;<br>
return res;<br>
};
/*{mod07/code/powx.cpp}{powx.cpp}*/
Patrząc na ten kod widzimy, że w pętli wykonywana jest bardzo prosta
instrukcja. Możemy więc się obawiać, że instrukcje związane z obsługą
pętli mogą stanowić spory narzut.
Co więcej, ich obecność utrudnia kompilatorowi optymalizację kodu oraz 
może uniemożliwić rozwinięcie funkcji w miejscu wywołania.
Najlepiej by było zaimplementować tę funkcję w ten sposób:
pow(x,n)<nowiki>=</nowiki> x*...*x; /*n razy*/
np.:
double pow2(double x) {return x*x;}<br>
double pow3(double x) {return x*x*x;}<br>
double pow4(double x) {return x*x*x*x;}<br>
...
Wymaga to jednak kodowania ręcznego dla każdej potęgi, której
potrzebujemy.  Ten sam efekt możemy osiągnąc za pomocą następującego
szablonu funkcji:
template<int N> inline double pow(x) {return x*pow<N-1>(x);}<br>
template<>  inline double pow<0>(x) {return 1.0;}
/*{mod07/code/powx.cpp}{powx.cpp}*/
pod warunkiem, że kompilator rozwinie wszystkie wywołania.
Poniżej zamieszczam wyniki pomiarów wykonania 100 milionów wywołań
każdej funkcji ({mod07/code/powx.cpp}{powx.cpp}). Czas jest
podany w sekundach. Zamieściłem wyniki dla różnych ustawień
optymalizacji.
<div align=center>
{| border=1
|-
|
| pow_int(x,5)
|  pow<5>(x)
|---
|align="center"|-O0
|align="center"|  7.22
|align="center"|  14.78
|---
|align="center"|-O1
|align="center"|  0.37
|align="center"|  0.04
|---
|align="center"|-O2
|align="center"|  0.42
|align="center"|  0.05
|---
|align="center"|-O3
|align="center"|  0.42
|align="center"|  0.05
|}
</div>
Widać dramatyczną różnicę po włączeniu optymalizacji. Wiąże się to
prawdopodobnie z umożliwieniem rozwijania funkcji <tt>inline</tt>.  Potem
wyniki już się nie zmieniają ale widać, że szablon <tt>pow</tt> wygenerował
funkcję 10 razy szybszą od pozostałych. Pokazuje to dobitnie, że
optymalizacja ręczna ciągle ma sens.
==7.6 Sortowanie bąbelkowe==
Zakończymy ten wykład bardziej skomplikowanym przykładem pokazującym,
że metaprogramowanie można stosować nie tylko do obliczeń
numerycznych. Popatrzmy na kod implementujący sortowanie bąbelkowe:
inline void swap (int &a,int &b) {int      tmp<nowiki>=</nowiki>a;a<nowiki>=</nowiki>b;b<nowiki>=</nowiki>tmp;};<br>
void bubble_sort_function (int* data, int N) {<br>
&nbsp;for(int i <nowiki>=</nowiki> N-1; i>0; --i)<br>
&nbsp;&nbsp;for(int j<nowiki>=</nowiki>0;j<i;++j)<br>
&nbsp;&nbsp;&nbsp;if(data[j]>data[j+1]) <br>
&nbsp;&nbsp;&nbsp;&nbsp;swap(data[j],data[j+1]);<br>
}
/* {mod07/code/bubble_template.cpp}{bubbletemplate.cpp} */
Znów widzimy tu dwie pętle i wszystkie uwagi dotyczące funkcji
<tt>pow_int</tt> tu też się stosują. Postaramy się więc zdefiniować szablon,
który dokona rozwinięcia tych obu pętli. Np. dla <tt>N<nowiki>=</nowiki>3</tt> chcielibyśmy otrzymać następujący kod:
//i<nowiki>=</nowiki>2 j<nowiki>=</nowiki>0<br>
if(data[0]>data[1]) swap(data[0],data[1]);<br>
//i<nowiki>=</nowiki>2 j<nowiki>=</nowiki>1<br>
if(data[1]>data[2]) swap(data[1],data[2]);<br>
//i<nowiki>=</nowiki>1 j<nowiki>=</nowiki>0<br> 
if(data[0]>data[1]) swap(data[0],data[1]);
Jeśli Państwo śledzili wykład (przynajmniej ten), to już Państwo wiedzą, że
pierwszym krokiem musi być przepisanie kodu sortowania na postać rekurencyjną:
void bubble_sort_function (int* data, int N) {<br>
&nbsp;for(int j<nowiki>=</nowiki>0;j<N-1;++j)<br>
&nbsp;&nbsp;if(data[j]>data[j+1]) <br>
&nbsp;&nbsp;&nbsp;swap(data[j],data[j+1]);<br>
&nbsp;if(N>2)<br>
&nbsp;&nbsp;bubble_sort_function(data,N-1);<br>
}
To jeszcze nie to co trzeba bo musimy zapisać pętle w postaci
rekurencyjnej. Jeśli oznaczymy sobie:
void loop(int * data,int N) {<br>
&nbsp;for(int j<nowiki>=</nowiki>0;j<N-1;++j)<br>
&nbsp;&nbsp;if(data[j]>data[j+1]) <br>
&nbsp;&nbsp;&nbsp;swap(data[j],data[j+1]);<br>
}
to łatwo zauważyć, że <tt>loop</tt> można zdefiniować następująco:
loop(int *data,int N) {<br>
&nbsp;if(N>0) {<br>
&nbsp;&nbsp;if(data[0]>data[1]) swap(data[0],data[1]);<br>
&nbsp;&nbsp;loop(++data,N-1);<br>
&nbsp;}<br>
}
co natychmiast tłumaczy się na szablony:
template<int N> inline void loop(int *data) {<br>
&nbsp;if(data[0]>data[1]) std::swap(data[0],data[1]);<br>
&nbsp;loop<N-1>(++data);<br>
}<br>
template<> inline void loop<0>(int *data) {};
Szablon funkcji <tt>bubble_sort_template</tt> ma więc postać:
template<int N>  inline void bubble_sort_template(int * data) {<br>
&nbsp;loop<N-1>(data);<br>
&nbsp;bubble_sort_template<N-1>(data);<br>
}<br>
template<>  inline void bubble_sort_template<2>(int * data) {<br>
&nbsp;loop<1>(data);<br>
};
/* {mod07/code/bubble_template.cpp}{bubbletemplate.cpp} */
Poniżej znów podaję porównanie czasu wykonywania się 100 milionów
wywołań funkcji <tt>bubble_sort_function</tt> i <tt>bubble_sort_template</tt> dla tablicy zawierającej 12 liczb całkowitych w kolejności malejącej. 
<div align=center>
{| border=1
|-
!|
| bubblesortfunction(a,12) 
| bubblesorttemplate<12>(a)
|---
|align="center"|-O0
|align="center"|43.3
|align="center"|42.2 
|-
|align="center"|-O1
|align="center"|21.0
|align="center"|4.8
|-
|align="center"|-O2
|align="center"|20.0
|align="center"|3.5
|-
|align="center"|-O3
|align="center"|20.0
|align="center"|3.6
|}
</div>
Widać, że wersja na szablonach jest ok. 4-5 razy szybsza. Zachęcam do
własnych eksperymentów.<br>
Zaprojektowany szablon działa tylko dla tablic liczb całkowitych. Jest
to ewidentne ograniczenie, które powinniśmy zlikwidować poprzez dodanie
doatkowego parametru szablonu. Niestety, prowadzi to do konieczności
dokonania specjalizacji częściowej, która nie jest dozwolona dla
szablonów funkcji. Na szczeście nie jest trudno przepisać naszą
implementację używając szablonów klas. Pozostawiam to jako ćwiczenie
dla czytelników :).
==7.7 Rozmiar kodu==
Jak pokazałem, kod generowany przez szablon <tt>bubble_sort_template</tt> jest
bardziej efektywny. Dzieje się to jednak kosztem jego rozmiaru. Są ku
temu dwa powody. Po pierwsze podwójna pętla wewnątrz procedury
<tt>bubble_sort_function</tt> wykonuje <math>(N-1)*(N-2)/2</math> iteracji i tyle
linijek powinien mieć w pełni rozwinięty kod w szablonie
<tt>bubble_sort_template</tt>. Po drugie każda instancja szablonu jest
osobną funkcją, stąd <tt>bubble_sort_template<50></tt> i
<tt>bubble_sort_template<51></tt> generują osobny kod każda. W celu
sprawdzenia tych przewidywań przedstawiam poniżej rozmiar wynikowego
kodu w bajtach dla programu, który kompilował funkcję
<tt>bubble_sort_template<N></tt>.
<div align=center>
{| border=1
|-
|align="center"|N 
|align="center"|size 
|-
|align="center"|10
|align="center"|9333   
|-
|align="center"|30
|align="center"|17846 
|-
|align="center"|50
|align="center"|21847 
|-
|align="center"|70
|align="center"|40399 
|-
|align="center"|90
|align="center"|33296 
|-
|align="center"|100
|align="center"|53606
|}
</div>
Widać, że choć rozmiar w zasadzie rośnie z <math>N</math>, to ta zależność nie jest
nawet monotoniczna. Wynika to pewnie z tego, że dla większych <math>N</math>
kompilator nie dokonuje całkowitego rozwiniecią kodu.
==7.8 Podsumowanie==

Wersja z 18:51, 21 sie 2006