Co decyduje o „szybkości” języka programowania?


38

Załóżmy, że program został napisany w dwóch różnych językach, niech to będą język X i język Y, jeśli ich kompilatory generują ten sam kod bajtowy, dlaczego powinienem używać języka X zamiast języka Y? Co określa, że ​​jeden język jest szybszy od drugiego?

Pytam o to, ponieważ często zdarza się, że ludzie mówią takie rzeczy: „C jest najszybszym językiem, ATS jest językiem szybkim jak C”. Chciałem zrozumieć definicję „szybkiego” dla języków programowania.


21
Jeśli jeden program jest szybszy od drugiego, oznacza to, że nie mogą mieć tego samego kodu bajtowego.
svick

5
Języki to tylko pojęcie używane do pisania programów, więc nie można tak naprawdę mówić o szybkości języka.
Juho

1
@Raphael Czuję, że to nie na temat, niejasne i zbyt szerokie. Chociaż temat ten lepiej nadaje się do inżynierii oprogramowania , podejrzewam, że zostałby tam zamknięty jako „zbyt szeroki”.
David Richerby

2
Realizacja na bok, „prędkość” jest niejednoznaczne, istnieją różne prędkości wykonawcze, kompilacja, uruchamianie i debugowanie, a ty zazwyczaj będzie handel off niektóre dla innych (w przeciwnym razie byłyby wszystkie korzystania z języka programowania)
Nick T

Jak wyżej. Języki nie generują tego samego kodu bajtowego. Niektóre języki są łatwiejsze do parsowania na kod bajtowy. Niektóre mają wyższy poziom abstrakcji.
superluminarny

Odpowiedzi:


23

Istnieje wiele powodów, dla których można rozważyć wybór języka X zamiast języka Y. Czytelność programu, łatwość programowania, przenośność na wiele platform, istnienie dobrych środowisk programowania może być takimi przyczynami. Zastanowię się jednak tylko nad szybkością wykonania, zgodnie z żądaniem zawartym w pytaniu. Pytanie wydaje się nie uwzględniać na przykład tempa rozwoju.

Dwa języki mogą się kompilować do tego samego kodu bajtowego, ale nie oznacza to, że zostanie wygenerowany ten sam kod,

W rzeczywistości bytecode jest kodem tylko dla konkretnej maszyny wirtualnej. Ma zalety inżynieryjne, ale nie wprowadza zasadniczych różnic w bezpośredniej kompilacji dla określonego harware. Równie dobrze możesz rozważyć porównanie dwóch języków skompilowanych do bezpośredniego wykonania na tym samym komputerze.

To powiedziawszy, kwestia względnej szybkości języków jest stara i sięga pierwszych kompilatorów.

Przez wiele lat w tamtych czasach profesjonalista uważał, że odręczny kod jest szybszy niż kod skompilowany. Innymi słowy, język maszynowy był uważany za szybszy niż języki wysokiego poziomu, takie jak Cobol lub Fortran. I było zarówno szybsze, jak i zwykle mniejsze. Języki wysokiego poziomu wciąż się rozwijały, ponieważ były znacznie łatwiejsze w użyciu dla wielu osób, które nie były informatykami. Koszt użycia języków wysokiego poziomu miał nawet nazwę: współczynnik rozszerzenia, który może dotyczyć wielkości generowanego kodu (bardzo ważna kwestia w tamtych czasach) lub liczby faktycznie wykonanych instrukcji. Pomysł był głównie eksperymentalny, ale początkowo stosunek był większy niż 1, ponieważ kompilatory wykonały dość prostą robotę według dzisiejszych standardów.

W ten sposób język maszynowy był szybszy niż powiedzieć Fortran.

Oczywiście zmieniło się to z biegiem lat, gdy kompilatory stały się bardziej wyrafinowane, do tego stopnia, że ​​programowanie w asemblerze jest teraz bardzo rzadkie. W przypadku większości aplikacji programy w asemblerze słabo konkurują z kodem generowanym przez optymalizację kompilatorów.

To pokazuje, że jednym z głównych problemów jest jakość kompilatorów dostępnych dla rozważanego języka, ich zdolność do analizy kodu źródłowego i odpowiedniej jego optymalizacji.

