&Notepad

Will Crichton – September 9, 2018
Mam problem z wyrażeniem „programowanie systemów”. Dla mnie zawsze wydawało się, że niepotrzebnie łączy dwa pomysły: programowanie niskopoziomowe (zajmowanie się szczegółami implementacji maszyny) i projektowanie systemów (tworzenie i zarządzanie złożonym zestawem współdziałających komponentów). Dlaczego tak jest? Od jak dawna jest to prawdą? I co moglibyśmy zyskać dzięki ponownemu zdefiniowaniu idei systemów?

Lata 70: Udoskonalanie montażu

Podróżujmy wstecz do początków współczesnych systemów komputerowych, aby zrozumieć, jak ewoluował ten termin. Nie wiem kto pierwotnie ukuł to wyrażenie, ale moje poszukiwania sugerują, że poważny wysiłek w definiowaniu „systemów komputerowych” rozpoczął się około wczesnych lat 70-tych. W Językach Programowania Systemów (Bergeron1 et al. 1972), autorzy mówią:

Program systemowy jest zintegrowanym zestawem podprogramów, razem tworzących całość większą niż suma jego części, i przekraczających pewien próg wielkości i/lub złożoności. Typowymi przykładami są systemy do multiprogramowania, tłumaczenia, symulacji, zarządzania informacją i dzielenia czasu. Poniżej przedstawiono częściowy zestaw właściwości, z których niektóre występują w niesystemach, a nie wszystkie muszą być obecne w danym systemie.

  1. Problem do rozwiązania jest szerokiej natury, składający się z wielu, i zazwyczaj dość zróżnicowanych, podproblemów.
  2. Program systemowy jest prawdopodobnie używany do wspierania innych programów i aplikacji, ale może być również kompletnym pakietem aplikacyjnym.
  3. Jest on przeznaczony do ciągłego „produkcyjnego” użycia, a nie do jednorazowego rozwiązania pojedynczego problemu aplikacyjnego.

Ta definicja jest dość zgodna – systemy komputerowe są wielkoskalowe, długo używane i zmienne w czasie. Jednakże, podczas gdy definicja ta jest w dużej mierze opisowa, kluczowy pomysł w dokumencie jest normatywny: opowiedzenie się za oddzieleniem języków niskiego poziomu od języków systemowych (w tym czasie przeciwstawiając asembler z FORTRAN-em).

Celem systemowego języka programowania jest dostarczenie języka, który może być używany bez zbędnego przejmowania się względami „bit twiddling”, a mimo to wygeneruje kod, który nie będzie znacząco gorszy niż wygenerowany ręcznie. Taki język powinien łączyć w sobie zwięzłość i czytelność języków wysokiego poziomu z efektywnością czasową i przestrzenną oraz możliwością „dotarcia” do maszyn i systemów operacyjnych, które są możliwe do uzyskania w języku asemblera. Czas projektowania, pisania i debugowania powinien być zminimalizowany bez narzucania niepotrzebnego narzutu na zasoby systemowe.

W tym samym czasie naukowcy z CMU opublikowali BLISS: A Language for Systems Programming (Wulf et al. 1972), opisując go jako:

Odnosimy się do BLISS jako do „języka implementacji”, chociaż przyznajemy, że termin ten jest nieco dwuznaczny, ponieważ, przypuszczalnie, wszystkie języki komputerowe są używane do implementacji czegoś. Dla nas wyrażenie to oznacza język ogólnego przeznaczenia, wyższego poziomu, w którym główny nacisk został położony na konkretne zastosowanie, mianowicie pisanie dużych, produkcyjnych systemów oprogramowania dla konkretnej maszyny. Języki specjalnego przeznaczenia, takie jak kompilatory, nie należą do tej kategorii, ani też nie zakładamy, że języki te muszą być niezależne od maszyny. W naszej definicji kładziemy nacisk na słowo „implementacja” i nie używamy słów takich jak „projekt” czy „dokumentacja”. Niekoniecznie oczekujemy, że język implementacji będzie odpowiednim narzędziem do wyrażania wstępnego projektu dużego systemu, ani do wyłącznej dokumentacji tego systemu. Koncepcje takie jak niezależność maszynowa, wyrażanie projektu i implementacji w tej samej notacji, autodokumentacja i inne, są wyraźnie pożądanymi celami i są kryteriami, według których ocenialiśmy różne języki.

Tutaj autorzy przeciwstawiają ideę „języka implementacji” jako będącego wyższego poziomu niż asembler, ale niższego niż „język projektu”. To opiera się definicji w poprzednim papierze, opowiadając się za tym, że projektowanie systemu i wdrażanie systemu powinny mieć oddzielne języki.

Oba te papiery są artefaktami badawczymi lub zwolennikami. Ostatnią pozycją do rozważenia (również z 1972 roku, roku produktywnego!) jest Systems Programming (Donovan 1972), tekst edukacyjny do nauki programowania systemowego.

