Przejdź do treści

Procesor (CPU)

Wprowadzenie

Procesor, nazywany również CPU (central processing unit), jest najważniejszym elementem komputera. To właśnie on wykonuje instrukcje maszynowe dostarczone mu przez użytkownika w formie (skompilowanego) programu. Każda instrukcja powoduje konkretne zachowanie, np. wczytanie danych, wykonanie operacji matematycznej czy wydanie polecenia innemu komponentowi komputera. Pojedyncza instrukcja zajmuje od jednego do kilku bajtów. Procesor jest osadzony na płycie głównej poprzez tzw. gniazdo procesora (socket).

cpu

procesor osadzony na płycie głównej [źródło]

Wydajność procesora decyduje o możliwej do uzyskania maksymalnej mocy obliczeniowej danego komputera. Kluczowe aspekty, które mają na nią wpływ to szybkość oraz równoległość. Przez ich pryzmat warto patrzeć na różne cechy budowy procesora.

cpu

Podstawowe parametry jakimi opisywane są procesory to:

  • liczba rdzeni,
  • częstotliwość taktowania,
  • rozmiar cache (pamięci podręcznej).

Są to istotne atrybuty pozwalające ocenić możliwości danego układu, przy czym poza samymi wartościami należy posiadać większe zrozumienie, na co się przekładają. Jednocześnie nie oddają one wszystkich czynników – szczególnie tych dotyczących równoległości obliczeń – mających wpływ na wydajność procesora.

cpu

przykładowa budowa układu procesora (Intel i7-3960X) [źródło]

Procesor jako fizyczny układ

Dla precyzji opisu, warto dodać, że kiedy mówimy o procesorze w kontekście fizycznego układu, taki układ w środku zawiera różne komponenty, np. kontroler pamięci. Co więcej, każda architektura procesora (a dokładniej mikroarchitektura) może różnić się od innych i definiować swoje komponenty. Architekturą (mikroarchitekturą) nazywamy schemat fizycznej realizacji układu, według którego są realizowane konkretne modele procesorów.

Przykładowo, wspomniany kontroler pamięci dawniej znajdował się poza układem CPU (w tzw. mostku północnym) i dopiero później został z nim zintegrowany. Innym przykładem są zintegrowane układy graficzne – jest to połączenie prostego układu GPU wraz ze standardowym CPU w obrębie jednego układu.

W dalszej części tego tekstu przez "procesor" odnosimy się do tych elementów fizycznego układu, które stricte odpowiadają za obliczenia.

Wielordzeniowość

Historycznie, procesor umożliwiał w danym momencie wykonywanie tylko jednego strumienia instrukcji. Jednakże współcześnie niemal każdy procesor to procesor wielordzeniowy (multi-core). Pomimo że fizycznie jest on jednym układem, wewnątrz składa się z wielu rdzeni, a każdy rdzeń (core) to właściwie osobny CPU (w dawnym rozumieniu). W konsekwencji, procesor (rozumiany jako całość) jest w stanie w jednym momencie obsługiwać kilka strumieni instrukcji – po jednym na każdy rdzeń.

cpu

wizualizacja wielordzeniowości – każdy rdzeń obsługuje swój strumień danych [źródło]

Określenie CPU a rdzeń procesora

Wielordzeniowość może prowadzić do nieścisłości na poziomie nazewnictwa – czasem pojęcie "procesor" (lub "CPU") może odnosić się do całego układu, a czasem tylko do pojedynczego rdzenia. W poniższym tekście będziemy używać "CPU" jako określenie na "rdzeń", a nie wielordzeniowy procesor. Z takiej nomenklatury korzysta system operacyjny oraz narzędzia prezentujące informacje o procesorze (np. htop, lscpu). Mówiąc ściślej, używają one pojęcia "CPU" jako określenie na tzw. procesor logiczny (zobacz hyperthreading oraz SMT).

Liczba rdzeni jest jednym z podstawowych parametrów (ale nie jedynym ⚠️), który wpływa na dostępną w procesorze równoległość obliczeń. Współcześnie komputery posiadają zazwyczaj procesory 4-6 rdzeniowe. Im mocniejszy komputer, tym liczba rdzeni jest większa. W serwerach obliczeniowych dostępnych w superkomputerach standardem jest kilkanaście do kilkudziesięciu rdzeni CPU. Trzeba jednak mieć na uwadze, że standardowy program sam z siebie wykorzysta tylko jeden rdzeń. Jest tak dlatego, że zwykły program to pojedynczy ciąg instrukcji, czyli tzw. jeden wątek.

