Niektóre języki programowania dopuszczają przeciążanie funkcji,
tzn. można zadeklarować wiele wersji funkcji które różnią się
tylko typami argumentów (czy zwracanych wartości). Inne
dopuszczają niejawne konwersje, np. liczby z mniejszego zakresu
można użyć wszędzie tam gdzie dopuszczalne są liczby z większego
zakresu. Celem zadań niżej jest przypatrzenie się
temu jak tego typu konstrukcje wpływają na budowę kompilatora.
Zadanie 1
W języku który dopuszcza konwersje domyślne (np. liczby z
mniejszego zakresu można użyć wszędzie tam gdzie dopuszczalne
są liczby z większego zakresu) i przeciążanie funkcji przy
sprawdzaniu poprawności wywołania trzeba uwzględniać potencjanie
dużą ilość konwersji. Jedna taktyka polega na przeglądaniu
wszystkich możliwych konwersji i sprawdzaniu czy dla danych
typów argumentów dostępna jest odpowiednia wersja funkcji.
Inna taktyka polega na przeglądaniu listy dostępnych wersji
funkcji i sprawdzaniu czy można skonwertować argumenty tak
by pasowały dla danej wersji. Porównać te taktyki, w szczególności
oczekowaną liczbę operacji.
Zadanie 2
Niektóre języki programowania dopuszczają przeciążanie funkcji,
tzn. można zadeklarować wiele wersji funkcji które różnią się
tylko typami argumentów (czy zwracanych wartości). W kompilatorze
dla pojedynczego wywołania zwykle stosuje się taktykę pełnego
przeglądania dostępnych (zadeklarowanych) możlowości. Dla
większych fragmentów mozliwe są taktyki rekursywnego dopasowania
z nawracaniem (najpierw próbujemy dobrać typ najbardzej zewnętrznie
występującej funkcji, potem dobieramy typy dla wyrażeń dających
argumentym jak argumenty nie pasują to wracamy do najwyższego
poziomu) i strategia "z dołu do góry" (najpierw patrzymy na
możliwości dla najprostszych podwyrażeń, potem dla bardziej
skomplikowanych, w szczególności możliwe typy dla wywołania
funkcji badamy po zbadaniu możliwości dla argumentów).
Jakie są wady a jakie zalety obu tych podejść. Oszacować
złożoność obliczeniową.
Zadanie 3
Niskopoziomowo program składa się z ciągów instrukcji obliczeniowych
zaczynających się od etykiety a kończących się skokiem. Od czasów
programowania strukturalnego zaleca się unikanie skoków. Jeśli
nie przejmujemy się wielkością i szybkością programu to istnieje
bardzo prosta metoda przekształcenia programu ze skokami na
program bez jawnych skoków. Mianowicie, wprowadzamy zmienną
pomocnią s (której niskopoziomowo odpowadały licznik rozkazów
procesora), dla każdej etykiety ei wprowadzamy nową wartość wi tej
zmiennej, skok do etykiety ei zastępujemy podstawieniem wi do s,
ciąg od etykiety ei do skoku zastępujemy instrukcją 'if' która wykonuje
się tylko wtedy gdy wartość s to ei, cały program otaczemy pętlą 'while'
kończącą się gdy zmienna pomocnicza ma wartość odpowiadającą zakończeniu
programu. Np. program:
l0:
if (a>=b) {
goto l1;
} else {
goto l2;
}
l2:
tmp = b;
b = a;
a = tmp;
goto l1;
l1:
if (a == b) {
goto koniec;
} else {
goto l3;
}
l3:
tmp = b;
b = a % b;
a = tmp;
goto l1;
zastępujmy przez:
enum {l0, l1, l2, l3, koniec} s;
s = l0;
while(s != koniec) {
if (s == l0) {
if ((a>=b) {
s = l1;
} else {
s = l2;
}
}
if (s == l1) {
if (a == b) {
s = koniec;
} else {
s = l3;
}
}
if (s == l2) {
tmp = b;
b = a;
a = tmp;
s = l1;
}
if (s == l3) {
tmp = b;
b = a % b;
a = tmp;
s = l1;
}
}
Jeśli stosujemy naiwny kompilator to "strukturalna" wersja często
wykonuje więcej operacji niż wersja ze skokami. Dokładniej, można
oczekiwać rzędu połowy okrążenia głownej pętli aby uzyskać efekt
skoku. Wspólczesne kompilatory mają szereg optymalizacji które
mogą poprawić sytuację, w szczególności jeśli skok prowadzi do
skoku warunkowego i wynik sprawdzania waruneku jest znany w trakcie
kompilacji to pierwszy skok modyfikuje się tak by prowadził prosto
do celu, omijając sprawdzanie warunku w tracie wykonania i drugi skok.
Pokazać że używając odpowiednie optymalizacje kompilator może
z wersji "strukturalnej" odtworzyć kod równoważny wersji ze skokami,
bez dodatkowych zmiennych czy zbędnych operacji.
Komentarz: powyższe zadanie jest trochę sztuczne, ale w praktyce
często mamy wybór między ładniejszym kodem który przy naiwnej
kompilacji byłby mało wydajny, a mniej ładnym kodem. Wtedy
pojawia się problem dobrania optymalizacji tak, by ładniejszy
kod dawał tak dobrą wydajność jak mniej ładny.