Oprogramowanie systemów mikroprocesorowych a oszczędność energii

| Technika

Przy projektowaniu systemu mikroprocesorowego jedną z ważniejszych rzeczy, które należy wziąć pod uwagę, jest jego spodziewane zapotrzebowanie na energię podczas normalnego użytkowania. Jest to szczególnie ważne w przypadku systemów przenośnych, zasilanych z baterii: wysokie zużycie energii staje się dla użytkownika od razu widoczne, skutkując koniecznością częstego ładowania baterii, a co za tym idzie, zmniejszeniem czasu, podczas którego urządzenie można efektywnie wykorzystywać.

Oprogramowanie systemów mikroprocesorowych a oszczędność energii

Jednak ten aspekt jest nie do pominięcia również w przypadku innych urządzeń zawierających mikrokontrolery - takich jak sprzęt medyczny, urządzenia testujące i pomiarowe, routery z nadajnikiem Wi-Fi - a to ze względu na wymogi ograniczenia ilości ciepła wydzielanego przez coraz wydajniejsze mikroprocesory oraz na konieczność zmniejszenia ogólnych kosztów dostarczania i zużycia prądu. W efekcie zagadnienia oszczędności energii nie można zlekceważyć prawie nigdy.

Odpowiedzialność za przestrzeganie nałożonych ograniczeń tego typu spoczywa zwykle na barkach projektantów sprzętu, ale projektanci oprogramowania również mają tu co nieco do powiedzenia. Ich wpływ na tę dziedzinę jest często niedoceniany lub zupełnie lekceważony. Tymczasem, jak to zostanie pokazane poniżej, sposób, w jaki oprogramowanie napisano, też może mieć duże znaczenie dla poboru mocy przez gotowy już produkt.

Podstawy

Ważne są tu cztery główne zagadnienia: przeznaczenie konkretnego urządzenia, częstotliwość pracy, poziom napięcia, miniaturyzacja układów.

Przeznaczenie urządzenia jest o tyle ważnym czynnikiem, że zastosowania dwóch urządzeń przenośnych mogą być na tyle odmienne, żeby wymagać dwóch krańcowo różnych strategii mających na celu oszczędzanie energii. Rozważmy przykład przenośnego odtwarzacza multimediów w porównaniu z telefonem komórkowym. Odtwarzacz musi być w stanie działać z maksymalną wydajnością przez długie okresy, podczas których użytkownik ogląda film, słucha muzyki itd. Ogólnie rzecz biorąc, w tego rodzaju urządzeniu trzeba się bardziej skoncentrować na optymalizacji algorytmów przetwarzania i przesyłania danych, niż na skutecznym wykorzystywaniu trybów pracy oszczędzających energię przez wprowadzenie urządzenia w stan niskiej aktywności.

Porównajmy to z telefonem komórkowym: to urządzenie spędza większość czasu w stanie niskiej aktywności, a nawet podczas połączenia użytkownik mówi przez ograniczony okres. Przez ten ograniczony czas procesor może znosić duże obciążenia, wykonując kodowanie i dekodowanie głosu, nadzorując transmisję danych itd.

Pozostałą część czasu połączenia zajmują różne procedury pomocnicze, w rodzaju przesyłania pakietów potwierdzeń dla sieci komórkowej, generowania sztucznego szumu (comfort noise), który sygnalizuje użytkownikowi, że połączenie nie zostało zerwane, nawet jeśli w danej chwili niczego się nie mówi. Przy tego rodzaju urządzeniu trzeba się skupić na trybach pracy oszczędzających jak największe ilości energii na czas jej maksymalnego wydatkowania, a dopiero w drugiej kolejności - na algorytmach przetwarzania danych.

Co do miniaturyzacji układów, obecnie stosuje się technologię 45 nm, a w niedalekiej przyszłości będzie to 28 nm. Mniejsze tranzystory ogólnie pozwalają także na stosowanie wyższych częstotliwości pracy, a to oznacza korzyści w postaci większej mocy przetwarzania danych; ale, z drugiej strony, większe częstotliwości pracy w połączeniu z wyższym napięciem powodują większy pobór prądu. W zależności od urządzenia, układ zegarowy odpowiada za zużycie od 50 do 90% mocy dynamicznej dostarczanej do urządzenia, a zatem jest bardzo ważne, aby mieć oko również na to zagadnienie.

Rodzaje poboru mocy

Całkowity pobór mocy ma dwie składowe: dynamiczną i statyczną. Oprogramowanie może kontrolować ten pierwszy czynnik, nie ma natomiast żadnego wpływu na ten drugi.

Statyczny pobór mocy urządzenie wykazuje zawsze, niezależnie od tego, co robi: nawet w stanie uśpienia dochodzi do pewnych strat, gdyż zawsze pewien prąd przepływa ze źródła zasilania urządzenia do masy. Czynnikami, które mają tu wpływ, jest napięcie zasilania, temperatura otoczenia i technologia wykonania półprzewodnika. To zagadnienie nie będzie nas dłużej zajmować, gdyż, jak to zostało powiedziane wyżej, oprogramowanie nie ma na ten rodzaj rozpraszania energii żadnego wpływu.

Dynamiczny pobór mocy zachodzi wtedy, kiedy urządzenie pobiera ją na potrzeby aktywnego użycia mikroprocesora, podsystemów, urządzeń peryferyjnych (takich jak DMA, urządzenia wejścia i wyjścia, Ethernet, aparat fotograficzny, kamera), pamięci, układów zegarowych. Uogólniając, można powiedzieć, że z tym rodzajem poboru mocy mamy do czynienia, kiedy tranzystory dokonują przełączeń.

