λ-rachunek: Co jest najbardziej wydajne w reprezentacji pamięci w funkcjach?


9

Chciałbym porównać wydajność struktur danych zakodowanych funkcyjnie (Church / Scott) i klasycznie zakodowanych (asembler / C).

Ale zanim to zrobię, muszę wiedzieć, jak wydajna jest / może być reprezentacja funkcji w pamięci. Funkcję tę można oczywiście częściowo zastosować (inaczej zamknięcie).

Interesuję się zarówno obecnym algorytmem kodowania, popularnymi językami funkcjonalnymi (Haskell, ML), jak i najbardziej wydajnym, jaki można osiągnąć.


Temperatura Bonus: Czy takie kodowanie mapy działać w zakodowanych liczb całkowitych natywnemu liczb ( short, intitd w C). Czy to w ogóle możliwe?


Cenię efektywność na podstawie wydajności. Innymi słowy, im bardziej wydajne jest kodowanie, tym mniej wpływa on na wydajność obliczeń z funkcjonalnymi strukturami danych.


Wszystkie moje próby Google nie powiodły się, być może nie znam odpowiednich słów kluczowych.
Ford O.

Czy możesz edytować pytanie, aby wyjaśnić, co rozumiesz przez „wydajny”? Wydajny na co? Gdy pytasz o wydajną strukturę danych, musisz określić, jakie operacje chcesz wykonywać na strukturze danych, ponieważ wpływa to na wybór struktury danych. A może masz na myśli, że kodowanie jest maksymalnie wydajne pod względem przestrzeni?
DW