Ta zdolność może zależeć w pewnym stopniu od cech języka, aby podkreślić strukturalne i matematyczne właściwości źródła, aby ułatwić pracę kompilatorowi. Na przykład język może pozwalać na dołączanie instrukcji o właściwościach algebraicznych funkcji zdefiniowanych przez użytkownika, aby umożliwić kompilatorowi wykorzystanie tych właściwości do celów optymalizacji.

Proces kompilacji może być łatwiejszy, dzięki czemu powstaje lepszy kod, gdy paradygmat programowania języka jest bliższy funkcjom maszyn, które zinterpretują kod, zarówno rzeczywistej, jak i wirtualnej.

Inną kwestią jest to, czy paradygmaty zaimplementowane w języku są zamknięte dla rodzaju programowanego problemu. Należy się spodziewać, że język programowania wyspecjalizowany w określonych paradygmatach programowania będzie bardzo skutecznie kompilował funkcje związane z tym paradygmatem. Dlatego wybór języka programowania może zależeć, dla jasności i szybkości, od wyboru języka programowania dostosowanego do rodzaju programowanego problemu.

Popularność C w programowaniu systemu wynika prawdopodobnie z faktu, że C jest zbliżony do architektury maszyny, a programowanie systemu jest również bezpośrednio związane z tą architekturą.

Niektóre inne problemy będą łatwiejsze do zaprogramowania, dzięki szybszemu wykonaniu przy użyciu programowania logiki i języków rozwiązywania ograniczeń .

Złożone systemy reaktywne mogą być bardzo skutecznie programowane za pomocą specjalistycznych synchronicznych języków programowania, takich jak Esterel, który zawiera bardzo specjalistyczną wiedzę na temat takich systemów i generuje bardzo szybki kod.

Lub, na przykład, niektóre języki są wysoce wyspecjalizowane, takie jak języki opisu składni używane do programowania parserów. Parser generator to nic innego jak kompilator dla tych języków. Oczywiście nie jest to kompletna metoda Turinga, ale te kompilatory są wyjątkowo dobre ze względu na swoją specjalizację: tworzenie wydajnych programów parsujących. Ponieważ dziedzina wiedzy jest ograniczona, techniki optymalizacji mogą być bardzo wyspecjalizowane i bardzo dokładnie dostrojone. Te generatory parsera są zwykle znacznie lepsze niż to, co można uzyskać, pisząc kod w innym języku. Istnieje wiele wysoce wyspecjalizowanych języków z kompilatorami, które wytwarzają doskonały i szybki kod dla ograniczonej klasy problemów.

Dlatego przy pisaniu dużego systemu może być wskazane, aby nie polegać na jednym języku, ale wybrać najlepszy język dla różnych składników systemu. To oczywiście rodzi problemy z kompatybilnością.

Inną kwestią, która często ma znaczenie, jest po prostu istnienie wydajnych bibliotek dla programowanych tematów.

Wreszcie, szybkość nie jest jedynym kryterium i może być w konflikcie z innymi kryteriami, takimi jak bezpieczeństwo kodu (na przykład w odniesieniu do złych danych wejściowych lub odporność na błędy systemowe), użycie pamięci, łatwość programowania (chociaż zgodność paradygmatu może w rzeczywistości pomóc ), rozmiar kodu obiektu, łatwość konserwacji programu itp.

Prędkość nie zawsze jest najważniejszym parametrem. Może również przybierać różne formy, na przykład złożoność, która może być średnią złożonością lub gorszą złożonością przypadku. Ponadto w dużym systemie, podobnie jak w mniejszym programie, są części, w których prędkość ma kluczowe znaczenie, i inne, w których nie ma to większego znaczenia. I nie zawsze łatwo jest to ustalić z góry.


Dzięki. To coś takiego. Szukałem. Trudno było znaleźć materiał na ten temat. To wystarczająco wyjaśniło.
Rodrigo Valente

@ RodrigoAraújoValente Dzięki, możesz przyjrzeć się temu pytaniu . Ekstremistyczny pogląd jest taki, że kompilator dla języka L może po prostu spakować interpreter dla L z kodem źródłowym programu, bez robienia czegokolwiek. Następnie możesz zrobić to lepiej, próbując zoptymalizować obliczenia pakietu (częściowa ocena). Im bardziej zoptymalizujesz i tym szybciej będzie twój język. Pytanie brzmi: co może pomóc w optymalizacji? Odpowiedzi mogą obejmować dobrą znajomość specjalistycznej tematyki, pomoc programisty, zaawansowane analizy itp.
babou