Wielowątkowość

Aby program mógł skorzystać z wielu rdzeni, programista musi podzielić jego pracę na wiele wątków, czyli użyć wielowątkowości.

Różnorodność rdzeni

Standardowo w danym procesorze rdzenie są identyczne i każdy oferuje takie same możliwości. Jednakże niedawno firma Intel w swoich procesorach (12. generacja Intel Core) wprowadziła hybrydową architekturę. Polega ona na tym, że w obrębie jednego układu umieszczone są dwa rodzaje rdzeni:

  • rdzenie typu "efficiency" (E-cores), nastawione na efektywność,
  • rdzenie typu "performance" (P-cores), nastawione na wydajność.

Rdzenie "E" są wolniejsze, ale nieco prostsze w budowie, ich zadaniem jest lepsza efektywność energetyczna. Rdzenie "P" to standardowe rdzenie, które są szybsze i bardziej złożone. Główną zaletą dodania prostszych rdzeni "E" jest zwiększenie ilości rdzeni; rdzenie "E" mogą odciążać procesor z wielu zadań lekkich obliczeniowo, pozostawiając rdzeniom "P" zadania wymagające większej mocy obliczeniowej.

Więcej informacji, artykuł What Are E-Cores and P-Cores?. Przykład procesora z rdzeniami dwóch rodzajów Intel Core i7-13700.

Taktowanie

Prędkość działania CPU reguluje zegar, który wysyła regularne impulsy. Okres między kolejnymi impulsami to tzw. cykl. Stąd szybkość procesora określa się jako częstotliwość taktowania (w skrócie taktowanie). Jest ona mierzona w GHz, czyli miliardach cykli na sekundę. Z perspektywy funkcjonowania procesora pojedynczy cykl to najmniejsza jednostka pracy – odpowiada wykonaniu jednej elementarnej operacji. Stąd, dla poszczególnych instrukcji czas wykonania mierzy się liczbą cykli potrzebnych do ich ukończenia. Różne instrukcje mogą wymagać różnej ilości cykli.

Dynamiczna zmiana częstotliwości pracy procesora

Dany procesor może działać z różnymi częstotliwościami taktowania. Wyróżnia się częstotliwość bazową oraz maksymalną (turbo). Na przykładzie procesora Intel Core i7-11700 wynoszą one odpowiednio 2.50 GHz oraz 4.90 GHz, a więc różnica może być znaczna. Szybkość procesora zmienia się automatycznie pod wpływem obciążenia – standardowo pracuje on na częstotliwości bazowej (lub niższej), a w momencie intensywnych obliczeń stopniowo przyspiesza (jest to tzw. dynamiczne skalowanie częstotliwości). Takie podejście pozwala na oszczędzanie energii, gdyż wyższa częstotliwość oznacza wyższe zużycie prądu.

Taktowanie, obok dostępnej w procesorze równoległości, jest głównym parametrem decydującym o wydajności procesora. Dzisiejsze typowe procesory mają 2-4 GHz. Najszybsze układy potrafią osiągać nawet 5-6 GHz. Jednakże z perspektywy programistycznej taktowanie jest czymś na co nie mamy wpływu. Zachodzi prosta zależność – im większa częstotliwość pracy CPU tym szybciej wykona on zadany ciąg instrukcji.

Ilość instrukcji jako miara czasu

Dla czasu działania programu znaczenie ma ilość i rodzaj wykonywanych instrukcji. Determinują one łączną liczbę cykli potrzebną do ukończenia programu.

Podkręcanie procesora

W różnych miejscach można spotkać się z pojęciem tzw. podkręcania procesora (overclocking). Odnosi się ono właśnie do zagadnienia taktowania. Podkręcanie to podnoszenie wydajności CPU poprzez zwiększania częstotliwości taktowania ponad fabryczne ustawienia. Taka praktyka niesie ze sobą ryzyko, ponieważ zwiększone napięcie i temperatury mogą skrócić żywotność komponentów. Co więcej, podkręcanie wymaga zmiany systemu chłodzenia na znacznie wydajniejszy, aby procesor nie uległ przegrzaniu. Podkręcanie można stosować raczej tylko na własnym sprzęcie i na własną odpowiedzialność.

Architektura rdzenia