Tego rodzaju pobór mocy zwiększa się wraz ze wzrostem liczby użytych w urządzeniu komponentów: rdzeni, jednostek arytmetycznych, pamięci, oraz wraz ze wzrostem częstotliwości pracy. Ogólnie: zwiększa je wszystko, co zwiększa liczbę przełączających się tranzystorów albo co zwiększa częstość ich przełączania. Całe zjawisko nie zależy od temperatury, ale nadal zależy od napięcia zasilania.

Maksymalny, przeciętny, najgorszy i typowy

Mierząc pobór mocy przez urządzenie, trzeba wziąć pod uwagę cztery główne poziomy tego poboru: maksymalny, przeciętny, najgorszy i typowy.

Dwa pierwsze to terminy ogólne. Mówiąc prosto, maksymalny pobór to największy chwilowy odczyt poboru mocy mierzonego w założonej jednostce czasu. Średni - to pobór mierzony w założonej jednostce czasu, podzielony przez wielkość tej jednostki. Na tym należy się skupić przy optymalizacji poboru mocy przez urządzenie, gdyż to od tej wielkości zależy, ile energii musi dostarczać bateria lub zasilacz, żeby zapewnić urządzeniu możliwość spełniania funkcji w dłuższej perspektywie czasowej; ma to też znaczenie przy ocenianiu stopnia emisji ciepła przez urządzenie.

Najgorszy i typowy przypadek poboru mocy bazuje na wskaźniku przeciętnym. Przypadek najgorszy to przeciętny pobór mocy przez urządzenie pracujące na 100% wszystkich swoich możliwości w zadanej jednostce czasu. Jest to wielkość kluczowa dla prawidłowej oceny mocy, jaką musi dostarczyć źródło zasilania, żeby zapewnić funkcjonowanie urządzenia pod maksymalnym możliwym obciążeniem.

W rzeczywiście funkcjonującym systemie urządzenie będzie rzadko (jeśli w ogóle) miało okazję osiągnąć najgorszy przypadek poboru mocy, bo prawie nie ma (albo nie ma wcale) takich okresów, w których oprogramowanie korzystałoby z wszystkich naraz podsystemów urządzenia. A zatem typowy pobór mocy można ocenić na podstawie zachowania urządzenia działającego pod kontrolą przeciętnej aplikacji, która wykorzystuje naraz od 50 do 70% komponentów całości. Na taką cechę oprogramowania, że przeważnie pojedynczy program potrzebuje tylko jakiejś części urządzenia, na którym działa - jeszcze zwrócimy baczniejszą uwagę.

Pomiar poboru mocy

Metoda pomiaru poboru mocy jest zależna od konkretnego urządzenia. Niektóre mikrokontrolery mają wbudowane pewne systemy umożliwiające monitorowanie poboru mocy, bywa też, że producenci procesorów dostarczają "kalkulatory mocy", dzięki którym można to i owo obliczyć teoretycznie. Istnieją też, z drugiej strony, układy scalone sterujące zasilaczami, dysponujące pewnymi możliwościami pomiarowymi, a niektóre z nich mają nawet wbudowane czujniki, z których można dokonać odczytu, podłączając się z zewnątrz przez odpowiednie porty wejścia/wyjścia. W końcu, istnieje staroświecka metoda szeregowego podłączenia amperomierza do wejścia zasilania urządzenia.

Pomiar prądu

Pojedynczy procesor może wymagać kilku takich zestawów pomiarowych, gdyż typowo ma oddzielne wejścia zasilania dla rdzeni, dla urządzeń peryferyjnych oraz dla pamięci, a to ze względu na różne wymagania napięciowe tych wszystkich składników. Przydaje się to przy analizie poboru mocy przez każdy komponent systemu z osobna.

Niedogodnością jest oczywiście konieczność odpowiedniego wyizolowania wszystkich elementów systemu, co zapewne będzie wymagało zmian na płytce, przelutowania jakichś mostków itd. Idealnie byłoby, gdyby dało się podłączyć zestaw pomiarowy (zasilacz i amperomierz) jak najbliżej wejść zasilania CPU a wskazania miernika zapisywać w pamięci komputera.

Moc statyczna a moc dynamiczna

Oprócz mierzenia całkowitego poboru mocy, istnieją metody pomiary jego składowej dynamicznej w stosunku do statycznej. Pomiar tej ostatniej stanowi podstawę do oszacowania, czego się można po urządzeniu spodziewać, kiedy będzie się znajdować w trybie uśpienia, oraz o ile więcej, w stosunku do tych potrzeb, będzie potrzebowało prądu na realizację swoich zadań pod kontrolą aplikacji. Można wtedy odjąć składową statyczną od całkowitej, zmierzonej przez nas, wartości poboru mocy, żeby uzyskać jego składową dynamiczną, i skupić się nad jej zmniejszeniem.

Statyczny pobór mocy można zwykle zmierzyć wprowadzając urządzenie w stan uśpienia. Uda się to przy założeniu, że po wejściu w ten tryb zatrzymywane są wszystkie zegary. Jeśli tak nie jest należy zbocznikować obwody PLL oraz odciąć wejściowy sygnał zegarowy. Dodatkowym czynnikiem, jak to już powiedziano wyżej, jest temperatura: dlatego pomiary należy przeprowadzić w różnych warunkach cieplnych.