Przez „ten sam kod bajtowy” domyślam się, że masz na myśli „ten sam rodzaj reprezentacji wykonywalnej”. Oczywiście identyczne pliki wykonywalne będą działać z tą samą prędkością (przy założeniu tego samego systemu wykonywania). (Prawdopodobnie spojrzałbym na to z perspektywy informacji / komunikacji. Teoretycznie programista może wiedzieć wszystko o programie i sprzęcie, podczas gdy język często ogranicza komunikację (ograniczając dozwoloną lub przydatną transformację), a kompilator może nie wiedzieć szczegóły mikroarchitektoniczne Kompilacja i rozwój kompilatora są analogiczne do programowania i szkolenia ...
Paul A. Clayton

W ten sposób dochodzimy do tego, w czym ludzie są ogólnie dobrzy (np. Rozpoznając ogólne wzorce) w porównaniu do tego, w czym komputery są zazwyczaj dobre (np. Prowadzenie dokumentacji i „arytmetyka”). Ponadto komunikacja informacji o środowisku wykonawczym jest często bardziej przyjazna dla komputera (optymalizacja za pomocą profilu może przezwyciężyć pewien brak informacji przekazywanych za pośrednictwem języka programowania). Optymalizacja dynamiczna byłaby niepraktyczna dla ludzkich programistów ...
Paul A. Clayton

(choć to samo można powiedzieć o pisaniu całego oprogramowania przez wykwalifikowanych programistów ukierunkowanych na konkretny sprzęt, do czasu zoptymalizowania oprogramowania nie tylko sprzęt zmieniłby się, ale oprogramowanie byłoby przestarzałe).
Paul A. Clayton

16

Chociaż wszystko ostatecznie działa na procesorze * , istnieją różne różnice między różnymi językami. Oto kilka przykładów.

Języki interpretowane Niektóre języki są interpretowane, a nie kompilowane , na przykład Python, Ruby i Matlab. Oznacza to, że kod Pythona i Ruby nie kompiluje się z kodem maszynowym, ale jest interpretowany w locie. Możliwe jest skompilowanie Pythona i Ruby na maszynie wirtualnej (patrz następny punkt). Zobacz także to pytanie . Z różnych powodów interpretowany kod jest ogólnie wolniejszy niż skompilowany kod. Sama interpretacja może być nie tylko powolna, ale także trudniejsze do optymalizacji. Jeśli jednak Twój kod spędza większość czasu na funkcjach bibliotecznych (przypadek Matlaba), wydajność nie ucierpi.

Maszyna wirtualna Niektóre języki są kompilowane do kodu bajtowego , wynalezionego „kodu maszynowego”, który jest następnie interpretowany. Typowymi przykładami są Java i C #. Podczas gdy kod bajtowy może być konwertowany w locie na kod maszynowy, kod prawdopodobnie nadal będzie działał wolniej. W przypadku Java do przenoszenia używana jest maszyna wirtualna. W przypadku C # mogą istnieć inne obawy, takie jak bezpieczeństwo.

Koszty ogólne Niektóre języki wymieniają się wydajnością dla bezpieczeństwa. Na przykład niektóre wersje Pascala sprawdzałyby, że nie masz dostępu do tablicy poza granicami. Kod C # jest „zarządzany”, a to kosztuje. Innym częstym przykładem jest wyrzucanie elementów bezużytecznych, które oszczędza czas programistom, ale nie jest tak wydajne, jak praktyczne zarządzanie pamięcią. Istnieją inne źródła narzutów, takie jak infrastruktura do obsługi wyjątków lub do wspierania programowania obiektowego.

* W rzeczywistości dzisiejsze systemy o wysokiej wydajności również uruchamiają kod na GPU, a nawet na FPGA.


Więc w zasadzie, jeśli potrzebuję większej wydajności, powinienem wybrać skompilowane języki? A o paradygmatach? Czy istnieje powód, aby wybrać funkcjonalny zamiast oop, czy odwrotnie?
Rodrigo Valente

Twoja uwaga na temat wyrzucania elementów bezużytecznych jest nieco uproszczona. Nie zawsze jest rozstrzygalne statycznie, gdy przydzielona pamięć nie jest już używana. Nawet jeśli jest to rozstrzygalne, ustalenie bez błędów może być bardzo trudne. Dlatego GC jest czasem konieczne i często bezpieczniejsze (jak sprawdzanie granic tablicy). Ponadto można go połączyć z jawnym wydaniem.
babou

@ RodrigoAraújoValente Zależy. Kiepski kod często kompiluje się do gównianego kodu. Może kod, który możesz napisać w Pythonie, jest w rzeczywistości szybszy niż kod, który możesz napisać w C.
Raphael

nit: jak wyjaśniono w pytaniu, które łączysz, python nie jest interpretowany „w locie” :)
Eevee

