Jak przydatna jest „prawdziwa” wielkość zmiennych C?

9

Jedną rzeczą, która zawsze intuicyjnie wydawała mi się pozytywną cechą C (a właściwie jej implementacji, takich jak gcc, clang, ...), jest to, że nie przechowuje żadnych ukrytych informacji obok twoich zmiennych w czasie wykonywania. Rozumiem przez to, że jeśli na przykład chciałbyś mieć zmienną „x” typu „uint16_t”, możesz być pewien, że „x” zajmie tylko 2 bajty miejsca (i nie przeniesie żadnych ukrytych informacji, takich jak jej typ itp. .). Podobnie, jeśli chcesz tablicę 100 liczb całkowitych, możesz być pewien, że jest ona tak duża jak 100 liczb całkowitych.

Jednak im bardziej próbuję wymyślić konkretne przypadki użycia tej funkcji, tym bardziej zastanawiam się, czy rzeczywiście ma ona jakieś praktyczne zalety. Jedyne, co do tej pory mogłem wymyślić, to to, że oczywiście potrzebuje mniej pamięci RAM. W przypadku ograniczonych środowisk, takich jak układy AVR itp., Jest to zdecydowanie ogromny plus, ale w codziennych przypadkach użytkowania komputerów stacjonarnych / serwerów wydaje się to raczej nieistotne. Inną możliwością, o której myślę, może być pomocna / kluczowa dla uzyskania dostępu do sprzętu, a może mapowania regionów pamięci (na przykład dla wyjścia VGA i tym podobnych) ...?

Moje pytanie: Czy są jakieś konkretne domeny, które albo nie mogą, albo mogą być bardzo niewygodne do wdrożenia bez tej funkcji?

PS Powiedz mi, czy masz na to lepszą nazwę! ;)

Thomas Oltmann
źródło
@gnat Myślę, że rozumiem na czym polega twój problem. To dlatego, że może być wiele odpowiedzi, prawda? Rozumiem, że to pytanie może nie pasować do sposobu, w jaki działa wymiana stosów, ale szczerze mówiąc, nie wiem, gdzie zapytać inaczej ...
Thomas Oltmann
1
@lxrec RTTI jest przechowywany w vtable, a obiekty przechowują tylko wskaźnik do vtable. Ponadto typy mają RTTI tylko wtedy, gdy mają już vtable, ponieważ mają funkcję virtualczłonka. Tak więc RTTI nigdy nie zwiększa rozmiaru żadnych obiektów, tylko powiększa plik binarny o stałą.
3
@ThomasOltmann Każdy obiekt, który ma metody wirtualne, potrzebuje wskaźnika vtable. Bez tego nie można mieć funkcji wirtualnych metod. Co więcej, wyraźnie decydujesz się na posiadanie wirtualnych metod (a zatem i vtable).
1
@ThomasOltmann Wyglądasz na bardzo zdezorientowanego. To nie jest wskaźnik do obiektu, który nosi wskaźnik vtable, to sam obiekt. To T *znaczy, zawsze ma ten sam rozmiar i Tmoże zawierać ukryte pole wskazujące na vtable. A nie kompilator C ++ kiedykolwiek wprowadzony vtables do obiektów, które ich nie potrzebują.

Odpowiedzi:

5

Istnieje kilka korzyści, z których oczywistą jest, że w czasie kompilacji parametry takie jak parametry funkcji pasują do przekazywanych wartości.

Ale myślę, że pytasz o to, co dzieje się w czasie wykonywania.

Należy pamiętać, że kompilator utworzy środowisko wykonawcze, które osadzi wiedzę o typach danych w wykonywanych operacjach. Każdy fragment danych w pamięci może nie być samoopisujący, ale kod z natury wie, co to są dane (jeśli poprawnie wykonałeś swoją pracę).

W czasie wykonywania rzeczy są nieco inne niż myślisz.

Na przykład nie zakładaj, że podczas deklaracji uint16_t używane są tylko dwa bajty. W zależności od procesora i wyrównania słów może on zajmować 16, 32 lub 64 bity na stosie. Może się okazać, że Twój zestaw szortów zużywa znacznie więcej pamięci, niż się spodziewałeś.

Może to być problematyczne w niektórych sytuacjach, w których należy odwoływać się do danych przy określonych przesunięciach. Dzieje się tak podczas komunikacji między dwoma systemami, które mają różne architektury procesorów, za pośrednictwem łącza bezprzewodowego lub plików.