Dynamiczny pobór mocy, jak to zasygnalizowano wyżej, obliczamy odejmując składową statyczną (odpowiednią dla danej temperatury) od całkowitego poboru mocy generowanego przez urządzenie. Przy optymalizacji oprogramowania pod tym kątem, należy notować wyniki wszystkich testów przed i po zastosowaniem danej techniki optymalizacji, żeby ocenić jej skuteczność.

Profilowanie kodu

Programista, zanim przystąpi do optymalizacji kodu pod kątem zmniejszenia poboru mocy w urządzeniu, powinien mieć podstawowe pojęcie na temat wpływu danego, niezoptymalizowanego jeszcze kodu na ten parametr. To zapewnia podstawę do mierzenia skuteczności wszelkich interwencji w program i daje pewność, że te interwencje prowadzą rzeczywiście do zmniejszenia poboru mocy, a nie odwrotnie. W celu uzyskania tych informacji programista powinien stworzyć program testujący.

Narzędziem, które należy do tego wykorzystać, jest profiler. Większość zintegrowanych środowisk programistycznych (IDE) takowy oferuje. Takie środowisko daje możliwość stworzenia próbnego kodu, skompilowania go i uruchomienia. Po zakończeniu jego działania można od modułu profilującego uzyskać różne dane statystyczne, takie jak: ilu użyto jednostek arytmetyczno-logicznych (ALU), ilu jednostek obliczania adresów (AGU), gdzie są krytyczne punkty programu, z których obszarów pamięci korzystano itd.

Na podstawie tych danych można zyskać ogólne pojęcie, w którym miejscu programu CPU spędzi najwięcej czasu (i tym samym, zużyje najwięcej energii). Następnie przekształcając ten kod w program-test działający w nieskończonej pętli możemy dostać informację, ile "typowo" mocy pobiera całe urządzenie podczas wykonywania takiego ważnego segmentu kodu.

Jest to prosty i efektywny sposób zmierzenia poboru mocy przez CPU przy różnych wariantach obciążenia. Ważne jest, żeby dość dobrze odtworzyć zachowanie konkretnego segmentu docelowej aplikacji: to pozwoli na ocenę np. efektywności wykorzystania kolejek rozkazów, gdyż dobry profiler powinien być w stanie ocenić rzeczywistą liczbę cykli maszynowych (przy uwzględnieniu kolejkowania) zużytych na wykonanie konkretnych sekcji kodu itp. Danych z narzędzia profilującego można użyć do oceny przeciętnego zużycia energii w jednostce czasu, przeciętnego zużycia energii na instrukcję, przeciętnego zużycia energii na cykl i tak dalej.

Optymalizacja przetwarzania danych

Optymalizacja przetwarzania danych wymaga skupienia się na zminimalizowaniu użycia pamięci, magistral i urządzeń peryferyjnych przy wykonywaniu konkretnych zadań. Optymalizacja algorytmu pozwala zmniejszyć ilość niezbędnych obliczeń, co pociąga za sobą spadek wykorzystania wrażliwych zasobów. Natomiast optymalizacja "sprzętowa" polega bardziej na lepszym wykorzystaniu sterowania częstotliwością zegara oraz różnymi trybami pracy mikroprocesora i ogólnie całego urządzenia.

Tryby oszczędzania energii

Programy na ogół przetwarzają dane w porcjach o określonej wielkości. Powiedzmy, w odtwarzaczu wideo, dane wizyjne do zdekodowania mogą napływać w tempie odpowiadającym częstotliwości odświeżania obrazu (np. 50 klatek na sekundę). Ale samo dekodowanie klatki może procesorowi zabierać czas o kilka rzędów wielkości mniejszy niż 1/50 sekundy, co daje szansę na chwilowe przełączenie CPU w tryb low-power, wyłączenie niektórych urządzeń peryferyjnych itd., a to z kolei redukuje zapotrzebowanie całości na energię.

W przypadku odtwarzacza multimediów i telefonu komórkowego, których przykład padł już powyżej, tryby pracy samego urządzenia będą podobne, ale programista musi zastosować różne strategie ich użycia ze względu na różne przeznaczenie obydwu urządzeń. Dość wspomnieć, że telefon komórkowy na ogół nie wykorzystuje stu procent swojej mocy nawet podczas realizacji połączenia, gdyż w czasie każdej rozmowy następują pauzy, które są bardzo długie, jeśli pomyśleć o nich w kategoriach cykli pracy mikroprocesora.

Podstawowe metody oszczędzania energii polegają na odcinaniu zasilania, zatrzymywaniu zegara, regulacji napięcia, regulacji częstotliwości zegara.

Odcinanie zasilania

Jak się można domyślić, polega to na odłączeniu prądu od tej części systemu, która w danej chwili nie jest w użyciu. Eliminuje to jej zapotrzebowanie zarówno na moc dynamiczną, jak i statyczną, ale okupione jest utratą wewnętrznych informacji znajdujących się w wyłączanym układzie i w ogóle całego jego stanu.

Oznacza to na ogół, że przed wyłączeniem trzeba gdzieś ten stan zapamiętać, żeby go przywrócić, gdy układ zostanie ponownie włączony. Współczesne układy mikroprocesorowe są na ogół wielofunkcyjne, a zatem prawie zawsze istnieje jakaś, w danej chwili "niepotrzebna" część systemu, której można się w ten sposób bezpiecznie pozbyć. Zyski z tego zależą oczywiście od konkretnego przypadku.

