podobnie jak wielu programistów, Rustem interesuję się od dłuższego czasu. Nie tylko dlatego, że pojawia się w wielu nagłówkach Hacker News, lub z powodu nowatorskiego podejścia języka do bezpieczeństwa i wydajności, ale także dlatego, że ludzie wydają się mówić o tym ze szczególnym poczuciem miłości i podziwu. Co więcej, Rust jest dla mnie szczególnie interesujący, ponieważ ma te same cele i cechy mojego ulubionego języka: Swift. Ponieważ niedawno poświęciłem czas na wypróbowanie Rusta w małych projektach osobistych, chciałem poświęcić trochę czasu na udokumentowanie moich wrażeń z języka, zwłaszcza w porównaniu do języka Swift.

duży obraz

Rust i Swift mają wiele wspólnego: oba są skompilowanymi językami z potężnymi, nowoczesnymi systemami typu i koncentrują się na bezpieczeństwie. Funkcje takie jak typy algebraiczne i pierwszorzędna obsługa wartości opcjonalnych pomagają przenieść wiele klas błędów z czasu uruchomieniowego do czasu kompilacji w obu tych językach.

czym różnią się te języki? Najlepszym sposobem na scharakteryzowanie różnicy jest:

Swift ułatwia pisanie bezpiecznego kodu.
Rust utrudnia pisanie niebezpiecznego kodu.

te dwa stwierdzenia mogą brzmieć równoważnie, ale istnieje ważne rozróżnienie. Oba języki mają narzędzia do osiągnięcia bezpieczeństwa, ale robią różne kompromisy, aby to osiągnąć: Swift priorytetyzuje ergonomię kosztem wydajności, podczas gdy Rust priorytetuje wydajność kosztem ergonomii.

Wymiana: Wydajność a ergonomia

największą różnicą w priorytecie jest podejście tych języków do zarządzania pamięcią. Zacznę od Rusta, ponieważ podejście języka do zarządzania pamięcią jest jednym z jego unikalnych punktów sprzedaży.

w Rust pamięć jest przede wszystkim zarządzana statycznie (tak, istnieją inne tryby zarządzania pamięcią, takie jak liczenie referencji, ale na razie je zignorujemy). Oznacza to, że kompilator Rusta analizuje Twój program i zgodnie z zestawem reguł decyduje, kiedy pamięć powinna zostać przydzielona i zwolniona.

w celu zapewnienia bezpieczeństwa, Rust stosuje nowatorską strategię zwaną sprawdzaniem pożyczkowym. Sposób, w jaki to działa w praktyce, polega na tym, że jako programista, za każdym razem, gdy przekazujesz zmienną (tj. odniesienie do lokalizacji pamięci), musisz określić, czy odniesienie jest zmienne, czy niezmienne. Kompilator następnie używa zestawu reguł, aby upewnić się, że nie można zmutować pojedynczego kawałka pamięci w dwóch miejscach na raz, co czyni możliwym do udowodnienia, że twój program nie ma wyścigów danych.

to podejście ma kilka bardzo korzystnych właściwości w odniesieniu do wykorzystania pamięci i wydajności. Zapożyczenie sprawdzanie może być bardzo oszczędne z pamięcią, ponieważ na ogół unika kopiowania wartości. Pozwala to również uniknąć kosztów związanych z wydajnością takich rozwiązań, jak garbage collection, ponieważ praca jest wykonywana w czasie kompilacji, a nie w czasie wykonywania.

jednak ma pewne wady, jeśli chodzi o łatwość użycia. Ze względu na charakter własności w Rust, istnieją pewne wzorce projektowe, które po prostu nie działają w Rust. Na przykład nie jest trywialne zaimplementowanie czegoś w rodzaju podwójnie powiązanej listy lub zmiennej globalnej. Z czasem staje się to bardziej intuicyjne i istnieją obejścia tych problemów, ale Rust z pewnością nakłada na programistę ograniczenia, których nie ma w innych językach.

chociaż nie jest to tak często omawiane jak Rust, Swift ma również ciekawą historię, jeśli chodzi o zarządzanie pamięcią.