C umożliwia określenie struktur o ziarnistości na poziomie bitów:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

Ta struktura ma trzy bajty, z krótkim zdefiniowanym, aby rozpocząć od nieparzystego przesunięcia. Będzie również musiał zostać zapakowany, aby był dokładnie taki, jak zdefiniowałeś. W przeciwnym razie kompilator wyrówna słowa.

Kompilator wygeneruje kod za kulisami, aby wyodrębnić te dane i skopiować do rejestru, abyś mógł robić z nim użyteczne rzeczy.

Teraz możesz zobaczyć, że za każdym razem, gdy mój program uzyskuje dostęp do członka struktury myMessage, będzie wiedział, jak dokładnie go wyodrębnić i na nim operować.

Może to stać się problematyczne i trudne w zarządzaniu podczas komunikacji między różnymi systemami z różnymi wersjami oprogramowania. Musisz dokładnie zaprojektować system i kod, aby zapewnić, że obie strony mają dokładnie taką samą definicję typów danych. W niektórych środowiskach może to być dość trudne. Tutaj potrzebujesz lepszego protokołu, który zawiera dane samoopisujące, takie jak Bufory protokołów Google .

Na koniec warto zadać sobie pytanie, jak ważne jest to w środowisku pulpitu / serwera. To zależy od tego, ile pamięci planujesz użyć. Jeśli wykonujesz coś takiego jak przetwarzanie obrazu, możesz skończyć z użyciem dużej ilości pamięci, co może wpłynąć na wydajność aplikacji. Jest to z pewnością zawsze problem w środowisku osadzonym, w którym pamięć jest ograniczona i nie ma pamięci wirtualnej.

Tereus Scott
źródło
2
„Może się okazać, że Twój zestaw szortów zużywa znacznie więcej pamięci, niż się spodziewałeś”. Jest to niewłaściwe w C: tablice mają gwarancję, że zawierają swoje elementy w sposób wolny od przerw. Tak, tablica musi być odpowiednio wyrównana, podobnie jak pojedyncza short. Jest to jednak jednorazowe wymaganie dotyczące początku tablicy, reszta jest automatycznie dopasowywana poprawnie ze względu na to, że jest kolejna.
cmaster
Również składnia wypełnienia jest nieprawidłowa, powinna być uint8_t padding: 6;, podobnie jak pierwsze dwa bity. Lub, bardziej precyzyjnie, tylko komentarz //6 bits of padding inserted by the compiler. Struktura, jak ją napisałeś, ma rozmiar co najmniej dziewięciu bajtów, a nie trzech.
cmaster
9

Trafiłeś na jeden z jedynych powodów, dla których jest to przydatne: mapowanie zewnętrznych struktur danych. Należą do nich bufory wideo odwzorowane w pamięci, rejestry sprzętowe itp. Obejmują one również dane przesyłane w stanie nienaruszonym poza programem, takie jak certyfikaty SSL, pakiety IP, obrazy JPEG i prawie każda inna struktura danych, która ma trwałe życie poza programem.

Ross Patterson
źródło
5

C jest językiem niskiego poziomu, prawie przenośnym asemblerem, więc jego struktury danych i konstrukcje językowe są zbliżone do metalu (struktury danych nie mają ukrytych kosztów - oprócz wypełnienia, wyrównania i ograniczeń wielkości narzuconych przez sprzęt i ABI ). Tak więc C rzeczywiście nie ma natywnego pisania dynamicznego. Ale jeśli potrzebujesz, możesz przyjąć konwencję, że wszystkie twoje wartości są agregatami, zaczynając od pewnych informacji o typie (np. Niektóre enum...); Zastosowanie union-s i (w przypadku, tablicowej przedmiotów) elastycznego członu matrycy na structzawierającego również rozmiar tablicy.

(programując w C, Twoim obowiązkiem jest zdefiniowanie, udokumentowanie i przestrzeganie użytecznych konwencji - w szczególności warunków wstępnych i dodatkowych oraz niezmienników; również dynamiczna alokacja pamięci C wymaga wyjaśnienia konwencji, kto powinien mieć strefę pamięci o freedużym malloczasięgu)