Trzeba zauważyć, że dokumentacja czasami mówi o "wyłączeniu" określonego komponentu systemu, mimo że w rzeczywistości nie chodzi o odłączenie zasilania, a jedynie o zatrzymanie zegara. Jeśli są co do tego wątpliwości, trzeba tę rzecz sprawdzić.

Zatrzymanie zegara

To z kolei polega na odcięciu sygnału zegarowego od "niepotrzebnej" części systemu. Jako że zapotrzebowanie na moc dynamiczną zależy od zmiany stanów tranzystora, a te następują pod wpływem impulsów zegarowych, zatrzymanie zegara redukuje całkowity pobór mocy danej części systemu do jego składowej statycznej.

Sygnał zegarowy dla współczesnych systemów mikroprocesorowych jest na ogół rozdzielany z głównego źródła na oddzielne "ścieżki" zasilające poszczególne komponenty, tj. rdzenie, pamięci urządzenia peryferyjne, a system przewiduje możliwość selektywnego blokowania tego sygnału, dzięki czemu rysują się możliwości całkiem dokładnego dostosowania poziomu poboru mocy do bieżących potrzeb.

Różne mikroprocesory oferują różne metody zatrzymania zegara, czasem z rozróżnieniem na sposób szybszy, ale mniej oszczędny, i sposób wymagający więcej dodatkowych zabiegów (jak upewnienie się, że ustała wszelka aktywność na magistralach łączących CPU z pamięcią oraz urządzeniami I/O), ale dający większe zyski na poborze mocy.

Opuszczenie takiego trybu pracy następuje przeważnie na sygnał przerwania przychodzący z zewnątrz. Ponieważ przyjęcie przerwania na ogół wiąże się z wywołaniem procedury jego obsługi - co dla samego w sobie obudzenia systemu z trybu oszczędzania energii może być zbędne, ale za to opóźnia reakcję mikroprocesora na podany sygnał - niektóre układy (jak np. Freescale MSC815x) pozwalają się obudzić również takim przerwaniem, które jest "wyłączone", to znaczy, zablokowane przez maskę priorytetów przerwań mikroprocesora. Gdy podać sygnał takiego przerwania, procesor "budzi się" ze stanu uśpienia, ale nie uruchamia procedury obsługi przerwania: zamiast tego po prostu zaczyna wykonywać rozkazy znajdujące się bezpośrednio za miejscem, w którym program poprzednio się zatrzymał.

Regulacja napięcia zasilania

Niektóre urządzenia dają możliwość regulacji napięcia zasilania za pośrednictwem linii sterujących połączonych z układem, który steruje zasilaczem stabilizowanym. Regulacja napięcia zasilania ma bezpośredni wpływ na pobór mocy przez urządzenie: można tego próbować, jeśli w danej chwili zapotrzebowanie na moc ze strony procesora lub urządzeń peryferyjnych jest obniżone. Niektóre mikrokontrolery (np. TI C6000) mają nawet obwód automatycznego sterowania napięciem, ale jako że jest on automatyczny, programista nie ma zbyt wielkiego wpływu na jego działanie, nie będziemy się tym więc tu bliżej zajmować.

Regulacja częstotliwości zegara

W normalnych warunkach, płynna regulacja napięcia zasilania oraz częstotliwości zegara przynoszą pewne korzyści, ale pod warunkiem, że nie stosujemy dwóch poprzednio opisanych technik, to jest całkowitego wyłączania zasilania i całkowitego zatrzymywania zegara. Jest tak, gdyż utrzymywanie wysokiej częstotliwości zegara pozwala szybciej wykonywać poszczególne zadania, a tym samym pozwala urządzeniu mieć więcej "wolnych cykli", w których zegar można całkowicie zatrzymać. Programista powinien zatem ocenić, która z tych dwóch strategii przynosi większe korzyści, i wybrać raczej jedną z nich niż stosować kombinację obydwu.

Dodatkową trudnością może być fakt, że regulacja częstotliwości zegara bywa czasochłonna, a w niektórych urządzeniach możliwa jedynie podczas uruchamiania całości. Należy w takim wypadku dobrać tę częstotliwość z zapasem wystarczającym do tego, żeby opłacalne było stosowanie innych technik oszczędzania energii. Ta wartość będzie różna w zależności od procesora i działającej na nim aplikacji - dlatego przed podjęciem strategicznych decyzji dobrym pomysłem jest dokonać wstępnego profilowania aplikacji pod tym kątem (patrz wyżej).

Budzenie zabiera czas

Ogólnie należy też wziąć pod uwagę, że gdy blok funkcjonalny jest w trybie oszczędzania energii, pewne jego komponenty nie są dostępne: może do dotyczyć też podsystemów wejścia/wyjścia. Niektóre urządzenia mogą dawać możliwość automatycznego "budzenia" uśpionych części systemu, ale nie jest to reguła.

W przypadku odcinania zasilania do całego bloku, trzeba zwrócić uwagę na kwestię zewnętrznych magistral, zegarów i sygnałów, które współdzielą z nim inne komponenty systemu. Nie do pominięcia jest też stan pamięci, a zwłaszcza kwestia jej zawartości w stosunku do zawartości cache'u procesora.

Wprowadzając urządzenie w tryb low- power programista powinien uważać, czy czas potrzebny na wybudzenie całości jest wystarczający przy założonych restrykcjach czasowych dla aplikacji czasu rzeczywistego. Tu, znowu, profilowanie kodu może się okazać konieczne: jeśli przetwarzanie pojedynczej porcji danych na pełnej mocy CPU zajmuje prawie cały interwał czasowy pomiędzy kolejno nadchodzącymi porcjami danych do przetworzenia, może się okazać, że wprowadzanie procesora w tryb oszczędzania energii nie ma żadnego sensu, bo spowoduje przekroczenie wymagań czasowych dla danej aplikacji.

