Podsumowanie: Znajdowanie i wykorzystywanie równoległości (na poziomie instrukcji) w programie jednowątkowym odbywa się wyłącznie sprzętowo, przez rdzeń procesora, na którym działa. I tylko nad oknem kilkuset instrukcji, a nie na dużą skalę zamawiania.
Programy jednowątkowe nie czerpią korzyści z wielordzeniowych procesorów, z wyjątkiem tego, że inne rzeczy mogą działać na innych rdzeniach zamiast tracić czas na zadanie jednowątkowe.
system operacyjny porządkuje instrukcje wszystkich wątków w taki sposób, aby nie czekały na siebie.
System operacyjny NIE zagląda do strumieni instrukcji wątków. Planuje tylko wątki do rdzeni.
W rzeczywistości każdy rdzeń uruchamia funkcję harmonogramu systemu operacyjnego, gdy musi dowiedzieć się, co dalej. Planowanie jest algorytmem rozproszonym. Aby lepiej zrozumieć maszyny wielordzeniowe, pomyśl o każdym rdzeniu jako o osobnym uruchamianiu jądra. Podobnie jak program wielowątkowy, jądro jest napisane, aby jego kod na jednym rdzeniu mógł bezpiecznie oddziaływać z jego kodem na innych rdzeniach w celu aktualizacji wspólnych struktur danych (takich jak lista wątków, które są gotowe do uruchomienia.
W każdym razie system operacyjny bierze udział w pomaganiu procesom wielowątkowym w wykorzystaniu równoległości na poziomie wątków, które muszą być jawnie ujawnione poprzez ręczne napisanie programu wielowątkowego . (Lub przez kompilator z automatyczną równoległością z OpenMP lub coś takiego).
Następnie interfejs CPU dalej porządkuje te instrukcje, dystrybuując jeden wątek do każdego rdzenia i dystrybuując niezależne instrukcje z każdego wątku wśród dowolnych otwartych cykli.
Rdzeń procesora uruchamia tylko jeden strumień instrukcji, jeśli nie jest zatrzymany (śpi do następnego przerwania, np. Przerwania timera). Często jest to wątek, ale może to być również moduł obsługi przerwań jądra lub inny kod jądra, jeśli jądro postanowiło zrobić coś innego niż powrót do poprzedniego wątku po obsłudze i przerwie lub wywołaniu systemowym.
W przypadku HyperThreading lub innych konstrukcji SMT fizyczny rdzeń procesora działa jak wiele „logicznych” rdzeni. Jedyną różnicą z punktu widzenia systemu operacyjnego między procesorem czterordzeniowym z hyperthreadingiem (4c8t) a zwykłą maszyną 8-rdzeniową (8c8t) jest to, że system operacyjny obsługujący HT spróbuje zaplanować wątki w celu oddzielenia rdzeni fizycznych, aby nie „ konkurować ze sobą. System operacyjny, który nie wiedział o hiperwątkowaniu, zobaczyłby tylko 8 rdzeni (chyba że wyłączysz HT w BIOSie, wykryje tylko 4).
Termin „ front-end” odnosi się do części rdzenia procesora, która pobiera kod maszynowy, dekoduje instrukcje i wydaje je do części rdzenia poza kolejnością . Każdy rdzeń ma własny interfejs i jest częścią rdzenia jako całości. Pobierane przez niego instrukcje są aktualnie uruchomione przez procesor.
W niedziałającej części rdzenia instrukcje (lub uops) są wysyłane do portów wykonawczych, gdy ich operandy wejściowe są gotowe i jest wolny port wykonawczy. Nie musi się to zdarzać w kolejności programów, więc w ten sposób procesor OOO może wykorzystać równoległość na poziomie instrukcji w jednym wątku .
Jeśli zamienisz „rdzeń” na „jednostkę wykonawczą” w swoim pomyśle, jesteś blisko poprawienia. Tak, procesor równolegle dystrybuuje niezależne instrukcje / polecenia do jednostek wykonawczych. (Ale istnieje pewna pomyłka terminologiczna, ponieważ powiedziałeś „front-end”, kiedy tak naprawdę to planista instrukcji CPU, zwany Reservation Station, wybiera instrukcje gotowe do wykonania).
Wykonanie poza kolejnością może znaleźć ILP tylko na poziomie lokalnym, tylko do kilkuset instrukcji, a nie między dwiema niezależnymi pętlami (chyba że są krótkie).
Na przykład równoważnik tego asm
int i=0,j=0;
do {
i++;
j++;
} while(42);
będzie działać tak szybko, jak ta sama pętla, zwiększając tylko jeden licznik na Intel Haswell. i++
zależy tylko od poprzedniej wartości i
, podczas gdy j++
zależy tylko od poprzedniej wartości j
, więc dwa łańcuchy zależności mogą działać równolegle, nie przerywając iluzji wszystkiego, co jest wykonywane w kolejności programu.
Na x86 pętla wyglądałaby mniej więcej tak:
top_of_loop:
inc eax
inc edx
jmp .loop
Haswell ma 4 porty wykonywania liczb całkowitych, a wszystkie z nich mają jednostki sumujące, więc może utrzymać przepustowość do 4 inc
instrukcji na zegar, jeśli wszystkie są niezależne. (Przy opóźnieniu = 1, więc potrzebujesz tylko 4 rejestrów, aby zmaksymalizować przepustowość, utrzymując 4 inc
instrukcje w locie. Porównaj to z wektorowym FP MUL lub FMA: opóźnienie = 5 przepustowość = 0,5 potrzebuje 10 wektorowych akumulatorów, aby utrzymać 10 FMA w locie aby zmaksymalizować przepustowość. Każdy wektor może mieć 256b i pomieścić 8 pływaków o pojedynczej precyzji).
Przejęta gałąź jest również wąskim gardłem: pętla zawsze zajmuje co najmniej jeden cały zegar na iterację, ponieważ przepustowość przejętej gałęzi jest ograniczona do 1 na zegar. Mógłbym umieścić jeszcze jedną instrukcję w pętli bez zmniejszania wydajności, chyba że odczytuje / zapisuje eax
lub edx
w takim przypadku wydłużyłby ten łańcuch zależności. Umieszczenie 2 dodatkowych instrukcji w pętli (lub jednej złożonej instrukcji wielopunktowej) stworzyłoby wąskie gardło w interfejsie, ponieważ może wydać tylko 4 impulsy na zegar do rdzenia poza kolejnością. (Zobacz to SO Q&A, aby uzyskać szczegółowe informacje na temat tego, co dzieje się w przypadku pętli, które nie są wielokrotnością 4 uops: bufor pętli i cache uop sprawiają, że rzeczy są interesujące.)
W bardziej skomplikowanych przypadkach znalezienie równoległości wymaga spojrzenia na większe okno instrukcji . (np. może istnieje sekwencja 10 instrukcji, które wszystkie zależą od siebie, a następnie kilka niezależnych).
Pojemność bufora ponownego zamówienia jest jednym z czynników ograniczających rozmiar okna poza kolejnością. W przypadku Intel Haswell jest to 192 ups. (I możesz nawet zmierzyć to eksperymentalnie , wraz z pojemnością zmiany nazwy rejestru (rozmiar pliku rejestru).) Rdzenie procesora o niskiej mocy, takie jak ARM, mają znacznie mniejsze rozmiary ROB, jeśli w ogóle wykonują się poza kolejnością.
Należy również pamiętać, że procesory muszą być przetwarzane potokowo, a także poza kolejnością. Musi więc pobierać i dekodować instrukcje na długo przed tymi, które są wykonywane, najlepiej o wystarczającej przepustowości, aby uzupełnić bufory po pominięciu jakichkolwiek cykli pobierania. Gałęzie są trudne, ponieważ nie wiemy, skąd je pobrać, jeśli nie wiemy, w którą stronę poszła gałąź. Właśnie dlatego przewidywanie gałęzi jest tak ważne. (I dlaczego współczesne procesory używają spekulatywnego wykonywania: domyślają się, w którą stronę pójdzie gałąź, i zaczną pobierać / dekodować / wykonywać strumień instrukcji. Po wykryciu błędnej prognozy przywracają do ostatniego znanego dobrego stanu i wykonują stamtąd.)
Jeśli chcesz dowiedzieć się więcej o wewnętrznych procesorach, na wiki wiki Stackoverflow x86 znajduje się kilka linków, w tym przewodnik mikroarchitera Agner Fog oraz szczegółowe opisy Davida Kantera ze schematami procesorów Intel i AMD. Z jego podsumowania mikroarchitektury Intel Haswell jest to ostatni schemat całego potoku rdzenia Haswella (nie całego układu).
To jest schemat blokowy pojedynczego rdzenia procesora . Czterordzeniowy procesor ma 4 na chipie, każdy z własną pamięcią podręczną L1 / L2 (współdzielenie pamięci podręcznej L3, kontrolerów pamięci i połączeń PCIe z urządzeniami systemowymi).
Wiem, że jest to niezwykle skomplikowane. Artykuł Kantera pokazuje także części tego, aby na przykład rozmawiać o interfejsie oddzielnie od jednostek wykonawczych lub pamięci podręcznych.