Porównanie wydajności języków backend GO vs Python vs Node.js

11/4/2024 Backend

Mateusz Kędziora

image

Wstęp

Codziennie wielu początkujących, a czasami nawet doświadczonych programistów zadaje sobie pytanie, jakiego języka do napisania backendu użyć? Dzisiaj postaram się odpowiedzieć na to pytanie z punktu widzenia wydajności kilku z popularniejszych języków backendowych.

Przebieg badania

Badanie polegało na przeprowadzeniu szeregu testów – obliczania zadanej precyzji liczby Pi za pomocą prostego algorytmu BBP. Każdy język miał postawiony serwer WWW, który po otrzymaniu zapytania o wymaganej precyzji liczył Pi i zwracał czas wykonania skryptu.

Kod obliczania liczby Pi za pomocą Go przy użyciu BBP formula

func calculatePi(n int) *big.Float {
	precision := uint(n * 14)
	pi := new(big.Float).SetPrec(precision)
	pi.SetInt64(0)

	one := new(big.Float).SetInt64(1)
	two := new(big.Float).SetInt64(2)
	four := new(big.Float).SetInt64(4)
	five := new(big.Float).SetInt64(5)
	six := new(big.Float).SetInt64(6)
	eight := new(big.Float).SetInt64(8)
	sixteen := new(big.Float).SetInt64(16)

	for i := 0; i < n; i++ {
		k := new(big.Float).SetInt64(int64(i))

		// Obliczanie 16^i
		powSixteen := new(big.Float).SetInt64(1)
		for j := 0; j < i; j++ {
			powSixteen.Mul(powSixteen, sixteen)
		}

		// Obliczanie 1/16^i
		denominator := new(big.Float).SetPrec(precision)
		if powSixteen.Sign() != 0 {
			denominator.Quo(one, powSixteen)
		} else {
			continue
		}

		// Obliczanie poszczególnych składników
		term1 := new(big.Float).SetPrec(precision)
		term1.Mul(eight, k)
		term1.Add(term1, one)
		term1.Quo(four, term1)

		term2 := new(big.Float).SetPrec(precision)
		term2.Mul(eight, k)
		term2.Add(term2, four)
		term2.Quo(two, term2)

		term3 := new(big.Float).SetPrec(precision)
		term3.Mul(eight, k)
		term3.Add(term3, five)
		term3.Quo(one, term3)

		term4 := new(big.Float).SetPrec(precision)
		term4.Mul(eight, k)
		term4.Add(term4, six)
		term4.Quo(one, term4)

		// Łączenie składników
		sum := new(big.Float).SetPrec(precision)
		sum.Sub(term1, term2)
		sum.Sub(sum, term3)
		sum.Sub(sum, term4)

		// Mnożenie przez 1/16^i i dodawanie do wyniku
		sum.Mul(sum, denominator)
		pi.Add(pi, sum)
	}

	return pi
}

Kod obliczania liczby Pi za pomocą Pythona przy użyciu BBP formula

from decimal import Decimal, getcontext

def calculate_pi(n):
    getcontext().prec = n * 14
    pi = Decimal(0)
    k = Decimal(0)
    x = Decimal(1)
    sixteen = Decimal(16)

    for i in range(n):
        pi += (Decimal(4) / (8*k + 1) -
                Decimal(2) / (8*k + 4) -
                Decimal(1) / (8*k + 5) -
                Decimal(1) / (8*k + 6)) * x
        k += 1
        x /= 16

    return pi

Kod obliczania liczby Pi za pomocą Node.js(JavaScirpt) przy użyciu BBP formula

const Decimal = require("decimal.js");

function calculatePi(n) {
  Decimal.config({ precision: n * 14 });

  let pi = new Decimal(0);
  let k = 0; // Using a simple integer for k
  let x = new Decimal(1);
  const sixteen = new Decimal(16);

  for (let i = 0; i < n; i++) {
    // Precompute common terms to avoid recalculating them
    const denominator1 = new Decimal(8).times(k).plus(1);
    const denominator2 = new Decimal(8).times(k).plus(4);
    const denominator3 = new Decimal(8).times(k).plus(5);
    const denominator4 = new Decimal(8).times(k).plus(6);

    // Calculate the contributions to pi
    const term1 = Decimal.div(4, denominator1);
    const term2 = Decimal.div(2, denominator2);
    const term3 = Decimal.div(1, denominator3);
    const term4 = Decimal.div(1, denominator4);

    // Update pi with the calculated terms
    pi = pi.plus(term1.minus(term2).minus(term3).minus(term4).times(x));

    // Increment k and update x
    k++;
    x = x.div(sixteen);
  }

  return pi;
}