Dostępy do pamięci

Powszechną praktyką jest taka organizacja dostępu do danych i kodu programu, żeby jak największe ich obszary zmieściły się w pamięci podręcznej (cache). Ma to oczywiście na celu minimalizację dostępów do wewnętrznej pamięci SRAM oraz zewnętrznej DRAM. Skutkiem tej minimalizacji jest nie tylko zwiększenie wydajności aplikacji, ale też zmniejszenie zapotrzebowania urządzenia na energię.

Jest tak dlatego, że dostępy do pamięci wymagają użycia magistral, kontrolerów pamięci oraz samych pamięci, a to pociąga za sobą aktywowanie sygnałów zegarowych dla odpowiednich elementów systemu oraz zintensyfikowane użycie zawartych w nich tranzystorów. Co z kolei, na omówionych powyżej zasadach, przekłada się na zwiększony pobór mocy.

Trzeba zaznaczyć, że pamięci dynamiczne typu DDR zawsze zużywają nieco prądu, nawet jeśli wyłączony jest sygnał CKE (Clock Enable - zezwalający pamięci na wykonywanie jakichkolwiek operacji). Niektóre kontrolery pamięci DDR udostępniają tryb oszczędzania energii, którego działanie polega na tym, że kontroler wyłącza sygnał CKE, kiedy nie ma zaplanowanych dostępów do pamięci DDR albo cykli jej odświeżania.

Jeśli pamięci mają możliwość automatycznego odświeżania zawartości, ten tryb oszczędnościowy można utrzymać dłużej, wyłączając go tylko na czas niezbędnych dostępów do zawartości DDR. Trzeba zaznaczyć, że ma to pewien ujemny wpływ na wydajność systemu, bo wyjście z trybu oszczędnościowego zajmuje pamięci nieco czasu: CPU musi w takim przypadku poczekać, aż pamięć osiągnie stan gotowości.

Producenci pamięci DDR oferują narzędzia do obliczania poboru mocy przez DDR dla poszczególnych jej stanów i zadanych jej operacji.

Jedną z cech pamięci DDR jest to, że reagują one na pewne "polecenia", a jednym z nich jest Activate: polecenie "otwarcia" konkretnego wiersza pamięci. Do jego wykonania pamięć potrzebuje stosunkowo dużo energii (mniej więcej dziesięć razy tyle, ile pobiera w stanie spoczynkowym, przy założeniu, że sygnał CKE jest aktywny).

Można spróbować zmniejszyć przeciętny pobór mocy z tego tytułu zwiększając minimalny interwał czasu, jaki musi upłynąć pomiędzy kolejnymi komendami Activate: funkcję tę oferują niektóre kontrolery DDR. Mimo że określona liczba dostępów spowoduje zużycie tej samej ilości energii, to zwiększenie tego interwału może spowodować, że szczytowe pobory mocy przez urządzenie będą mniejsze.

Niektóre kontrolery pamięci DDR oferują ułatwienia w rodzaju automatycznego podawania do pamięci komend Precharge ("zamknięcia" wiersza pamięci i zapisania jego zawartości), kiedy CPU zapisuje coś do tej pamięci. To oczywiście wydatnie (i niepotrzebnie) zwiększa pobór mocy, jeśli program np. 10 razy pod rząd generuje dostępy do tego samego wiesza pamięci. Ma to też negatywny wpływ na wydajność systemu, bo wykonanie Precharge oczywiście zajmuje pewien czas, który CPU musi odczekać, zanim zleci następny zapis.

Istnieją pewne techniki mające na celu taką konfigurację pamięci DDR, żeby kolejne dostępy pociągały za sobą jak najmniejsze liczby komend typu Activate i Precharge. Ponieważ jednak wymaga to dobrej znajomości struktury banków pamięci DDR oraz jej organizacji w konkretnym urządzeniu, nie będziemy tu w to wnikać.

Każda kolejna generacja pamięci DDR jest też bardziej restrykcyjna pod względem odczytów seryjnych (burst reads): DDR2 pozwala na odczyty czteroi ośmioimpulsowe, podczas gdy DDR3 tylko na te drugie. Oznacza to, że DDR3 będzie traktować wszystkie odczyty jako dostępy do 64 bajtów (8 impulsów po 8 bajtów).

Jeśli w rzeczywistości te porcje danych są mniejsze, wystąpią opóźnienia w dostępie do nich: efektywnie, jeśli pobieramy z DDR3 dane w porcjach po 32 bajty, pamięć będzie pracować z połową swojej wydajności, bo konstrukcja pamięci wymaga wykonania tych samych operacji przy dostępie do 32 bajtów, co przy dostępie do 64.

W konsekwencji pamięci zużywają przy obu operacjach tyle samo energii, a zatem 32-bajtowe dostępy będą marnować jej dokładnie połowę. Programista może tu wpłynąć na efektywność wykorzystania pamięci choćby organizując dane w pamięci DDR tak, żeby początki wszelkich struktur były wyrównane do granicy 64 bajtów.

Pamięci statyczne