Swift ma dwa podstawowe typy zmiennych: typy referencyjne i typy wartości. Ogólnie rzecz biorąc, typy referencyjne są przydzielane stertami i są zarządzane przez liczenie odniesień. Oznacza to, że w trybie runtime liczba odniesień do liczonego obiektu jest śledzona, a obiekt jest dealokowany, gdy liczba osiągnie zero. Liczenie referencji w Swift jest zawsze atomowe: oznacza to, że za każdym razem, gdy liczba referencji się zmienia, musi nastąpić synchronizacja między wszystkimi wątkami procesora. Ma to tę zaletę, że eliminuje możliwość omyłkowego uwolnienia odniesienia w aplikacji wielowątkowej, ale wiąże się ze znacznym kosztem wydajności, ponieważ synchronizacja procesora jest bardzo kosztowna.

Rust ma również narzędzia do liczenia referencji i atomowego liczenia referencji, ale są one opcjonalne, a nie domyślne.

typy wartości natomiast są ogólnie przydzielane na stos, a ich pamięć jest zarządzana statycznie. Jednak zachowanie typów wartości w języku Swift różni się znacznie od sposobu, w jaki Rust obsługuje pamięć. W języku Swift typy wartości mają tak zwane zachowanie „Kopiuj przy zapisie”, co oznacza, że za każdym razem, gdy typ wartości jest zapisywany do nowej zmiennej lub przekazywany do funkcji, kopiowana jest.

kopiowanie przy zapisie domyślnie osiąga te same cele: jako programista nigdy nie musisz się martwić o tajemniczą zmianę wartości z powodu nieoczekiwanego efektu ubocznego w innym miejscu programu. Wymaga również nieco mniejszego obciążenia poznawczego niż sprawdzanie zapożyczeń, ponieważ istnieją całe klasy błędów kompilacji związanych z własnością w Rust, które po prostu nie istnieją w Swift. Jednak ma to swoją cenę: te dodatkowe kopie wymagają dodatkowego użycia pamięci i cykli procesora.

w Rust możliwe jest również kopiowanie wartości w celu wyciszenia błędów sprawdzania, ale to dodaje wizualny szum, ponieważ kopie muszą być jawnie określone.

oto dobry przykład kompromisów tych dwóch języków: Swift daje pewne ogólne założenia dotyczące zarządzania pamięcią przy jednoczesnym zachowaniu poziomu bezpieczeństwa. To trochę tak, jak programista C++ może obsługiwać pamięć zgodnie z najlepszymi praktykami, zanim poświęci dużo uwagi optymalizacji. To sprawia, że bardzo łatwo jest wskoczyć i napisać kod bez zastanawiania się nad niskimi szczegółami, a także osiągnąć pewne podstawowe bezpieczeństwo i poprawność w czasie wykonywania gwarantuje, że nie uzyskasz w języku takim jak Python, a nawet Golang. Jednak ma pewne klify wydajności, które łatwo spaść, nawet nie zdając sobie z tego sprawy, dopóki nie uruchomisz programu. Możliwe jest pisanie wysokowydajnego kodu Swift, ale często wymaga to starannego profilowania i optymalizacji.

Rust, z drugiej strony, daje Ci wiele specyficznych narzędzi do określania sposobu zarządzania pamięcią, a następnie nakłada pewne ograniczenia na sposób korzystania z nich, aby uniknąć niebezpiecznych zachowań. Daje to bardzo ładne właściwości wydajności od razu po wyjęciu z pudełka, ale wymaga to podjęcia dodatkowych kosztów poznawczych związanych z zapewnieniem przestrzegania wszystkich zasad.

moja opinia z tego wynika, że chociaż te języki mają pewne wspólne cele, mają zasadniczo różne cechy, które nadają się do różnych przypadków użycia. Rust, na przykład, wydaje się oczywistym wyborem dla czegoś takiego jak embedded development, gdzie optymalne wykorzystanie pamięci i cykli procesora jest niezwykle ważne, a pętla code-compile-run może być wolniejsza, więc warto złapać każdy możliwy problem podczas kompilacji. Podczas gdy Swift może być lepszym wyborem dla czegoś takiego jak data science lub logika bezserwerowa, gdzie wydajność jest problemem drugorzędnym i warto pracować bliżej domeny problemu bez konieczności rozważania wielu szczegółów niskiego poziomu.

w każdym razie, będę bardzo zainteresowany śledzeniem obu tych języków w przyszłości, i będę śledzić ten post z większą ilością obserwacji na temat porównania między Swift i Rust.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.