Co to jest programowanie systemowe? Możesz wyobrażać sobie komputer jako pewnego rodzaju bestię, która słucha wszystkich poleceń. Mówi się, że komputery są w zasadzie ludźmi wykonanymi z metalu lub odwrotnie, ludzie są komputerami wykonanymi z krwi i kości. Kiedy jednak zbliżymy się do komputerów, zauważymy, że są one w zasadzie maszynami, które wykonują bardzo konkretne i prymitywne instrukcje. W początkach istnienia komputerów ludzie komunikowali się z nimi za pomocą włączników i wyłączników oznaczających prymitywne instrukcje. Wkrótce ludzie zapragnęli wydawać bardziej złożone instrukcje. Na przykład, chcieli móc powiedzieć X = 30 * Y; biorąc pod uwagę, że Y = 10, ile wynosi X? Dzisiejsze komputery nie są w stanie zrozumieć takiego języka bez pomocy programów systemowych. Programy systemowe (np. kompilatory, loadery, makroprocesory, systemy operacyjne) zostały stworzone po to, aby komputery były lepiej dostosowane do potrzeb ich użytkowników. Co więcej, ludzie chcieli większej pomocy w mechanice przygotowywania swoich programów.

Podoba mi się, że ta definicja przypomina nam, że systemy służą ludziom, nawet jeśli są tylko infrastrukturą, która nie jest bezpośrednio wystawiona na działanie użytkownika końcowego.

Lata 90: The rise of scripting

W latach 70. i 80. wydaje się, że większość badaczy postrzegała programowanie systemów zwykle jako kontrast do programowania asemblerowego. Po prostu nie było innych dobrych narzędzi do budowania systemów. (Nie jestem pewien, gdzie w tym wszystkim był Lisp? Żadne z zasobów, które czytałem nie powoływało się na Lispa, chociaż jestem niejasno świadomy, że maszyny Lisp istniały jednak krótko.)

Jednakże w połowie lat 90-tych nastąpiła poważna zmiana w językach programowania wraz z pojawieniem się dynamicznie napisanych języków skryptowych. Udoskonalając wcześniejsze systemy skryptowe powłoki, takie jak Bash, języki takie jak Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995) i Javascript (1995) utorowały sobie drogę do głównego nurtu. Kulminacją tych działań był wpływowy artykuł „Scripting: Higher Level Programming for the 21st Century” (Ousterhout 1998). Wyartykułował on „dychotomię Ousterhouta” pomiędzy „systemowymi językami programowania” a „językami skryptowymi”.”

Języki skryptowe są zaprojektowane do innych zadań niż języki programowania systemowego, a to prowadzi do fundamentalnych różnic w tych językach. Języki programowania systemowego zostały zaprojektowane do budowania struktur danych i algorytmów od podstaw, począwszy od najbardziej prymitywnych elementów komputera, takich jak słowa pamięci. W przeciwieństwie do nich, języki skryptowe zostały zaprojektowane do klejenia: zakładają istnienie zestawu wydajnych komponentów i są przeznaczone przede wszystkim do łączenia komponentów ze sobą. Języki programowania systemowego są silnie typowane, aby pomóc w zarządzaniu złożonością, podczas gdy języki skryptowe są pozbawione typów, aby uprościć połączenia między komponentami i zapewnić szybkie tworzenie aplikacji. Kilka ostatnich trendów, takich jak szybsze maszyny, lepsze języki skryptowe, rosnące znaczenie graficznych interfejsów użytkownika i architektur komponentów, a także rozwój Internetu, znacznie zwiększyły możliwości zastosowania języków skryptowych.

Na poziomie technicznym, Ousterhout skontrastował skrypty z systemami wzdłuż osi bezpieczeństwa typu i instrukcji na deklarację, jak pokazano powyżej. Na poziomie projektowym, scharakteryzował nowe role dla każdej klasy języka: programowanie systemowe służy do tworzenia komponentów, a skryptowe do ich sklejania.

Mniej więcej w tym czasie, języki statycznie typowane, ale zbierające śmieci również zaczęły zdobywać popularność. Java (1995) i C# (2000) zamieniły się w tytanów, których znamy dzisiaj. Chociaż te dwa języki nie są tradycyjnie uważane za „języki programowania systemowego”, zostały użyte do zaprojektowania wielu największych systemów oprogramowania na świecie. Ousterhout nawet wyraźnie wspomniał „w świecie Internetu, który teraz nabiera kształtów, Java jest używana do programowania systemowego.”

2010s: Granice się zacierają