Radykalnym sposobem zaoszczędzenia na dostępach do zewnętrznej pamięci dynamicznej jest unikanie korzystania z niej na rzecz użycia pamięci wewnętrznej systemu. Pozwala to na zmniejszenie poboru mocy nie tylko przez samą pamięć dynamiczną, ale też przez wszystkie związane z nią struktury, takie jak kontroler, magistrale itp.

Szybkie pamięci wewnętrzne to zwykle pamięci statyczne (SRAM). Różnią się one od dynamicznych tym, że nie ma w nich kwestii komend Activate, Precharge itd., a także nie istnieje problem ich cyklicznego odświeżania.

Najlepsza zasada optymalizacji dostępu do każdej pamięci jest taka, żeby skoncentrować się na uzyskaniu jak największej wydajności programu. Automatycznie oznacza to redukcję dostępów do pamięci, a co za tym idzie, zmniejszenie ogólnych kosztów korzystania z niej, to znaczy, na przykład, zużywania mocy na pracę kontrolerów, magistral, buforów itd.

Programista ma na to wpływ przez odpowiednią organizację danych i programu. Program można tak zoptymalizować, żeby zajmował jak najmniej miejsca: mniejsze programy wymagają uaktywnienia mniejszej ilości pamięci w celu odczytania kodu przez CPU. To dotyczy w takim samym stopniu pamięci dynamicznych jak i statycznych - mniej dostępów do pamięci automatycznie przekłada się na mniejszy pobór mocy.

Pożądany efekt można osiągnąć stosując różne sposoby, częściowo zależne od zadania wykonywanego przez program. Na pewno warto się zastanowić nad stworzeniem funkcji wykonujących najczęstsze zadania oraz nad wykorzystaniem złożonych rozkazów CPU, o ile architektura to przewiduje. Na przykład, użycie rozkazów mnożenia i akumulacji (multiply-accumulate, MAC), które wiele procesorów sygnałowych wykonuje w jednym cyklu, jest bardziej efektywne niż przeprowadzenie tego samego obliczenia przy użyciu dwóch oddzielnych rozkazów: najpierw mnożenia, a potem dodawania.

Ogólnym problemem jest często występująca sprzeczność pomiędzy optymalizacją programu pod względem jego wielkości a optymalizacją programu pod względem jego wydajności: program zoptymalizowany pod kątem największej wydajności przeważnie nie jest jednocześnie najmniejszy. Ogólną wskazówką może być używanie przede wszystkim takich technik programowania, które zmniejszają rozmiar kodu bez zmniejszania jego wydajności. W skrajnych przypadkach można zastosować regułę "80/20": optymalizacja wydajności powinna zostać zaaplikowana do 20% kodu, który wykonuje 80% całego zadania, natomiast resztę programu trzeba zoptymalizować pod kątem wielkości.

Ogólnie techniki optymalizacji kodu są ściśle zależne od architektury procesora, na którym będziemy uruchamiali program. Czynniki, które należy brać pod uwagę to: wielkość cache'u, liczba rdzeni, wielkość i liczba kolejek przetwarzania rozkazów, rozkazy złożone, jakie są do dyspozycji itd. Część zadania wykona za programistę dedykowany kompilator, ale trzeba pamiętać, że, wbrew pozorom, w niektórych językach (np. w języku C) to programista decyduje o strukturze kodu wynikowego, a w związku z tym użycie pewnych konstrukcji - np. pętli liczącej wstecz od konkretnej wartości do -1 zamiast "naturalnej" dla człowieka pętli liczącej w przód od 0 - może mieć wpływ na wydajność programu skompilowanego dla konkretnej maszyny. Stosowanie tego rodzaju technik na pewno wymaga doświadczenia i dobrej znajomości konkretnego procesora.

Pewne korzyści można osiągnąć przez odpowiednią organizację danych w pamięci, jednak zależy to od konstrukcji konkretnej maszyny. Dajmy na to, jeśli pamięć podręczna (cache) CPU jest połączona z wewnętrzną pamięcią systemu przez 128-bitową magistralę, wyrównanie struktur danych w pamięci SRAM do granicy 128 bitów (16 bajtów) może dać wymierne korzyści zarówno w dziedzinie wydajności jak i poboru mocy.

Pamięci podręczne

Jest interesującym faktem, że im większe są rozmiary pamięci podręcznej, tym większe jest zarazem dynamiczne i statyczne zapotrzebowanie na moc - przy czym najbardziej rośnie to drugie. Jako programiści nie mamy wpływu na wielkość cache'u procesora, ale, jeśli już mamy jego określoną ilość do dyspozycji, powinniśmy się postarać jak najlepiej ją wykorzystać (skoro i tak powoduje zwiększone zużycie energii przez całość urządzenia).

Podstawową zasadą, na której opiera się efektywność pamięci podręcznych, jest zasada tak zwanej "lokalności". Mówi ona, że jeśli procesor żąda dostępu do określonego adresu w pamięci zewnętrznej, to zachodzi stosunkowo duże prawdopodobieństwo, że wkrótce zażąda dostępu do adresu znajdującego się w bezpośrednim sąsiedztwie.

Z tego założenia wynika sposób wypełniania pamięci cache: jeśli dana, do której CPU żąda dostępu, nie znajduje się w cache'u, do cache'u zostanie pobrana zawartość całego wiersza pamięci, w której znajduje się owa dana. Jeśli wiersz liczy 256 bajtów, żądanie dostępu do naszej danej spowoduje pobranie do pamięci podręcznej tej danej oraz 255 bajtów innych danych, które znajdują się w tym samym wierszu pamięci zewnętrznej.