W skład pojedynczego rdzenia CPU wchodzą m.in.: układ sterowania, rejestry, jednostki wykonawcze. Oprócz tego każdy rdzeń ma do dyspozycji pamięć podręczną, czyli tzw. cache️. Architektura rdzenia w istotny sposób przekłada się na równoległość w ramach pojedynczego wątku programu. Aby z niej skorzystać, wystarczy użycie odpowiednich instrukcji, a do pewnego stopnia jest ona stosowana przez procesor automatycznie.

Rejestry

Rejestry służą do przechowywania danych, na których CPU pracuje w danej chwili. Procesor może do wybranego rejestru wpisać dowolną liczbę albo pobrać do niego wartość z pamięci RAM. Na wartościach w rejestrach procesor wykonuje zlecone mu operacje (np. modyfikacja wartości konkretnego rejestru, dodanie do siebie dwóch rejestrów, porównanie wartości).

W dzisiejszych procesorach podstawowe rejestry, tzw. rejestry ogólnego przeznaczenia, mają rozmiar 64 bitów (8 bajtów) i służą do przechowywania liczb całkowitych. Stąd mowa o 64-bitowych procesorach (dokładniej jest to architektura x86-64). Program ma do dyspozycji kilkanaście takich rejestrów. To właśnie do nich trafiają zmienne typu char, short, int czy long. Dostępne są również rejestry na liczby zmiennoprzecinkowe (float, double).

Oprócz nich CPU posiada specjalne rejestry wektorowe o zwiększonym rozmiarze. Współcześnie zwykle mają one 256 bitów (standard AVX) albo 512 bitów (rozszerzenie AVX-512). Pozwalają na użycie instrukcji wektorowych (SIMD), czyli równoległe wykonanie tej samej operacji na kilku zmiennych (np. jednoczesne przetwarzanie ośmiu wartości int). Można z nich korzystać zarówno dla liczb całkowitych jak i zmiennoprzecinkowych.

Wektoryzacja

Wektoryzacja, czyli korzystanie z instrukcji wektorowych, jest kluczowym elementem akceleracji obliczeń w obrębie pojedynczego rdzenia.

Jednostki wykonawcze

Jednostki wykonawcze (execution units) to serce CPU. Są to elementy rdzenia, które odpowiadają za faktyczne wykonanie zleconych operacji. W pewnym uproszczeniu, cała praca procesora polega na ciągłym przygotowywaniu i dostarczaniu danych oraz instrukcji do odpowiednich jednostek wykonawczych, aby tam instrukcje były wykonywane.

Pojedynczy rdzeń posiada wiele jednostek wykonawczych różnego typu, dzięki czemu w jednej chwili może być wykonywanych kilka instrukcji maszynowych! Taka cecha procesora jest określana jako superskalarność. Zróżnicowanie jednostek wykonawczych polega na tym, że każdy typ potrafi wykonywać pewien rodzaj instrukcji maszynowych. CPU może posiadać kilka jednostek tego samego typu. W pewnym przybliżeniu można wyróżnić następujące rodzaje:

  • ALU (arithmetic logic unit) — odpowiedzialny za wykonywanie na rejestrach ogólnego przeznaczenia (czyli na liczbach całkowitych) standardowych operacji arytmetycznych (dodawanie, odejmowanie, mnożenie) oraz operacji bitowych (AND, OR, XOR);
  • FPU (floating-point unit) — odpowiedzialny za wykonywanie operacji dotyczących rejestrów zmiennoprzecinkowych;
  • AGU (address generation unit) — jednostka pomocnicza do obliczania adresów pamięci, pod które procesor ma się odwołać (np. w celu pobrania lub zapisu danych); taka jednostka operuje na ogólnych rejestrach i wykonuje podstawową arytmetykę typu <adres-początku-tablicy> + <indeks> * <rozmiar-pojedynczego-elementu>;
  • jednostki wektorowe — odpowiedzialne za wykonywanie operacji dotyczących rejestrów wektorowych,
  • LSU (load-store unit) — jednostki zajmujące się pobieraniem i zapisywaniem danych do i z pamięci.

Nie ma ścisłego katalogu typów jednostek wykonawczych, gdyż każda architektura procesora na swój sposób definiuje ich rodzaje. Można np. spotkać się z tym, że ALU potrafi wykonywać  tylko dodawanie i odejmowanie, a inna jednostka jest przewidziana do mnożenia, a jeszcze inna do dzielenia liczb całkowitych. Podobnie w przypadku instrukcji zmiennoprzecinkowych czy wektorowych. Najdokładniejszą charakteryzacją jest więc określenie, które instrukcje maszynowe potrafi obsługiwać dana jednostka. Jako przykład patrz na tabelę "Execution Units" w architektura Intel Skylake.