W ostatniej dekadzie granica między językami skryptowymi a językami programowania systemowego zaczęła się zacierać. Firmy takie jak Dropbox były w stanie zbudować zaskakująco duże i skalowalne systemy na samym Pythonie. Javascript jest używany do renderowania w czasie rzeczywistym, złożonych interfejsów użytkownika na miliardach stron internetowych. Stopniowe typowanie zyskało na sile w Pythonie, Javascript i innych językach skryptowych, umożliwiając przejście od kodu „prototypowego” do kodu „produkcyjnego” poprzez przyrostowe dodawanie statycznych informacji o typie.

Na panelu zatytułowanym Systems Programming in 2014 and Beyond wystąpiły największe umysły stojące za dzisiejszymi samookreślonymi językami systemowymi: Bjarne Stroustrup (twórca C++), Rob Pike (twórca Go), Andrei Alexandrescu (deweloper D) i Niko Matsakis (deweloper Rust). Na pytanie „czym jest język programowania systemowego w 2014 roku”, odpowiedzieli (transkrypcja zredagowana):

  • Niko Matsakis: Pisanie aplikacji po stronie klienta. Polarne przeciwieństwo tego, do czego Go jest przeznaczony. W tych aplikacjach masz wysokie potrzeby opóźnień, wysokie wymagania bezpieczeństwa, wiele wymagań, które nie pojawiają się po stronie serwera.
  • Bjarne Stroustrup: Programowanie systemów wyszło z dziedziny, w której miałeś do czynienia ze sprzętem, a następnie aplikacje stały się bardziej skomplikowane. Musisz poradzić sobie z kompleksowością. Jeśli masz jakiekolwiek problemy z istotnymi ograniczeniami zasobów, jesteś w domenie programowania systemowego. Jeśli potrzebujesz drobnoziarnistej kontroli, to również jesteś w domenie programowania systemowego. To właśnie ograniczenia decydują o tym, czy jest to programowanie systemowe. Czy kończy ci się pamięć? Czy kończy ci się czas?
  • Rob Pike: Kiedy po raz pierwszy ogłosiliśmy Go, nazwaliśmy go językiem programowania systemowego i trochę tego żałuję, ponieważ wiele osób założyło, że jest to język do pisania systemów operacyjnych. Powinniśmy byli nazwać go językiem pisania dla serwerów, czyli tym, o czym tak naprawdę myśleliśmy. Teraz rozumiem, że to, co mamy, to język infrastruktury chmury. Inna definicja programowania systemowego to rzeczy, które działają w chmurze.
  • Andrei Alexandrescu: Mam kilka papierków lakmusowych do sprawdzenia, czy coś jest językiem programowania systemowego. Systemowy język programowania musi być w stanie pozwolić ci napisać w nim własny alokator pamięci. Powinieneś być w stanie podrobić liczbę do wskaźnika, ponieważ tak działa sprzęt.

Czy w programowaniu systemowym chodzi więc o wysoką wydajność? Ograniczenia zasobów? Kontrola nad sprzętem? Infrastruktura chmury? Wydaje się, ogólnie rzecz biorąc, że języki z kategorii C, C ++, Rust i D są rozróżniane pod względem ich poziomu abstrakcji od maszyny. Języki te odsłaniają szczegóły sprzętu bazowego, takie jak alokacja/układ pamięci i drobnoziarniste zarządzanie zasobami.

Inny sposób, aby o tym pomyśleć: kiedy masz problem z wydajnością, jak wiele swobody masz, aby go rozwiązać? Wspaniałą częścią niskopoziomowych języków programowania jest to, że kiedy zidentyfikujesz nieefektywność, jest w twojej mocy, aby wyeliminować wąskie gardło poprzez staranną kontrolę nad szczegółami maszyny. Wektoryzuj tę instrukcję, zmień rozmiar tej struktury danych, aby utrzymać ją w pamięci podręcznej, i tak dalej. W ten sam sposób typy statyczne zapewniają większą pewność3, jak „te dwie rzeczy, które próbuję dodać, są zdecydowanie liczbami całkowitymi”, języki niskiego poziomu zapewniają większą pewność, że „ten kod wykona się na maszynie, jak określiłem.”

Dla kontrastu, optymalizacja języków interpretowanych jest absolutną dżunglą. Niewiarygodnie trudno jest wiedzieć, czy runtime będzie konsekwentnie wykonywał twój kod w sposób, którego oczekujesz. To jest dokładnie ten sam problem z auto-paralelizacją kompilatorów – „auto-wektoryzacja nie jest modelem programowania” (zobacz The story of ispc). To jak pisanie interfejsu w Pythonie, myśląc „cóż, mam nadzieję, że ktokolwiek wywoła tę funkcję, da mi int.”

Dzisiaj: …więc czym jest programowanie systemowe?