Sugeruje to optymalne sposoby organizacji danych w pamięci: po pierwsze, struktury danych powinny być wyrównane do granicy wierszy pamięci zewnętrznej (zależy to od jej budowy); po drugie, wspomniane struktury powinny być zorganizowane tak, żeby zawierać bloki danych przetwarzanych w możliwie jak najmniejszym okresie czasu.

Im więcej danych, których żąda procesor, znajduje się w cache'u, tym mniejsza jest aktywność zewnętrznych pamięci oraz związanych z nimi kontrolerów i magistral - wypływa stąd prosty wniosek, że im więcej krytycznych danych znajduje się w cache'u, tym mniejszy jest pobór mocy urządzenia podczas pracy.

Można z tego wyciągnąć też kolejny wniosek: jeśli program potrzebuje dostępu do większej liczby tablic zawierających dane niezbędne do złożonych obliczeń, należy zadbać, żeby dane znajdujące się w kolejnych tablicach, a przetwarzane w jednym cyklu pracy programu (np. jednym przebiegu pętli), nie znajdowały się na tych samych pozycjach (offsetach) - bo jeśli tak będzie, odpowiednie wiersze pamięci podręcznej będą ciągle nadpisywane nowymi danymi, co z kolei spowoduje zarówno straty wydajności jak i zwiększony pobór mocy. Jako programiści, możemy w tej kwestii liczyć na wsparcie ze strony kompilatora: jest on w stanie przetasować dane w tablicach tak, żeby ich analogiczne segmenty nie "pokrywały się" w cache'u.

Pamięci podręczne mogą też na ogół pracować w dwóch trybach: z buforowaniem zapisu (write-back) albo bez (write-through). W tym pierwszym trybie dane są zapisywane tylko do pamięci podręcznej, a pamięć zewnętrzna dostaje je dopiero wtedy, kiedy wiersz pamięci cache ma zostać nadpisany nowymi danymi pobranymi z tejże pamięci zewnętrznej.

W drugim trybie dane, które zapisuje procesor, są przesyłane zarówno do cache'u jak i do pamięci zewnętrznej. Przy konfigurowaniu pamięci podręcznej trzeba się zastanowić nad potencjalnymi zyskami i stratami wynikającymi z odpowiedniego ustawienia cache'u: w przypadku procesora wielordzeniowego, gdzie każdy rdzeń ma swoją pamięć podręczną, pozornie jedyną opcją jest brak buforowania zapisu, bo jest jasne, że kilka rdzeni CPU żądających dostępu do tego samego obszaru pamięci zewnętrznej musi "widzieć" tę samą jej zawartość.

Z drugiej strony powoduje to straty wydajności i zwiększony pobór mocy, bo każdy rdzeń musi czekać na wykonanie się zapisu do pamięci zewnętrznej, a sama częstość tych zapisów powoduje zwiększone zapotrzebowanie na energię z powodu intensywnego użycia magistral, kontrolerów pamięci oraz samych pamięci.

Dlatego lepiej jest ustawić cache w tryb z buforowaniem, ale, w celu utrzymania zawartości cache spójnej z zawartością pamięci zewnętrznej, w strategicznych momentach należy stosować rozkazy wymuszonego zapisu (flush) wybranych segmentów pamięci podręcznej do pamięci zewnętrznej.

Trzeba tu zwrócić uwagę na fakt, że procesory nierzadko udostępniają bardziej zaawansowane rozkazy zarządzania pamięcią podręczną, pozwalające np. zwolnić lub zapełnić konkretne jej wiersze czy segmenty. Jeśli mamy pewność, że pewna porcja danych będzie intensywnie użytkowana w bezpośredniej przyszłości, nie od rzeczy jest kazać załadować ją do cache'u (o ile się tam zmieści). Trzeba przy tym oczywiście zdawać sobie sprawę, że spowoduje to usunięcie z cache'u innych danych, muszą one być zatem na pewno niepotrzebne w danej chwili.

Obwody peryferyjne

Mikrokontrolery przesyłają strumienie danych głównie przez DMA, interfejsy szeregowe, Ethernet, i łącza radiowe. Urządzenia peryferyjne zwykle potrzebują oddzielnego taktowania, co powoduje zwiększony pobór mocy podczas korzystania z nich. Największy wpływ będą tu oczywiście miały urządzenia pozwalające na szybkie transfery danych, takie jak kontrolery DMA, Ethernet i PCI Express. Techniki odłączania zegara oraz trybów oszczędzania energii, które mają tu zastosowania, zostały omówione powyżej. Tutaj zajmiemy się ekonomią samego użycia urządzeń I/O.

Trzeba nadmienić, że jeśli któryś z interfejsów ma być ciągle gotowy do przyjęcia danych do przetworzenia, a momentu, w którym te dane się pojawią, nie można z góry przewidzieć, stosowanie wyłączenia zegara albo trybu oszczędzania energii może się nie opłacić. Może się za to opłacić podwyższenie częstotliwości taktowania dla urządzenia peryferyjnego, dzięki czemu prześle ono dane szybciej, tym samym zwiększając okresy nieaktywności CPU, co z kolei może spowodować, że może się znowu opłacać użycie trybu oszczędzania energii dla procesora. Programista ma tu pewne trudne decyzje do podjęcia, a ułatwić to mogą narzędzia dostarczone przez producenta, w rodzaju wspomnianych już powyżej kalkulatorów poboru mocy.