Do tego został napisany skrypt w JavaScript, który miał za zadanie wykonywać po kolei zapytania do backendów.

Całość kodu użytego przy testach dla wszystkich języków dostępna jest tutaj.

Środowisko testowe

Testy zostały przeprowadzone na komputerze testowym wyposażonym w procesor Intel Core i3-10100 wspierany przez 16Gb pamięci ram. Urządzenie działa pod kontrolą GNU/Linux.

Wersje języków i pakietów użyte przy testach:

  • Golang - go version go1.23.2 linux/amd64
  • Python - Python 3.10.12
  • Node.js - v20.18.0

Opis języków i frameworków

  • Go, stworzony przez inżynierów Google, to język programowania statycznie typowany, kompilowany, znany ze swojej prostoty i wydajności. Jego składnia jest minimalistyczna, co przyspiesza zarówno pisanie, jak i czytanie kodu. Go jest szczególnie dobrze przystosowany do tworzenia aplikacji sieciowych, które muszą obsługiwać duże obciążenia. Dzięki swojej wydajności, niskiemu zużyciu zasobów i możliwościach współbieżności, Go stał się popularnym wyborem dla backendów wymagających szybkiej odpowiedzi i skalowalności.
  • Flask, choć często mylnie utożsamiany z językiem programowania, jest lekkim frameworkiem webowym opartym na języku Python. Jego minimalistyczne podejście i elastyczność sprawiają, że jest doskonałym wyborem dla projektów o różnej skali. Python, znany z czytelnej składni, nadaje Flasku przewagę w zakresie szybkiego prototypowania i rozwoju. Choć nie jest tak szybki jak Go, Flask, dzięki optymalizacji i wykorzystaniu odpowiednich bibliotek, może osiągać zaskakująco dobre wyniki wydajnościowe, szczególnie w przypadku mniejszych aplikacji i API.
  • Node.js to środowisko runtime oparte na języku JavaScript, które zrewolucjonizowało sposób tworzenia aplikacji webowych po stronie serwera. Jego architektura oparta na pętli zdarzeń (event loop) umożliwia wydajne obsługę wielu połączeń jednocześnie, co czyni go idealnym wyborem dla aplikacji wymagających dużej skalowalności i responsywności. Node.js odznacza się lekką wagą i prostotą, co przyspiesza proces rozwoju. Znajduje szerokie zastosowanie w tworzeniu aplikacji real-time, takich jak czaty, gry online czy aplikacje IoT. Dzięki dużej społeczności i bogatemu ekosystemowi pakietów NPM, Node.js oferuje niezliczone możliwości rozszerzenia funkcjonalności aplikacji.

Wyniki testów

Wyniki w formie tabelarycznej

Precyzja: 100

JęzykŚredni czas (ms)Mediana (ms)Min (ms)Max (ms)
Go0.6750.6900.5820.797
Python1.6731.6971.5171.771
Node.js8.6718.7377.7609.605

Precyzja: 500

JęzykŚredni czas (ms)Mediana (ms)Min (ms)Max (ms)
Go12.02112.31511.36116.159
Python58.81358.62357.75260.038
Node.js599.401601.134585.441610.239

Precyzja: 1000

JęzykŚredni czas (ms)Mediana (ms)Min (ms)Max (ms)
Go45.11445.28942.27349.646
Python343.353344.702342.768345.834
Node.js4365.6294354.2014328.2464417.671

Precyzja: 1500

JęzykŚredni czas (ms)Mediana (ms)Min (ms)Max (ms)
Go108.187107.97098.775117.946
Python1063.6541059.1571047.5311066.307
Node.js14280.66914276.41214258.18814310.617

Precyzja: 2000

JęzykŚredni czas (ms)Mediana (ms)Min (ms)Max (ms)
Go186.828176.861176.797210.801
Python2088.1852087.2512085.5752092.221
Node.js33577.74333573.96033474.16033615.152

Precyzja: 2500

JęzykŚredni czas (ms)Mediana (ms)Min (ms)Max (ms)
Go290.621286.428283.795298.840
Python3619.5663610.8953609.4523634.539
Node.js65183.96765148.34965012.79765316.498

