Data publikacji: 29 stycznia 2025 r.
Wywóz śmieci z WebAssembly (WasmGC)
Istnieją 2 rodzaje języków programowania: języki z zbieraniem elementów i języki, które wymagają ręcznego zarządzania pamięcią. Przykładami takich języków są m.in. Kotlin, PHP i Java. Przykładami takich języków są C, C++ i Rust. Zasadniczo języki programowania wyższego poziomu mają funkcję zbierania elementów usuniętych jako standardową funkcję.
Upraszczając, zamiatanie polega na próbie odzyskania pamięci przydzielonej przez program, której już nie używa. Takie dane są nazywane śmieciami. Istnieje wiele strategii implementacji zbierania elementów. Jednym z najłatwiejszych do zrozumienia jest liczenie odwołań, którego celem jest zliczanie odwołań do obiektów w pamięci.
Może to wyglądać na początek, ale języki programowania są implementowane w innych językach programowania. Na przykład środowisko uruchomieniowe PHP jest głównie implementowane w języku C. Jeśli deweloperzy chcą skompilować język, np. PHP, do Wasm, muszą zwykle skompilować wszystkie części, takie jak parsowanie języka, obsługa biblioteki, zbieranie elementów i inne kluczowe komponenty.
Wasm działa w przeglądarce w kontekście języka hosta JavaScript. W Chrome kod JavaScript i Wasm są wykonywane w V8, czyli mechanizmie JavaScript o otwartym kodzie źródłowym od Google. Poza tym V8 ma już swojego własnego zbieracza śmieci. Oznacza to, że deweloperzy korzystający np. z PHP skompilowanego na Wasm w końcu wysyłają do przeglądarki implementację kolektora pamięci dla przeniesionego języka (PHP), która już ma takiego kolektora, co jest niepotrzebnym marnotrawstwem. Właśnie w tym przypadku przydaje się WasmGC.
Więcej informacji o WMGC znajdziesz w artykule WebAssembly Garbage Collection (WMGC) jest teraz domyślnie włączony w Chrome. Jeśli chcesz dowiedzieć się więcej, przeczytaj wpis na blogu V8 Nowy sposób na efektywne wykorzystanie języków programowania z zbieraniem pamięci podręcznej w WebAssembly.
Optymalizacja wywołań ogonowych w Wasm
Połączenie jest w pozycji ogonowej, jeśli jest ostatnią instrukcją wykonaną przed powrotem z bieżącej funkcji. Kompilatory mogą optymalizować takie wywołania, odrzucając ramkę wywołującego i zastępując wywołanie skokiem. Jest to szczególnie przydatne w przypadku funkcji rekurencyjnych. Weźmy na przykład funkcję C, która zlicza elementy listy połączonej:
int sum(List* list, int acc) {
if (list == nullptr) return acc;
return sum(list->next, acc + list->val);
}
W przypadku zwykłego wywołania zajmuje to miejsce na stosie O(n): każdy element listy dodaje nowy element do stosu wywołań. W przypadku wystarczająco długiej listy może to bardzo szybko spowodować przepełnienie stosu. Zastąpienie wywołania skokiem powoduje, że optymalizacja wywołania ogonowego skutecznie zamienia tę funkcję rekurencyjną w pętlę, która używa miejsca na stosie O(1):
int sum(List* list, int acc) {
while (list != nullptr) {
acc = acc + list->val;
list = list->next;
}
return acc;
}
Ta optymalizacja jest szczególnie ważna w przypadku języków funkcjonalnych. W ich przypadku często korzysta się z funkcji rekurencyjnych, a czyste języki, takie jak Haskell, nie mają nawet struktur sterowania pętlą. Każdy rodzaj niestandardowej iteracji zwykle w jakimś stopniu wykorzystuje rekurencję. Bez optymalizacji wywołania rekurencji bardzo szybko wystąpiłoby przepełnienie stosu w przypadku każdego nietrywialnego programu, który w przeciwnym razie szybko zabrałby całą pamięć stosu.
Początkowo WebAssembly nie zezwalał na takie optymalizacje wywołań o długim ogonie, ale zmieniło się to wraz z propozycją rozszerzenia wywołań o długim ogonie. Aby dowiedzieć się więcej, przeczytaj artykuł WebAssembly tail calls na blogu V8.
Podsumowanie
Teraz, gdy optymalizacje WasmGC i wywołania ogonowego są dostępne jako nowa podstawa, więcej aplikacji może korzystać z tych funkcji, aby zwiększyć wydajność. Tak zrobiły na przykład Arkusze Google, które przeniosły element obliczeniowy do WasmGC.