ARTYKUŁ W TRAKCIE TWORZENIA
CZYTASZ NA WŁASNE RYZYKO
:-)
Autor: BlueDraco
Redakcja: Dondu
Kurs języka C: Tips & Tricks - Spis treści
Bardzo często mamy w programie potrzebę „zawinięcia” indeksu tablicy, czyli spowodowania, by po dojściu do końca tablicy indeks „przeskoczył" na jej początek. Korzystamy z tego m.in. przy obsłudze mulipleksowanych wyświetlaczy LED oraz przy realizacji wszelkich buforów cyklicznych.
Najbardziej ogólne i uniwersalne rozwiązanie programowe polega na zdefiniowaniu rozmiaru wektora jako stałej preprocesora i zadeklarowaniu bufora oraz indeksu, na przykład tak:
#define BUF_SIZE 8 unsigned int buf[BUF_SIZE]; unsigned int idx;
Musimy przy tym oczywiście zwrócić uwagę, by typ użyty dla indeksu umożliwiał zapisanie wartości z zakresu od 0 do BUF_SIZE – 1, czyli przy BUF_SIZE nie przekraczającym 256 wystarczy unsigned char, a dla większych rozmiarów powinniśmy użyć typu unsigned int.
Dostęp do kolejnego elementu wektora i modyfikacja indeksu mogą wyglądać np. tak:
x = buf[idx ++];
Po czym zaraz powinno nastąpić zawinięcie indeksu, co najprościej zapisać tak:
idx %= BUF_SIZE;
Zapis ten jest bardzo prosty i czytelny - idx zyskuje wartość będącą resztą z dzielenia bieżącej wartości przez BUF_SIZE, czyli wartość BUF_SIZE zostaje zamieniona w wartość 0.
Warto jednak przyjrzeć się bliżej tej prościutkiej instrukcji i zdać sobie sprawę z konsekwencji jej użycia, które silnie zależą od wartości stałej BUF_SIZE.
Zacznijmy od zauważenia, że jeśli BUF_SIZE ma wartość 256, a idx jest typu unsigned char, to instrukcja „zawijająca” indeks staje się całkowicie zbędna, gdyż w wyniku samej inkrementacji 8-bitowej danej o wartości 255 uzyskujemy wartość 0.
Jeżeli BUF_SIZE jest potęgą dwójki (np. 4, 8, 16, 32 itd.), to dobry kompilator zamieni naszą instrukcję
idx %= BUF_SIZE;
na równoważny jej zapis
idx &= BUF_SIZE – 1;
Czyli np. zamiast
idx %= 4;
kompilator wykona operację
idx &= 3;
Jeśli nasze zaufanie do zdolności optymalizacyjnych kompilatora jest ograniczone, możemy wyręczyć kompilator zapisując instrukcję jawnie w postaci iloczynu logicznego.
Co się stanie, gdy rozmiar bufora nie będzie potęgą dwójki?
Po prostu kompilator nie zoptymalizuje operacji zawinięcia według pokazanego powyżej schematu i wykona dzielenie zgodnie z zapisem programu, przypisując do zmiennej idx wartość reszty z tego dzielenia.
Programista powinien zdawać sobie sprawę z kosztu tej operacji. Jeśli procesor nie ma instrukcji dzielenia, dzielenie jest realizowane przez procedurę programową, a jego czas jest porównywalny z czasem wykonania od kilkudziesięciu do nawet kilkuset dodawań. Z taką sytuacją mamy do czynienia w niemal wszystkich mikrokontrolerach 8-bitowych (w tym AVR i PIC), ale również w 32-bitowym rdzeniu Cortex-M0. Nawet w procesorach używanych w komputerach PC czas operacji dzielenia liczb całkowitych jest porównywalny z czasem kilkunastu dodawań.
Jeśli więc nie chcemy przeciążyć naszego mikrokontrolera zbędnym w tym przypadku dzieleniem, powinniśmy napisać:
if (idx == BUF_SIZE) idx = 0;
która to sekwencja na typowym mikrokontrolerze 8-bitowym, ale również na 32-bitowych Cortexach-M0 wykona się szybciej, niż np.
idx %= 23;
Co prawda nasza strona jest poświęcona mikrokontrolerom, ale wypada wspomnieć, że w przypadku procesorów x86 stosowanych w PC wcale nie jest oczywiste, które z tych rozwiązań jest szybsze – odgrywa tutaj rolę wiele złożonych czynników.
Przykłady
Poniżej kilka przykładów dostępnych w kompilatorze CManiak, który powinien być w tej chwili widoczny w lewym górnym rogu przeglądarki. Możesz uruchamiać oraz modyfikować przykłady we własnym zakresie, aby poćwiczyć zawijanie indeksów.
Przykład 1:
Obliczenie kwadratów liczb na bazie indeksu tablicy i zapisanie wyniku ich do tablicy pod odpowiadającym mu indeksem.
#include <stdio.h>
int main(void){
#define BUF_SIZE 8
unsigned int buf[BUF_SIZE];
unsigned int idx;
//oblicz kwadraty kolejnych liczb i zapisz w tablicy
idx = 0;
while(idx < BUF_SIZE) buf[idx++] = idx * idx; //idx do kwadratu
//pokaż wyniki zapisane w tablicy
idx = 0;
while(idx < BUF_SIZE) {
printf("idx[%d] = %d \n", idx, buf[idx]);
idx++;
}
return 0;
}
Przykład 2:
Przykład podobnie jak powyższy liczący kwadraty liczb, lecz wyświetlający wielokrotnie zawartość tablicy począwszy od wybranej komórki.
#include <stdio.h>
#include <stdio.h>
int main(void){
#define BUF_SIZE 8
unsigned int buf[BUF_SIZE];
unsigned int idx;
//wypełniamy tablicę wartościami kwadratu idx
//oraz pokaż wyniki zapisane w tablicy
printf("Tablica wypełniona - stan początkowy: \n");
for(idx=0; idx<BUF_SIZE; idx++){
buf[idx] = idx * idx;
printf("tab[%d] = %d \n", idx, buf[idx]);
}
//Wyświetalmy tablicę począwszy od wybranej komórki
//wykonując X zadanych kroków
//po natrafieniu na koniec tablicy zawijamy indeks, by kolejne
//komórki tablicy pokazywać od jej poczatku
printf("\nElementy pokazane począwszy od wybranego: \n");
#define ILOSC_KROKOW 18
#define KOMORKA_POCZATKOWA 3
idx = KOMORKA_POCZATKOWA;
unsigned int krok = 0;
while(krok++ < ILOSC_KROKOW) {
printf("krok=%-3d tab[%d]=%d \n", krok, idx, buf[idx]);
idx++;
idx %= BUF_SIZE; //zawiń indeks
}
return 0;
}
Kurs języka C: Tips & Tricks - Spis treści
40






Brak komentarzy:
Prześlij komentarz