Żaden z języków wymienionych w sekcji tłumaczonej nie jest tłumaczony w locie. Python jest kompilowany do kodu bajtowego, Ruby został skompilowany do AST, ale myślę, że teraz jest kompilowany do kodu bajtowego. Matlab, wydaje mi się, że jest teraz skompilowany w JIT. Właściwie nie znam żadnej implementacji w języku niszowym, która interpretuje rzeczy w locie, a nie kompiluje się do jakiegoś rodzaju reprezentacji maszyny wirtualnej.
Winston Ewert

5

Istnieją różne czynniki wyboru X zamiast Y, jak

  • Łatwość nauki
  • Łatwość zrozumienia
  • Szybkość rozwoju
  • Pomoc w egzekwowaniu poprawnego kodu
  • Wydajność skompilowanego kodu
  • Obsługiwane środowiska platformowe
  • Ruchliwość
  • Nadające się do celu

Niektóre języki są odpowiednie do tworzenia projektów biznesowych, takich jak C # lub Python, ale z drugiej strony niektóre z nich są dobre do programowania systemu, takiego jak C ++.

Musisz określić, na jakiej platformie będziesz pracować i jaką aplikację zamierzasz stworzyć.


1
Jak więc określić wydajność skompilowanego kodu? Właśnie to skłoniło mnie do zrobienia tego pytania.
Rodrigo Valente

6
Ta odpowiedź z pewnością ma dobrą radę, ale nie rozumiem, w jaki sposób odpowiada na pytanie, które dotyczy szybkości jako czynnika wyboru dla języka.
babou

@ RodrigoAraújoValente Mogą nawet nie być skompilowanym kodem (jeśli Twój język jest interpretowany).
Raphael

1
Możesz dodać „Dobre biblioteki” i „Dobre narzędzia”.
Raphael

@ RodrigoAraújoValente Uruchom go i profiluj.
Kyle

2

„Najszybszym” językiem programowania, jaki można uzyskać na dowolnej platformie, jest język asemblera chipsetu, z którym mamy do czynienia. Na tym poziomie nie ma tłumaczeń. Jednak trzeba mieć pewną wiedzę o tym, jak chipset wykonuje instrukcje, szczególnie te, które mogą robić rzeczy równolegle.

Konwersja z C do zestawu jest bardzo „płytka”, ponieważ jest bliska jeden do jednego, ale jest bardziej czytelna. Jednak ma wiele warstw nad nim ze względu na standardowe biblioteki w celu poprawy przenośności. Kompilator nie musi robić tak wielu rzeczy, aby dostać się do kodu asemblera, a generalnie istnieją silniejsze optymalizacje, aby wprowadzić zmiany specyficzne dla maszyny.

C ++ dodaje bogatszy język. Jednak ponieważ język dodaje tyle złożoności, kompilatorowi trudniej jest stworzyć optymalny kod dla platformy.

Następnie przechodzimy na drugą stronę skali. Języki interpretowane. Zwykle są one najwolniejsze, ponieważ oprócz wykonania pracy jest trochę czasu na parsowanie kodu i przekształcenie go w wywołania maszynowe.

Następnie mamy te pomiędzy. Zasadniczo mają warstwę maszyny wirtualnej zoptymalizowaną dla platformy. A kompilator utworzy kod do uruchomienia maszyny wirtualnej. Czasami dzieje się to od razu jak Perl, Pascal, Ruby lub Python. Lub w kilku etapach, takich jak Java.

Niektóre z tych maszyn wirtualnych dodają pojęcie kompilatora JIT, który przyspiesza również środowisko wykonawcze poprzez tworzenie kodu na poziomie maszyny zamiast tłumaczenia kodu bajtu pośredniego.