cpu

fragment architektury Intel Nehalem (2008) – jednostki wykonawcze [źródło]

cpu

fragment architektury Intel Skylake (2015) – jednostki wykonawcze [źródło]

cpu

fragment architektury AMD Zen 3 (2020) – jednostki wykonawcze [źródło]

Powyższe grafiki pokazują jeszcze jedną ważną cechę CPU – jednostki wykonawcze mogą być ze sobą zblokowane. Każda grupa (do której prowadzi inny port) reprezentuje osobny "kanał" przetwarzania. W jednym kanale tylko jedna operacja może być wykonywana w danym momencie. Oznacza to, że maksymalna równoległość jednego rdzenia to nie tyle ilość jednostek wykonawczych, co ilość kanałów przetwarzania.

Podsumowując, superskalarność to kolejny aspekt budowy procesora, dający potencjał na równoległość obliczeń. Bezpośrednia obsługa tej równoległości nie zależy od programisty. Pojedynczy rdzeń w sposób automatyczny – poprzez różne techniki optymalizacyjne – wykonuje równolegle do siebie kolejne instrukcje programu, jeśli tylko nie ma między nimi zależności. Jednakże znajomość mechanizmu sprawia, że można próbować pisać program w sposób, który będzie sprzyjał automatycznemu zrównoleglaniu przez procesor.

Superskalarność

Odpowiedni przeplot operacji różnego typu w kodzie źródłowym może przełożyć się na program, który w lepszy sposób będzie korzystał z superskalarności.

Cache

Cache to pamięć podręczna procesora pośrednicząca w dostępie do pamięci głównej (RAM). Fizycznie znajduje się w procesorze, dzięki czemu jest ona bardzo szybka, jednak jednocześnie ma niewielki rozmiar (liczony w megabajtach). Celem pamięci podręcznej jest zwiększenie wydajności dostępu do pamięci RAM poprzez zmniejszenie czasu oczekiwania (latency) do pojedynczej komórki RAM. Przekłada się to na znaczne zwiększenie wydajności procesora, gdyż im mniej procesor czeka na pobranie lub zapis danych, tym więcej może wykonać obliczeń.

Podstawą działania cache’u jest:

  • przechowywanie kopii ostatnio używanych danych (na tyle na ile pozwala pojemność),
  • pobieranie z wyprzedzeniem z RAM-u danych, które znajdują się w bliskim sąsiedztwie komórki do której dostępu zażądał CPU.

Te mechanizmy są skuteczne, gdyż odpowiadają naturalnym dla większości programów schematom: wielokrotnemu dostępowi do tych samych danych oraz sekwencyjnemu przetwarzaniu wartości znajdujących się w kolejnych komórkach tablicy (czyli w spójnym bloku pamięci). Ich efektem jest to, że nie ma potrzeby każdorazowo sięgać do pamięci RAM.

Cache ma strukturę hierarchiczną – wyróżniamy trzy poziomy pamięci podręcznej, oznaczane jako L1, L2, L3. Różnią się one pojemnością, szybkością i lokalizacją. Cache L1 znajduje się najbliżej jednostek wykonawczych CPU, jest najszybszy, ale ma najmniejszą pojemność. Dalej znajduje się L2, a potem L3 – ich szybkość jest odpowiednio mniejsza, za to pojemność jest większa.

Cache działa w sposób automatyczny, tj. z perspektywy instrukcji maszynowych program odwołuje się tylko do pamięci RAM. Gdy CPU sięga pod konkretny adres w pamięci, w pierwszej kolejności następuje sprawdzenie, czy potrzebne dane znajdują się w L1. Jeżeli ich tam nie ma, wtedy sprawdzane są kolejne poziomy. Jeżeli danych nie ma w pamięci podręcznej, dopiero wtedy nastąpi ich pobranie z RAM-u. W efekcie odwołania do RAM, "w drodze powrotnej" dane zostają zapisane w L3, L2 oraz L1.

Schemat dostępu do pamięci

Schemat dostępu do pamięci w programie ma kluczowe znaczenie dla efektywności wykorzystania mechanizmu pamięci podręcznej.


Ostatnia aktualizacja: 7 grudnia 2023