1
To jest dość szerokie. Istnieje wiele abstrakcyjnych maszyn do rachunku lambda, które mają na celu wydajne wykonanie go (patrz np. SECD, CAM, Krivine's, STG). Ponadto należy wziąć pod uwagę dane zakodowane w Church / Scott, co stwarza więcej problemów. Np. Na listach zakodowanych w Kościele operacja tail musi być O (n) zamiast O (1). Wydaje mi się, że gdzieś czytałem, że istnienie kodowania list w Systemie F z operacjami głowy i ogona O (1) było nadal otwartym problemem.
chi

@DW Mówię o wydajności / kosztach ogólnych. Na przykład z wydajnym mapowaniem kodowania listy kościoła i listy Haskella powinno zająć to samo.
Ford O.

Wydajność dla jakich operacji? Co chcesz zrobić z funkcjami? Czy chcesz ocenić te funkcje na jakąś wartość? Raz, czy oceniasz tę samą funkcję na wielu wartościach? Zrobić z nimi coś jeszcze? Pytasz tylko, jak skompilować funkcję (napisaną w języku funkcjonalnym), aby można ją było wykonać tak wydajnie, jak to możliwe?
DW

Odpowiedzi:


11

Chodzi o to, że naprawdę nie ma wiele swobody w zakresie kodowania funkcji. Oto główne opcje:

  • Przepisywanie terminów: przechowujesz funkcje jako ich abstrakcyjne drzewa składniowe (lub ich pewne kodowanie. Kiedy wywołujesz funkcję, ręcznie przechodzisz przez drzewo składniowe, aby zastąpić jego parametry argumentem. Jest to łatwe, ale strasznie nieefektywne pod względem czasu i miejsca .

  • Zamknięcia: masz jakiś sposób reprezentowania funkcji, może drzewo składniowe, bardziej prawdopodobny kod maszynowy. W tych funkcjach odwołujesz się do argumentów przez odniesienie w jakiś sposób. Może być przesunięciem wskaźnika, może być liczbą całkowitą lub indeksem De Bruijn, może być nazwą. Następnie reprezentujesz funkcję jako zamknięcie : funkcja „instrukcje” (drzewo, kod itp.) W połączeniu ze strukturą danych zawierającą wszystkie wolne zmienne funkcji. Kiedy funkcja jest rzeczywiście zastosowana, w jakiś sposób wie, jak wyszukać wolne zmienne w swojej strukturze danych, używając środowisk, arytmetyki wskaźnika itp.

Jestem pewien, że istnieją inne opcje, ale są to podstawowe i podejrzewam, że prawie każda inna opcja będzie wariantem lub optymalizacją podstawowej struktury zamknięcia.

Pod względem wydajności zamknięcia prawie ogólnie działają lepiej niż przepisywanie terminów. Która z odmian jest lepsza? Zależy to w dużej mierze od twojego języka i architektury, ale podejrzewam, że „kod maszynowy ze strukturą zawierającą wolne zmienne” jest najbardziej wydajny. Ma wszystko, czego potrzebuje funkcja (instrukcje i wartości) i nic więcej, a wywoływanie nie kończy się przechodzeniem przez dłuższy czas.

Interesuje mnie zarówno bieżący algorytm kodowania, jak popularne języki funkcjonalne (Haskell, ML)

Nie jestem ekspertem, ale jestem 99%, że większość smaków ML używa pewnych odmian zamknięć, które opisuję, aczkolwiek z pewnymi optymalizacjami. Zobacz to z perspektywy (prawdopodobnie nieaktualnej).

Haskell robi coś nieco bardziej skomplikowanego ze względu na leniwą ocenę: wykorzystuje Ponowne Pisanie Grafów Bez Tagu .

a także w najbardziej wydajnym, jaki można osiągnąć.

Co jest najbardziej wydajne? Nie ma implementacji, która byłaby najbardziej wydajna dla wszystkich danych wejściowych, więc otrzymujesz implementacje, które są średnio wydajne, ale każda z nich będzie wyróżniać się w różnych scenariuszach. Dlatego nie ma określonego rankingu najbardziej lub najmniej wydajnych.

Tu nie ma magii. Aby zapisać funkcję, musisz w jakiś sposób zapisać jej wolne wartości, w przeciwnym razie kodujesz mniej informacji niż sama funkcja. Być może możesz zoptymalizować niektóre z darmowych wartości za pomocą częściowej oceny, ale jest to ryzykowne z punktu widzenia wydajności i musisz być ostrożny, aby upewnić się, że zawsze się zatrzymuje.

A może możesz użyć jakiegoś rodzaju kompresji lub sprytnego algorytmu, aby uzyskać oszczędność miejsca. Ale wtedy albo zamieniasz czas na przestrzeń, albo jesteś w sytuacji, w której zoptymalizowałeś się w niektórych przypadkach, a spowolniłeś w przypadku innych.

Można zoptymalizować dla wspólnej sprawy, ale co wspólna sprawa to może zmienić się na języku, obszar zastosowania, itp rodzaj kodu, który jest szybki do gier wideo (liczba skrzypienie, ciasne pętle z dużym wejściem) jest prawdopodobnie inny niż co jest szybkie dla kompilatora (przechodzenie przez drzewa, listy robocze itp.).

Punkt bonusowy: Czy istnieje takie kodowanie, które odwzorowuje liczby całkowite zakodowane w funkcji na liczby całkowite natywne (krótkie, całkowite itp. W C). Czy to w ogóle możliwe?

Nie, to nie jest możliwe. Problem polega na tym, że rachunek lambda nie pozwala introspekcji terminów. Gdy funkcja pobiera argument tego samego typu co liczba kościelna, musi być w stanie ją wywołać, bez sprawdzania dokładnej definicji tej liczby. Tak właśnie jest z kodowaniem Kościoła: jedyne, co możesz z nimi zrobić, to zadzwonić do nich i możesz symulować wszystko, co jest przydatne, ale nie bez kosztów.

Co ważniejsze, liczby całkowite zajmują każde możliwe kodowanie binarne. Więc jeśli lambdy byłyby reprezentowane jako ich liczby całkowite, nie byłoby sposobu reprezentowania lambd nie liczących kościoła! Lub wprowadziłbyś flagę wskazującą, czy lambda jest liczbą, czy nie, ale wtedy wszelka pożądana wydajność prawdopodobnie zniknie z okna.

EDYCJA: Od momentu napisania tego, uświadomiłem sobie trzecią opcję implementacji funkcji wyższego rzędu: defunkcjonalizację . Tutaj każde wywołanie funkcji zmienia się w dużą switchinstrukcję, w zależności od tego, która abstrakcja lambda została podana jako funkcja. Kompromis polega na tym, że jest to transformacja całego programu: nie można osobno skompilować części, a następnie połączyć w ten sposób, ponieważ trzeba wcześniej przygotować komplet abstrakcji lambda.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.