Niektóre maszyny wirtualne mają niski poziom, co pozwala na mniejszą translację kodu bajtowego na kod maszynowy. Które przyspieszają, zachowując przenośność.


Historycznie C został zaprojektowany, aby umożliwić łatwe tłumaczenie na kod maszynowy. Jednak w coraz większym stopniu przekształcenie C w wydajny kod C wymaga od kompilatora zrozumienia, co programista próbuje zrobić, a następnie przetłumaczenia tej intencji na kod maszynowy. Na przykład, historycznie, odpowiednik kodu maszynowego *p++=*q++;na wielu komputerach byłby szybszy niż, array1[i]=array2[i];ale na wielu procesorach odwrotność jest często prawdziwa, a zatem kompilatory mogą w końcu przekonwertować poprzedni styl kodu na drugi - nie jest to „płytka” konwersja.
supercat

Zwykle, jeśli to zrobisz -O0, nie dokona żadnych optymalizacji. Optymalizacje to premia, którą można uzyskać dzięki kompilatorowi, ale sam język może tłumaczyć od jednego do jednego do asemblera.
Archimedes Trajano

2

Nie wspomniałem jeszcze o tym, że w niektórych językach uruchomienie tego samego fragmentu kodu wielokrotnie wykona tę samą sekwencję działań; komputer musi więc tylko raz ustalić, co powinna zrobić sekcja kodu. Jedną z głównych zalet dialektu JavaScript „używaj ściśle” jest to, że gdy silnik JavaScript zorientuje się, co robi fragment kodu, może wykorzystać te informacje przy następnym uruchomieniu; bez „ścisłego użycia” nie może.

Na przykład przy braku „ścisłego użycia” fragment kodu taki jak:

function f() { return x; }

może zwrócić zmienną X w kontekście bezpośredniego wywołania, jeśli istnieje, lub zmienną X z zewnętrznego kontekstu wywołania, lub może zwrócić Undefined. Gorzej, w pętli takiej jak:

for (i=0; i<40; i+=1) { g(i); }