Mimo że poszczególne urządzenia peryferyjne różnią się pod względem konstrukcji i zasad działania, mają jednak wspólną cechę: pozwalają na odczyt i zapis danych. A zatem, naszym głównym zadaniem jest maksymalizacja przepływu danych w okresach aktywności urządzenia peryferyjnego - co pokrywa się z jak najefektywniejszym jego wykorzystaniem.

Najczęściej stosowanym sposobem zrobienia tego jest zwiększenie rozmiaru pojedynczej porcji transferowanych danych. W przypadku DMA programista ma możliwość ustawienia tej wielkości, poza wielkością całego transferu oraz adresem początkowym i końcowym. Może też zażądać wpisywania danych do pamięci począwszy od określonej granicy adresów, oraz umieszczania napływających porcji danych co określone odstępy w pamięci. Pozwala to na wpisanie danych odczytywanych z urządzenia od razu w docelowe miejsca w pamięci i wpasować je w zadane struktury, bez konieczności przepisywania ich procesorem.

Kiedy już mowa o DMA, należy się zastanowić, czy do przesyłania danych pomiędzy różnymi obszarami pamięci należy używać układów DMA czy też wykonywać przesłania procesorem. Ogólnie rzecz biorąc, układy DMA działają z niższą częstotliwością niż CPU, a poza tym są zoptymalizowane pod kątem wykonywania jednego zadania: odczytu i zapisu danych z i do pamięci.

Płynie z tego prosty wniosek, że tego typu transfery danych należy robić zawsze przez DMA, o ile wielkość samego transferu jest wystarczająco duża, żeby uzasadnić straty wydajności związane z programowaniem kontrolera DMA do wykonania transferu. W przeciwnym wypadku bardziej opłaca się przesłać dane "ręcznie", przy użyciu CPU. To samo rozumowanie w identyczny sposób odnosi się do wszelkich specjalizowanych koprocesorów, jakie programista ma do dyspozycji: na ogół ich użycie opłaca się tym bardziej im większe porcje danych można im jednorazowo powierzyć do przetworzenia.

Konfiguracja magistrali

Niektóre urządzenia oferują możliwość konfiguracji dostępu wszystkich elementów systemu do magistrali, tak żeby częściej używane jego komponenty traciły jak najmniej czasu na bezproduktywne czekanie na jej zwolnienie w celu wykonania zadanych transferów. Takie czekanie oznacza "puste" cykle pracy urządzenia i prowadzi oczywiście do odpowiednich strat energii. Producenci na ogół oferują odpowiednie narzędzia (np. profilujące), które pozwalają wykryć, gdzie jest ewentualne wąskie gardło.

Kontrola przepływu danych

Skoro już jesteśmy przy zagadnieniu urządzeń wyposażonych w kanały DMA, trzeba sobie zadać pytanie, skąd CPU ma wiedzieć, że zaprogramowany transfer danych się zakończył. Albo, skąd ma wiedzieć, że urządzenie jest gotowe do przyjęcia danych. Istnieją trzy sposoby powiadamiania procesora o tego typu wydarzeniach, które tu pokrótce omówimy.

Ciągłe odpytywanie urządzenia (polling) polega na tym, że procesor sprawdza w pętli zawartość określonych rejestrów sprzętowych, w których może pojawić się sygnał, że transfer danych się zakończył lub powinien zostać zainicjowany. Jak się łatwo domyślić, polling jest sposobem bardzo rozrzutnym: procesor zużywa wiele aktywnych cykli pracy (a tym samym: wiele mocy) na oczekiwanie, aż w rejestrze ustawi się bądź zgaśnie jeden bit.

Ta metoda ma tylko jedną zaletę, mianowicie, kiedy już się tego sygnału doczekamy, reakcja na jego pojawienie się jest błyskawiczna i niezwiązana z żadnymi dodatkowymi czynnościami ze strony CPU (na przykład, obsługą przerwania, przełączeniem kontekstu itd.). Dlatego należy ją stosować raczej wtedy, kiedy system z jednej strony musi szybko zareagować, a z drugiej - nie ma specjalnych restrykcji jeśli chodzi o zużycie energii.

Odpytywanie okresowe - w niektórych zastosowaniach (dajmy na to, w technologii GSM) programista może być pewien, że porcje danych do obrobienia będą nadchodzić co określony interwał czasowy (np. co 20 ms). W takiej sytuacji można wykorzystać tryby oszczędzania energii i "budzić" CPU przy użyciu przerwania zegarowego zaprogramowanego na ten właśnie interwał, żeby procesor mógł sprawdzić, czy kolejna porcja danych jest gotowa do odbioru.

Sposób ten zbliżony jest do pollingu, ale nie marnuje dużo mocy procesora, dzięki czemu można oszczędzić stosunkowo dużo energii. Przerwanie jest najbardziej oczywiste jest "obudzenie" uśpionego procesora nie przerwaniem zegarowym, ale przerwaniem wygenerowanym przez urządzenie peryferyjne, sygnalizującym, że dane są gotowe do odbioru (lub nadawania).

To z tej metody należy skorzystać, o ile tylko jest dostępna, gdyż oszczędności na poborze mocy będą tu największe. Większość współczesnych urządzeń I/O oczywiście oferuje możliwość zasygnalizowania pewnych stanów sygnałem przerwania podanym na CPU; w celu oszczędzenia sobie strat wynikłych ze związanym z tym często przełączeniem kontekstu, można skorzystać z możliwości wybudzenia CPU przerwaniem zabronionym, jak to już zasugerowano powyżej.

Konrad Kokoszkiewicz

Zobacz również