Wykres zależności precyzji od czasu wykonania

Wykres przedstawiający zależność precyzji od czasu wykonania

Analiza wyników

W przeprowadzonych testach wydajności, Go okazało się zdecydowanym zwycięzcą, konsekwentnie, osiągając najkrótsze czasy obliczeń dla wszystkich poziomów precyzji. Python, choć również sprawdził się dobrze przy niższych poziomach precyzji, znacząco zwolnił wraz ze wzrostem złożoności obliczeń. Node. Js, z kolei, wypadł najgorzej, demonstrując wyraźne pogorszenie wydajności przy większej liczbie miejsc po przecinku. Te wyniki sugerują, że Go jest optymalnym wyborem dla obliczeniowo intensywnych zadań, takich jak liczenie liczby pi z wysoką precyzją.

Wpływ precyzji na wyniki

Go i Python dość dobrze poradziły sobie z większą liczbą miejsc po przecinku, ich wydajność nie spadała zbyt drastycznie wraz ze zwiększającą się precyzją, natomiast Node.js odnotowywał praktycznie 2-krotny wzrost czasu potrzebnego na obliczenie dodatkowych 500 liczb po przecinku, jest to zaskakujące, ponieważ żaden język nie używał zaawansowanych optymalizacji, czy np. Goroutines. Co jeszcze ciekawsze na etapie pisania kodów źródłowych testowałem kilka możliwych algorytmów do liczenia Pi, wszystkie działały podobnie źle na środowisku Node, a zadowalająco lub dobrze na Go, czy Python.

Ograniczenia badania

Otrzymane wyniki są obiecujące, jednak należy pamiętać o pewnych ograniczeniach tego badania. Przede wszystkim testy przeprowadzono w ściśle określonych warunkach, co może nie odzwierciedlać rzeczywistych zastosowań. Warto rozważyć przeprowadzenie dodatkowych eksperymentów, np. z wykorzystaniem innych algorytmów obliczania liczby π, większych zbiorów danych lub różnych architektur sprzętowych. Ponadto warto zbadać wpływ optymalizacji kodu i wyboru bibliotek na wydajność poszczególnych języków. W przyszłości interesujące byłoby również porównać wydajność Go, Pythona i Node.js w innych typach obliczeń, takich jak przetwarzanie danych czy uczenie maszynowe.

Wnioski płynące z tego badania mają istotne implikacje dla praktycznego zastosowania tych języków. Deweloperzy tworzący aplikacje wymagające dużej wydajności obliczeniowej, takie jak systemy symulacyjne czy aplikacje naukowe, powinni poważnie rozważyć wybór języka Go. Z kolei, Python i Node.js mogą być bardziej odpowiednie dla projektów, w których wydajność nie jest głównym kryterium, a priorytetem jest szybkość rozwoju, czy łatwość integracji z innymi narzędziami.

Podsumowując, przeprowadzone testy potwierdzają wysoką wydajność języka Go w obliczeniach numerycznych. Jednakże, aby uzyskać pełniejszy obraz możliwości tych języków, konieczne są dalsze badania, które uwzględnią różne scenariusze zastosowań i ograniczenia poszczególnych technologii.

Podsumowanie

Reasumując badanie wykazało, że spośród 3 sprawdzanych języków niekwestionowanym zwycięzca pod względem wydajności jest Go, natomiast najgorzej wypadł JavaScript w Node.Js, Python uplasował się w środku stawki, lecz jego pozycja wcale nie odbiega aż tak znacząco od wydajności oferowanej przez Go szczególnie w przypadkach obliczania niskiej precyzji liczby Pi.

Rekomendacje

Moim zdaniem każdy język ma swoje zastosowanie, dla mniejszych, prostszych i niewymagających skomplikowanych obliczeń najlepszy będzie Node.Js, dla bardziej wymagających obliczeniowo, wymagających dużej skalowalności, obliczeń rozproszonych najlepszym wyborem będzie Go, natomiast tam, gdzie potrzebne jest uczenie maszynowe, wchodzą algorytmy Sztucznej inteligencji (AI), optymalnym wyborem będzie Python, ze swoją ogromną bazą narzędzi właśnie do tego celu. Ale i tak Liderem wydajności pozostanie Go.

Polecane artykuły