silnik JavaScript nie g()może wiedzieć, co może z tym zrobić i[lub gsamemu sobie. Ponieważ glub imoże całkiem słusznie zmienić isię w ciąg, silnik JavaScript nie może po prostu użyć dodawania liczbowego i porównania numerycznego w pętli, ale musi przy każdym przejściu przez pętlę sprawdzać, czy którekolwiek z wywołań funkcji coś zrobiło ilub g. Natomiast w dialekcie „użyj ścisłego” [nieco rozsądnego] silnik JavaScipt może zbadać powyższy kod i wiedzieć, że każde przejście przez pętlę będzie używać tej samej zmiennej liczbowej i wywoływać tę samą funkcję. Będzie zatem musiał jedynie zidentyfikować ii funkcjonowaćg raz, zamiast konieczności sprawdzania ich przy każdym przejściu przez pętlę - duża oszczędność czasu.


2

Cóż, są tu dość profesjonalne odpowiedzi, ta nie jest im bliska, ale może być dla ciebie intuicyjna.

Być może słyszałeś wiele razy, że gdy chcesz wykonać zadanie tak szybko, jak to możliwe, chciałbyś napisać kod, który wykonuje go w asemblerze. Wynika to z faktu, że wykonujesz tylko te polecenia, które są rzeczywiście potrzebne do wykonania zadania i nic więcej. Chociaż w języku wysokiego poziomu możesz zaimplementować to zadanie w kilku wierszach, kompilator nadal musi je przetłumaczyć na język maszynowy. To tłumaczenie nie zawsze jest minimalistyczne, ponieważ można je napisać bezpośrednio. Oznacza to, że maszyna wyda wiele zegarów na wykonywanie poleceń, które można oszczędzić.

Chociaż kompilatory są dziś bardzo wyrafinowane, nadal nie są skuteczne, jak mogą być najlepsi programiści.

Kontynuując w tym kierunku, te niepotrzebne polecenia rosną w swojej ilości (zwykle), gdy język jest wyższy poziom. (nie dotyczy to w 100% wszystkich języków wysokiego poziomu)

Więc dla mnie język X jest szybszy niż język Y (w środowisku wykonawczym), jeśli dla określonego fragmentu kodu kod maszynowy X jest krótszy niż Y.


Montaż totalnie kołysze. Jednak prawdziwy artysta musi przewyższyć najlepsze kompilatory.
Johan - przywróć Monikę

1

Trudno jest ostatecznie odpowiedzieć na to pytanie, ponieważ jest ono tak złożone i wielowymiarowe (jest prawie jak np. Porównywanie marek samochodów z różnymi kryteriami), ale istnieją nowe badania naukowe, w tym doskonałe repozytorium kodów znane jako kod Rosetta ( przegląd Wikipedii ). W ankiecie Nanz i Furii z 2014 r. Badano to pytanie dość definitywnie i naukowo w oparciu o następujące typowe kryteria i rzadką analizę ilościową typowo subiektywnych cech kodu. Streszczenie zawiera pewne obiektywnie uzasadnione ustalenia i uogólnienia. (Mam nadzieję, że w przyszłości zostaną przeprowadzone inne badania oparte na tych wynikach).

  • RQ1. Które języki programowania zapewniają bardziej zwięzły kod?

  • RQ2. Które języki programowania kompilują się w mniejsze pliki wykonywalne?

  • RQ3. Które języki programowania mają lepszą wydajność w czasie wykonywania?

  • RQ4. Które języki programowania wydajniej wykorzystują pamięć?

  • RQ5. Które języki programowania są mniej podatne na awarie?

Streszczenie - Czasami debaty na temat języków programowania są bardziej religijne niż naukowe. Pytania o to, który język jest bardziej zwięzły lub wydajny, lub który zwiększa produktywność programistów, są dyskutowane z zapałem, a ich odpowiedzi zbyt często opierają się na anegdotach i nieuzasadnionych przekonaniach. W tym badaniu wykorzystujemy w dużej mierze niewykorzystany potencjał badawczy Rosetta Code, repozytorium kodu rozwiązań typowych zadań programistycznych w różnych językach, które oferuje duży zestaw danych do analizy. Nasze badanie oparte jest na 7 087 programach rozwiązań odpowiadających 745 zadaniom w 8 powszechnie używanych językach reprezentujących główne paradygmaty programowania (proceduralne: C i Go; obiektowe: C # i Java; funkcjonalne: F # i Haskell; skrypty: Python i Ruby). Nasza analiza statystyczna ujawnia przede wszystkim, że: języki funkcjonalne i skryptowe są bardziej zwięzłe niż języki proceduralne i obiektowe; C jest trudny do pokonania, jeśli chodzi o szybkość surową przy dużych nakładach, ale różnice w wydajności w porównaniu z wejściami o umiarkowanym rozmiarze są mniej wyraźne i pozwalają konkurować nawet tłumaczonym językom; skompilowane języki silnie typowane, w których można wykryć więcej błędów w czasie kompilacji, są mniej podatne na awarie środowiska wykonawczego niż języki interpretowane lub słabo typowane. Omawiamy konsekwencje tych wyników dla programistów, projektantów języków i nauczycieli. tam, gdzie w czasie kompilacji można wykryć więcej defektów, są one mniej podatne na awarie środowiska wykonawczego niż języki interpretowane lub słabo typowane. Omawiamy konsekwencje tych wyników dla programistów, projektantów języków i nauczycieli. tam, gdzie w czasie kompilacji można wykryć więcej defektów, są one mniej podatne na awarie środowiska wykonawczego niż języki interpretowane lub słabo typowane. Omawiamy konsekwencje tych wyników dla programistów, projektantów języków i nauczycieli.


0

Języki komputerowe to tylko abstrakcja poleceń wyjaśniających komputerowi, co robić.

Możesz nawet pisać w języku komputerowym Pythoni kompilować go za pomocą kompilatora C (cython).

Mając to na uwadze, nie można porównywać szybkości języków komputerowych.

Ale do pewnego stopnia możesz porównywać kompilatory dla tego samego języka. Na przykład GNU Ckompilator kontra Intel Ckompilator. (Wyszukaj test kompilatora)


2
Jeśli chcesz komentować pytanie, użyj pola komentarza, a nie swojej odpowiedzi. Zauważ, że jest to Computer Science Stack Exchange, a informatyka nie programuje, tak jak literatura nie jest edytorem tekstu. Pytania programistyczne na żywo w inżynierii oprogramowania lub przepełnieniu stosu .
David Richerby
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.