Tak więc, aby przedstawić wartości, które są liczbami całkowitymi w ramkach, ciągami znaków lub jakimś symbolem podobnym do schematu lub wektorem wartości, koncepcyjnie użyjesz oznaczonego połączenia (zaimplementowanego jako połączenie wskaźników) - zawsze zaczynając od rodzaju -, np .:

enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
   const void* vptr; // generic pointer, e.g. to free it
   enum value_kind_en* vkind; // the value of *vkind decides which member to use
   struct intvalue_st* vint;
   struct strvalue_st* vstr;
   struct symbvalue_st* vsymb;
   struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE  ((value_t){NULL})
struct intvalue_st {
  enum value_kind_en kind; // always V_INT for intvalue_st
  int num;
};
struct strvalue_st {
  enum value_kind_en kind; // always V_STRING for strvalue_st
  const char*str;
};
struct symbvalue_st {
  enum value_kind_en kind; // V_SYMBOL
  struct strvalue_st* symbname;
  value_t symbvalue;
};
struct vectvalue_st {
  enum value_kind_en kind; // V_VECTOR;
  unsigned veclength;
  value_t veccomp[]; // flexible array of veclength components.
};

Aby uzyskać dynamiczny typ jakiejś wartości

enum value_kind_en value_type(value_t v) {
  if (v.vptr != NULL) return *(v.vkind);
  else return V_NONE;
}

Oto „dynamiczny rzut” na wektory:

struct vectvalue_st* dyncast_vector (value_t v) {
   if (value_type(v) == V_VECTOR) return v->vvect;
   else return NULL;
}

oraz „bezpieczny akcesorium” wewnątrz wektorów:

value_t vector_nth(value_t v, unsigned rk) {
   struct vectvalue_st* vecp = dyncast_vector(v);
   if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
   else return NULL_VALUE;
}

Zazwyczaj zdefiniujesz większość krótkich funkcji powyżej, jak static inlinew niektórych plikach nagłówkowych.

BTW, jeśli możesz użyć zbieracza śmieci Boehm'a, możesz dość łatwo kodować w stylu wyższego poziomu (ale niebezpiecznym), a kilka interpretatorów schematów jest wykonywanych w ten sposób. Wariacyjny konstruktor wektorowy może być

value_t make_vector(unsigned size, ... /*value_t arguments*/) {
   struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
   vec->kind = V_VECTOR;
   va_args args;
   va_start (args, size);
   for (unsigned ix=0; ix<size; ix++) 
     vec->veccomp[ix] = va_arg(args,value_t);
   va_end (args);
   return (value_t){vec};
}

a jeśli masz trzy zmienne

value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;

możesz zbudować z nich wektor za pomocą make_vector(3,v1,v2,v3)

Jeśli nie chcesz używać zbieracza śmieci Boehm'a (lub zaprojektować swój własny), powinieneś bardzo ostrożnie definiować destruktory i dokumentować, kto, jak i kiedy pamięć powinna być free-d; zobacz ten przykład. Możesz więc użyć malloc(ale następnie przetestować pod GC_MALLOCkątem jego awarii) zamiast powyższego, ale musisz dokładnie zdefiniować i użyć funkcji destruktoravoid destroy_value(value_t)

Siła C ma być na niskim poziomie, aby umożliwić kod jak wyżej i zdefiniować własne konwencje (szczególnie dla twojego oprogramowania).

Basile Starynkevitch
źródło
Myślę, że źle zrozumiałeś moje pytanie. Nie chcę dynamicznego pisania w C. Byłem ciekawy, czy ta konkretna właściwość C ma jakieś praktyczne zastosowanie.
Thomas Oltmann
Ale do jakiej konkretnej właściwości C odnosisz się? Struktury danych C są zbliżone do metalu, więc nie mają ukrytych kosztów (oprócz ograniczeń związanych z wyrównaniem i rozmiarem)
Basile Starynkevitch,
Dokładnie tak: /
Thomas Oltmann
C został wynaleziony jako język niskiego poziomu, ale po włączeniu optymalizacji w kompilatorach takich jak gcc przetwarza język, który używa składni niskiego poziomu, ale nie zapewnia niezawodnie dostępu na niskim poziomie do zapewnionych przez platformę gwarancji behawioralnych. Potrzebny jest sizeof, aby używać malloc i memcpy, ale użycie do bardziej wyszukanych obliczeń adresów może nie być obsługiwane w „nowoczesnym” C.
supercat