To sprowadza mnie z powrotem do mojego pierwotnego gripe’a. To, co wielu ludzi nazywa programowaniem systemowym, ja traktuję jako programowanie niskopoziomowe – odsłaniające szczegóły maszyny. Ale co w takim razie z systemami? Przypomnij sobie naszą definicję z 1972 roku:

  1. Problem do rozwiązania jest szerokiej natury, składający się z wielu, i zazwyczaj dość zróżnicowanych, podproblemów.
  2. Program systemowy jest prawdopodobnie używany do wspierania innych programów i aplikacji, ale może być także kompletnym pakietem aplikacji.
  3. Jest zaprojektowany do ciągłego „produkcyjnego” użycia, a nie jako jednorazowe rozwiązanie pojedynczego problemu z aplikacjami.
  4. Prawdopodobnie będzie stale ewoluował pod względem liczby i typów obsługiwanych funkcji.
  5. Program systemowy wymaga pewnej dyscypliny lub struktury, zarówno wewnątrz modułów, jak i pomiędzy nimi (tj. „komunikacji”), i jest zwykle projektowany i wdrażany przez więcej niż jedną osobę.

Wydaje się, że są to o wiele bardziej kwestie związane z inżynierią oprogramowania (modułowość, ponowne użycie, ewolucja kodu) niż kwestie wydajności niskiego poziomu. Co oznacza, że każdy język programowania, który stawia sobie za priorytet rozwiązanie tych problemów, jest językiem programowania systemowego! To wciąż nie oznacza, że każdy język jest językiem programowania systemowego. Dynamiczne języki programowania są prawdopodobnie wciąż dalekie od języków systemowych, ponieważ dynamiczne typy i idiomy takie jak „prośba o wybaczenie, nie o pozwolenie” nie sprzyjają dobrej jakości kodu.

Co zatem daje nam ta definicja? Oto gorące ujęcie: języki funkcyjne, takie jak OCaml i Haskell, są o wiele bardziej zorientowane na systemy niż języki niskiego poziomu, takie jak C lub C++. Kiedy uczymy studentów programowania systemowego, powinniśmy włączyć zasady programowania funkcjonalnego, takie jak wartość niezmienności, wpływ bogatych systemów typów na poprawę projektowania interfejsów i użyteczność funkcji wyższego rzędu. Szkoły powinny uczyć zarówno programowania systemowego, jak i programowania niskopoziomowego.

Czy istnieje rozróżnienie między programowaniem systemowym a dobrą inżynierią oprogramowania? Nie bardzo, ale problemem jest to, że inżynieria oprogramowania i programowanie niskopoziomowe są często nauczane w oderwaniu od siebie. Podczas gdy większość zajęć z inżynierii oprogramowania jest zazwyczaj skoncentrowana na Javie „pisz dobre interfejsy i testy”, powinniśmy również uczyć studentów o tym, jak projektować systemy, które mają znaczące ograniczenia zasobów. Być może nazywamy programowanie niskopoziomowe „systemami”, ponieważ wiele z najbardziej interesujących systemów oprogramowania jest niskopoziomowych (np. bazy danych, sieci, systemy operacyjne, itp.). Ponieważ systemy niskopoziomowe mają wiele ograniczeń, wymagają od projektantów kreatywnego myślenia.

Innym założeniem jest to, że programiści niskiego poziomu powinni starać się zrozumieć, jakie pomysły w projektowaniu systemów można zaadaptować do radzenia sobie z rzeczywistością współczesnego sprzętu. Myślę, że społeczność Rust była niezwykle innowacyjna w tym względzie, patrząc na to jak dobre zasady projektowania oprogramowania/programowania funkcyjnego mogą być zastosowane do problemów niskiego poziomu (np. futures, obsługa błędów, lub oczywiście bezpieczeństwo pamięci).

Podsumowując, to co nazywamy „programowaniem systemowym” myślę, że powinno być nazywane „programowaniem niskiego poziomu”. Projektowanie systemów komputerowych jako dziedzina jest zbyt ważne, by nie mieć swojej własnej nazwy. Wyraźne rozdzielenie tych dwóch idei zapewnia większą jasność konceptualną w przestrzeni projektowania języków programowania, a także otwiera drzwi do dzielenia się spostrzeżeniami z tych dwóch przestrzeni: jak możemy zaprojektować system wokół maszyny i vice versa?

Proszę kierować komentarze do mojej skrzynki odbiorczej na [email protected] lub Hacker News.

  1. Fajny fakt: dwaj autorzy tego artykułu, R. Bergeron i Andy Van Dam, są członkami-założycielami społeczności graficznej i konferencji SIGGRAPH. Część stałego wzorca, w którym badacze grafiki wyznaczają trendy w projektowaniu systemów, np. powstanie GPGPU.

  2. Obowiązkowy link do Skalowalności! Ale jakim kosztem?

  3. Idealnie statyczne typy są 100% gwarancją (lub zwrotem pieniędzy), ale w praktyce większość języków pozwala na pewną ilość Obj.magic.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.