Preprocesor w C++

Preprocesor w C++
AN
  • Rejestracja:prawie 19 lat
  • Ostatnio:około 4 godziny
0

Ja hobbystycznie czasem programuję w C na mikrokontrolery i C++/Qt na Windows. W obu przypadkach często korzystam z dyrektywy #define i czasem z #ifdef. U mnie najczęstsze zastosowania są następujące:

  1. Włączanie i wyłączanie fragmentów kodu poprzez zmianę w jednym miejscu (na przykład, jak testuję różne rozwiązania tego samego, bądź jak chce mieć możliwość wydruku pewnych informacji przez cout tylko do testów, podczas, gdy ostatecznie ten wydruk nie jest potrzebny).
  2. Stała, która pojawia się w wielu miejscach w kodzie, również w definicji zmiennej (np. wielkość tablicy).
  3. Bardziej skomplikowane wyrażenie, zamiast pisać w wielu miejscach to robię #define i w nim to wyrażenie, a jak potrzebuję zmienić, to zmieniam w jednym miejscu.

Ja doskonale znam sposób działania #define i używam szczególnie, jak zależy mi na wydajności (kod funkcji jest bardzo często uruchamiany w pętli). Wiem co to jest "inline", ale czytałem, że nie każdy kompilator i nie w każdym przypadku to respektuje. A jak chce się używać tego samego dla zmiennych różnych typów, to albo musi być tyle funkcji, ile jest możliwości, albo kompilator musi dodać zmianę typu zmiennej przy uruchamianiu funkcji, podczas, gdy to nie jest potrzebne. Wyrażenia warunkowe dla wartości stałych? Trochę bez sensu obsługiwać zmienną, która przez cały czas będzie mieć tą samą wartość i na przykład w zależności od niej ma uruchomić lub pominąć jakiś kod. A jak wyrażenie nie jest nigdzie użyte, to nie powstaje martwy kod.

Widzę, że w innych językach nie ma preprocesora, w C# jakiś jest, ale nie ma możliwości zdefiniowania stałych fragmentów kodu (czyli #define z wyrażeniem, które ma być wstawiane w miejscu zdefiniowanego słowa).

Przykładowo, mam takie wyrażenia, które używam w swoich projektach. jest to chyba lepsze rozwiązanie wydajnościowo niż robienie funkcji dla różnych typów liczb, prawda?

Kopiuj
#define Diff(X, Y) ((((X) - (Y)) > 0) ? ((X) - (Y)) : ((Y) - (X))) // Różnica bezwzględna
#define Abs(X) (((X) > 0) ? (X) : (0 - (X))) // Wartość bezwzględna
#define Min(X, Y) (((X) > (Y)) ? (Y) : (X)) // Wartość minimalna z dwóch
#define Max(X, Y) (((X) < (Y)) ? (Y) : (X)) // Wartość maksymalna z dwóch
#define Shl(X, Y) (((Y) >= 0) ? ((X) << (Y)) : ((X) >> (0 - (Y)))) // Przesunięcie bitowe w lewo, ale wartość ujemna w ilości bitów powoduje przesunięcie w prawo
#define Shr(X, Y) (((Y) >= 0) ? ((X) >> (Y)) : ((X) << (0 - (Y)))) // Przesunięcie bitowe w prawo, ale wartość ujemna w ilości bitów powoduje przesunięcie w lewo
#define Range(X, A, B) (((X) < (A)) ? (A) : (((X) > (B)) ? (B) : (X))) // Ograniczenie wartości do wskazanego zakresu, jak wykracza poza zakres, to wartość jest zmieniana do wartości granicznej zakresu

Czy może #define ma jakieś wady, które sprawiają, że w innych językach programowania nie ma takiej możliwości?
Czy w "poważnych" (dużych, komercyjnych) projektach w C i C++ używa się #define? Jeżeli nie to dlaczego (chodzi o inny powód niż brak potrzeby w danym projekcie)?

Inny przykład, który wymyśliłem na własne potrzeby, który pozwala wychwycić utworzone i niezniszczone obiekty (wyciek pamięci), ponowne zniszczenie zniszczonego obiektu, pomylenie delete z delete[], pomieszanie new/delete z malloc/free:

Kopiuj
#define OBJMEMSTR std::cout
#define OBJMEMSIZETYPE size_t
#ifdef OBJMEMDEBUG
 #define NEW(T, C)              ([](T * __ptr) -> T*   { OBJMEMSTR << "NEW_OBJ_" << (void*)__ptr << "_" << #T << std::endl;   return __ptr; })(new C)
 #define NEWARR(T, C)           ([](T * __ptr) -> T*   { OBJMEMSTR << "NEW_ARR_" << (void*)__ptr << "_" << #T << std::endl;   return __ptr; })(new C)
 #define NEWARRINIT(T, C, N, V) ([](T * __ptr, OBJMEMSIZETYPE __N, T __V) -> T*   { OBJMEMSTR << "NEW_ARR_" << (void*)__ptr << "_" << #T << std::endl; for (OBJMEMSIZETYPE __it = 0; __it < __N; __it++) { __ptr[__it] = __V; } return __ptr; })(new C, N, V)
 #define DEL(T, P)              ([](T * __ptr) -> void { OBJMEMSTR << "DEL_OBJ_" << (void*)__ptr << "_" << #T << std::endl;   delete __ptr; })(P)
 #define DELARR(T, P)           ([](T * __ptr) -> void { OBJMEMSTR << "DEL_ARR_" << (void*)__ptr << "_" << #T << std::endl; delete[] __ptr; })(P)

 #define MALLOC(S)              ([](OBJMEMSIZETYPE __S)                     -> void* { void * __ptr = malloc(__S);      OBJMEMSTR << "NEW_MEM_" << __ptr << "_void" << std::endl; return __ptr; })(S)
 #define CALLOC(N, S)           ([](OBJMEMSIZETYPE __N, OBJMEMSIZETYPE __S) -> void* { void * __ptr = calloc(__N, __S); OBJMEMSTR << "NEW_MEM_" << __ptr << "_void" << std::endl; return __ptr; })(N, S)
 #define FREE(P)                ([](void * __ptr)                           -> void  {                                  OBJMEMSTR << "DEL_MEM_" << __ptr << "_void" << std::endl;  free(__ptr); })(P)
 #define REALLOC(P, S)          ([](void * __ptr, OBJMEMSIZETYPE __S)       -> void* { void * __ptr0; OBJMEMSTR << "DEL_MEM_" << __ptr << "_void" << std::endl; __ptr0 = realloc(__ptr, __S); OBJMEMSTR << "NEW_MEM_" << __ptr0 << "_void" << std::endl; return __ptr0; })(P, S)
#else
 #define NEW(T, C)              new C
 #define NEWARR(T, C)           new C
 #define NEWARRINIT(T, C, N, V) ([](T * __ptr, OBJMEMSIZETYPE __N, T __V) -> T*   { for (OBJMEMSIZETYPE __it = 0; __it < __N; __it++) { __ptr[__it] = __V; } return __ptr; })(new C, N, V)
 #define DEL(T, P)              delete P
 #define DELARR(T, P)           delete[] P

 #define MALLOC(S)              malloc(S)
 #define CALLOC(N, S)           calloc(N, S)
 #define FREE(P)                free(P)
 #define REALLOC(P, S)          realloc(P, S)
#endif

Wystarczy zamiast new i delete używać NEW i DELETE, a po przetestowaniu programu skasować #define OBJMEMDEBUG i program skompiluje się dokładnie tak, jakby w ogóle tego powyższego nie było.

edytowany 1x, ostatnio: andrzejlisek
au7h
ale brzydki ten kod. Ktoś będzie chciał ogarnąć tego byka: #define NEWARRINIT(T, C, N, V) ([](T * __ptr, OBJMEMSIZETYPE __N, T _V) -> T* { OBJMEMSTR << "NEW_ARR" << (void*)__ptr << "_" << #T << std::endl; for (OBJMEMSIZETYPE __it = 0; __it < __N; __it++) { __ptr[__it] = __V; } return __ptr; })(new C, N, V) to na trzeźwo nie da rady
AN
Ten kod powstał trochę metodą prób, błędów i przerabiania tego, co znalazłem za pomocą www.google.com, nie musiał być ładny, ale ma spełniać swoją funkcję i ją spełnia. Musiałem jakoś wepchnąć typ obiektu, typ elementu, konstruktor obiektu i wartość, którą tablica ma być zainicjowana. Sposób użycia to: uchar * Bin = NEWARR(uchar, uchar[BinSize], BinSize, 0) OBJMEMSIZETYPE jest potrzebny, bo typ wielkość pamięci może być różny w zależności od kompilatora Poza tym, nie znalazłem sposobu, jak jedną definicję opisać w kilku liniach, dlatego kod nie jest sformatowany
AN
A jak zajdzie potrzeba, żeby zamiast na ekran wprowadzać treść do pliku, to wystarczy tylko zmienić definicję OBJMEMSTR
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
2

Wiem co to jest "inline", ale czytałem, że nie każdy kompilator i nie w każdym przypadku to respektuje

Z dwadzieścia lat temu to pewnie tak było - obecnie jednak to kompilatory są lepsze w tego typu optymalizacjach niż ludzie.

Widzę, że w innych językach nie ma preprocesora

Niektóre inne języki posiadają systemy makr (np. Rust), który pełni zbliżoną funkcję.

Przykładowo, mam takie wyrażenia, które używam w swoich projektach. jest to chyba lepsze rozwiązanie wydajnościowo niż robienie funkcji dla różnych typów liczb, prawda?

"chyba"? W sensie, że klepnąłeś sobie kilkadziesiąt makr i ani jednego nie przetestowałeś pod kątem wydajności, tylko w ciemno założyłeś, że Twoja wersja jest lepsza?

Pierwsze wydanie GCC pojawiło się w 1987 roku - od tego czasu twórcy mieli naprawdę ogrom czasu na wprowadzenie wszelkiego rodzaju optymalizacji; GCC (kontynuując przykład) potrafi wektoryzować kod, automatycznie inline'ować funkcje (wcale nie musisz mu w tym pomagać!), zmieniać kolejność instrukcji w celu optymalizacji kodu na konkretną architekturę i tak dalej, i tak dalej.

Twoje makra mające na celu "inline'owanie" wypadają przy tym wszystkim blado i prawdopodobnie nawet te dwadzieścia lat temu byłyby zbędne - to są absolutne podstawy i punkty wejścia do kolejnych optymalizacji we wszystkich kompilatorach.

Rzuć sobie okiem, jakie cuda potrafi robić LLVM (np. kompilując wysokopoziomowy, idiomatyczny kod Rusta) to dopiero zrobisz wielkie oczy ;-)

Inny przykład, który wymyśliłem na własne potrzeby, który pozwala wychwycić [...] pomylenie delete z delete[] [...]

Przecież w dalszym ciągu mogę zrobić NEWARR(...), po którym odpalę FREE(...) - w czym tu pomogą Twoje makra?


edytowany 13x, ostatnio: Patryk27
AN
  • Rejestracja:prawie 19 lat
  • Ostatnio:około 4 godziny
0

Przykładowo, mam takie wyrażenia, które używam w swoich projektach. jest to chyba lepsze rozwiązanie wydajnościowo niż robienie funkcji dla różnych typów liczb, prawda?

"chyba"? W sensie, że klepnąłeś sobie kilkadziesiąt makr i ani jednego nie przetestowałeś pod kątem wydajności, tylko w ciemno założyłeś, że Twoja wersja jest lepsza?

Pierwsza wersja GCC pojawiła się w 1987 roku - od tego czasu twórcy mieli naprawdę ogrom czasu na wprowadzenie wszelkiego rodzaju optymalizacji; GCC (kontynuując przykład) potrafi wektoryzować kod, automatycznie inline'ować funkcje (wcale nie musisz mu w tym pomagać!), zmieniać kolejność instrukcji w celu optymalizacji kodu na konkretną architekturę i tak dalej, i tak dalej.

Twoje makra mające na celu optymalizację wypadają przy tym wszystkim blado i prawdopodobnie nawet te dwadzieścia lat temu byłyby zbędne - to są absolutne podstawy we wszystkich kompilatorach.

Przykład pierwszy z brzegu, może i nie trafiony, ale chciałem zilustrować sens używania makr. Równie dobrze może być bardziej skomplikowane wyrażenie potrzebne w danym konkretnym projekcie nie występujące w bibliotece standardowej.

Inny przykład, który wymyśliłem na własne potrzeby, który pozwala wychwycić [...] pomylenie delete z delete[] [...]

Przecież w dalszym ciągu mogę zrobić NEWARR(...), po którym odpalę FREE(...) - w czym tu pomogą Twoje makra?

Na standardowym wyjściu lub w innym strumieniu wypisze się taki tekst:
NEW_ARR_int_1234
DEL_MEM_1234
Po tym tekście będziesz wiedział, dlaczego Twój program nie zadziałał tak, jak oczekiwałeś (w miejscu 1234 będzie adres obiektu). Nawet, jak zadziałał, to przy dalszym rozwoju prędzej czy później będzie jakiś problem i normalnie to byś pół dnia szukał, dlaczego jest problem.

A jak zobaczysz jeden z poniższych tekstów, to znaczy, że tablicę utworzyłeś i zniszczyłeś prawidłowo.
NEW_ARR_int_1234
DEL_ARR_int_1234
lub
NEW_MEM_1234
DEL_MEM_1234
A jak Twój program nadal nie działa, to przyczyna jest inna niż nieprawidłowość przy tworzeniu i niszczeniu tablicy.
Dodatkowo, jak przy DEL będzie inny adres niż przy NEW, to znaczy, że coś pomieszałeś.

A jak zdebugujesz i usuniesz wszystkie błędy, to robisz jedną zmianę (usunięcie "#define OBJMEMDEBUG") i kod po przejściu przez preprocesor staje się identyczny, jak przy standardowym tworzeniu i niszczeniu obiektów, bez dodatkowych czynności. A jak rozwiniesz swój program, to znowu dodasz "#define OBJMEMDEBUG" i będziesz mógł sprawdzić, czy prawidłowo tworzysz i usuwasz obiekty. Prościej się chyba nie da, a przynajmniej w sposób działający niezależnie od kompilatora i systemu operacyjnego.

edytowany 3x, ostatnio: andrzejlisek
MY
  • Rejestracja:ponad 9 lat
  • Ostatnio:9 dni
  • Postów:1082
0

@andrzejlisek co do wydajności to pytanie jak to testowałeś. Wiadomo, że wywołanie funkcji jest związane z pewnym narzutem. Jednak jest to dosłownie kilka instrukcji na poziomie CPU. Pytanie czy nie jest tak, że w większości przypadków, że będzie to zaledwie 1% czasu całego wywołania funkcji? Wiadomo większość funkcji jest bardziej skomplikowana niż te parę instrukcji. Jeśli tak, to nie tędy droga. Generalnie należałoby zapuścić prolifer i sprawdzić co zjada największą ilość czasu i dopiero te miejsca zacząć optymalizować. Zgodnie z powiedzonkiem The premature optimization is the root of all evil...

Jeśli chodzi o to dlaczego NIE używać makr. Chodzi chociażby o czytelność kodu. Weź spróbuj zdebugować taki kod najeżony wywołaniami takich definicji. Ja dziękuję, postoję.

AN
  • Rejestracja:prawie 19 lat
  • Ostatnio:około 4 godziny
0
Mr.YaHooo napisał(a):

@andrzejlisek co do wydajności to pytanie jak to testowałeś. Wiadomo, że wywołanie funkcji jest związane z pewnym narzutem. Jednak jest to dosłownie kilka instrukcji na poziomie CPU. Pytanie czy nie jest tak, że w większości przypadków, że będzie to zaledwie 1% czasu całego wywołania funkcji? Wiadomo większość funkcji jest bardziej skomplikowana niż te parę instrukcji. Jeśli tak, to nie tędy droga. Generalnie należałoby zapuścić prolifer i sprawdzić co zjada największą ilość czasu i dopiero te miejsca zacząć optymalizować. Zgodnie z powiedzonkiem The premature optimization is the root of all evil...

Nie robiłem dokładnych testów wydajnościowych, ale przyjąłem założenie, że mam trzy kody robiące dokładnie to samo:
Kod A:

Kopiuj
int func(int A, int B)
{
    return (A != B) ? ((A > B) ? 1 : -1) : 0;
}
int main()
{
    int L = 100;
    int T1[100];
    int T2[100];
    int T3[100];
    int T4[100];
    int K1 = 0;
    int K2 = 0;

    // W tym miejscu powinien być kod wypełnienia tablic jakimiś danymi

    for (int I = 0; I < L; I++)
    {
        K1 += func(T1[I], T2[I]);
        K2 += func(T3[I], T4[I]);
    }
    cout << K1 << " " << K2 << endl;
    return 0;
}

Kod B:

Kopiuj
int main()
{
    int L = 100;
    int T1[100];
    int T2[100];
    int T3[100];
    int T4[100];
    int K1 = 0;
    int K2 = 0;

    // W tym miejscu powinien być kod wypełnienia tablic jakimiś danymi

    for (int I = 0; I < L; I++)
    {
        K1 += (T1[I] != T2[I]) ? ((T1[I] > T2[I]) ? 1 : -1) : 0;
        K2 += (T3[I] != T4[I]) ? ((T3[I] > T4[I]) ? 1 : -1) : 0;
    }
    cout << K1 << " " << K2 << endl;
    return 0;
}

Kod C:

Kopiuj
#define func(A, B) ((A) != (B)) ? (((A) > (B)) ? 1 : -1) : 0
int main()
{
    int L = 100;
    int T1[100];
    int T2[100];
    int T3[100];
    int T4[100];
    int K1 = 0;
    int K2 = 0;

    // W tym miejscu powinien być kod wypełnienia tablic jakimiś danymi

    for (int I = 0; I < L; I++)
    {
        K1 += func(T1[I], T2[I]);
        K2 += func(T3[I], T4[I]);
    }
    cout << K1 << " " << K2 << endl;
    return 0;
}

Intuicja podpowiada, że kod B jest wydajniejszy od kodu A, ponieważ nie ma wywołania i powrotu z funkcji, za to kod A wygeneruje mniejszy plik wykonywalny od kodu B, bo w B powtarza się to samo wyrażenie. Wiadomo, że optymalizacja ogólna nie istnieje, bo albo jest optymalizacja wydajności kosztem wielkości, albo optymalizacja wielkości kosztem wydajności. Natomiast kod C przypomina kod A, jednak po przetworzeniu makr przed właściwą kompilacją stanie się identyczny z kodem B. Czy moje rozumowanie jest prawidłowe?

Jeśli chodzi o to dlaczego NIE używać makr. Chodzi chociażby o czytelność kodu. Weź spróbuj zdebugować taki kod najeżony wywołaniami takich definicji. Ja dziękuję, postoję.

Z tym się zgadzam, jeżeli się za bardzo przesadzi z makrami.

edytowany 1x, ostatnio: andrzejlisek
AK
  • Rejestracja:ponad 6 lat
  • Ostatnio:dzień
  • Postów:3561
0
Patryk27 napisał(a):

Wiem co to jest "inline", ale czytałem, że nie każdy kompilator i nie w każdym przypadku to respektuje

Z dwadzieścia lat temu to pewnie tak było - obecnie jednak to kompilatory są lepsze w tego typu optymalizacjach niż ludzie.

Widzę, że w innych językach nie ma preprocesora

Niektóre inne języki posiadają systemy makr (np. Rust), który pełni zbliżoną funkcję.

Makro-generacja w C była spadkiem po makro-assemblerach
Język (chyba już słusznie zapomniany) TCL jest jakby jednym wielkim makrogeneratorem, tam nawet if() { był swoistym makrem (dla mnie jest koszmarkiem, kiedyś miałem API aplikacji w TCLu pod opieką)
Java odrzuciła przerosty i miejsca niebezpieczne C++, w tym makra.
Delphi i C# (które podejmowało/korygowało szczęśliwe/nieszczęśliwe pomysły Java) mają warunkową kompilację bloku kodu i tyle. Myślę że to jest właściwe podsumowanie refleksji nad makrami. Resztę historia (ewolucja) wyeliminowała

zagadka, jak się wykona

Kopiuj
int c = MAX(*s++, ' ')

Myślę że nie ma sensu dla ambitnych "funkcyjnych" makr, ze względów na inline, potęgę współczesnych kompilatorów itd...


Bo C to najlepszy język, każdy uczeń ci to powie
edytowany 1x, ostatnio: AnyKtokolwiek
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

Intuicja podpowiada, że kod B jest wydajniejszy od kodu A, ponieważ nie ma wywołania i powrotu z funkcji,

To może nie sugeruj się intuicją, tylko wykonaj benchmark? ;-p

Ewentualnie wklej swój kod do Compiler Explorer i na własne oczy zobacz, że już przy -O1 żadnego wywołania funkcji tam w rzeczywistości nie otrzymasz, ponieważ - jak już wspomniałem - mamy XXI wiek, kompilatory nie są głupie.


MY
  • Rejestracja:ponad 9 lat
  • Ostatnio:9 dni
  • Postów:1082
0
andrzejlisek napisał(a):

Intuicja podpowiada, że kod B jest wydajniejszy od kodu A, ponieważ nie ma wywołania i powrotu z funkcji, za to kod A wygeneruje mniejszy plik wykonywalny od kodu B, bo w B powtarza się to samo wyrażenie. Wiadomo, że optymalizacja ogólna nie istnieje, bo albo jest optymalizacja wydajności kosztem wielkości, albo optymalizacja wielkości kosztem wydajności. Natomiast kod C przypomina kod A, jednak po przetworzeniu makr przed właściwą kompilacją stanie się identyczny z kodem B. Czy moje rozumowanie jest prawidłowe?

Wiesz, że w podejściu naukowym nie kierujemy się intuicją wysuwając twierdzenia? :) Intuicja może służyć jako pierwsze podejście do postawienia jakiejś tezy, jednak wymaga ona później dowodzenia. Gdybyś się tak mocno interesował optymalizacją, poczytałbyś co oznacza owe wywołanie funkcji na poziomie asemblera. Generalnie to będzie parę instrukcji (przygotowanie ramki stosu, odłożenie argumentów) oraz skok pod odpowiedni adres. To, czy jest o co kruszyć kopię zależy od tego co robi funkcja. Jeśli sama funkcja jest długa, skomplikowana, to te parę instrukcji jest poniżej 1% czasu wykonania całości. Moim zdaniem nie ma co wtedy bawić się makra itp. Natomiast jeśli funkcja jest krótka (jak w przypadku Twojego MIN) można się pokusić o coś takiego. Jednak moim zdaniem o wiele bardziej eleganckie jest zastosowanie funkcji inline niż makra. Jedynym wyjątkiem jaki mógłbym dopuścić było by jakieś oprogramowanie embedded z bardzo ograniczoną, powiedzmy do parę KB ilością pamięci. Wtedy każdy zaoszczędzony bajt jest na wagę złota. Dziś wielkości cache procesorów oraz dysków twardych są tak duże, ze nie opłaca się wywalać funkcji ze względu na rozmiar wynikowego kodu.

AK
  • Rejestracja:ponad 6 lat
  • Ostatnio:dzień
  • Postów:3561
0
Mr.YaHooo napisał(a):

Jednak moim zdaniem o wiele bardziej eleganckie jest zastosowanie funkcji inline niż makra. Jedynym wyjątkiem jaki mógłbym dopuścić było by jakieś oprogramowanie embedded z bardzo ograniczoną, powiedzmy do parę KB ilością pamięci. Wtedy każdy zaoszczędzony bajt jest na wagę złota. Dziś wielkości cache procesorów oraz dysków twardych są tak duże, ze nie opłaca się wywalać funkcji ze względu na rozmiar wynikowego kodu.

No to powiem tak: Atmega 8, kod miałem w konwencji C, w AS v4, potem go "plusplusowałem" w AS 7- wbrew intuicji mikroprocesorowych guru *) - kod okazał się mniejszy w sensie bajtów. Oczywiście nie z każdą kompilacją analizowałem powstały kod maszynowy, ale takie pozytywne "kwiatki" jak wykrycie że pole statyczne znikąd nie jest używane itd ... a sekwencje wywołania znacznie krótsze.

Generalnie specyficzne środowisko programistów embedded trzyma się swoich "intuicji" np o makrach, bardziej niż realnych faktów.

*) że przez C++ kury jajek nie niosą, a krowy dają kwaśne mleko


Bo C to najlepszy język, każdy uczeń ci to powie
MY
@AnyKtokolwiek akurat nie mam doświadczenia w takich zabawach, ale brzmi to ciekawie.
AN
  • Rejestracja:prawie 19 lat
  • Ostatnio:około 4 godziny
0

Z tematy wyciągam wniosek taki, że te bardziej znane kompilatory, takie jak GCC lub Visual Studio są tak dopracowane, że są w stanie same przerobić program tak, żeby był bardziej optymalny (jednak nie wiemy, czy jest to optymalizacja wydajności czy wielkości, bo na ogół jedno optymalizuje się kosztem drugiego). W PC to w większości przypadków nie ma różnicy, czy jakiś kod wykonuje się o kilka milisekund dłużej lub krócej, bądź zajmuje więcej czy mniej miejsca.

Co do embedded, to ja jestem przekonany, że wielkość kodu, wydajność i użycie RAM jest ważne, nie wiem, czy kompilatory takie jak AVR Studio czy SDCC też mają takie optymalizacje, pewnie jakieś mają, ale chyba nie tak zaawansowane, jak GCC.

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0

jednak nie wiemy, czy jest to optymalizacja wydajności czy wielkości

Jak najbardziej wiemy i jak najbardziej jest to konfigurowalne (w większości kompilatorów).

nie wiem, czy kompilatory takie jak AVR Studio czy SDCC też mają takie optymalizacje, pewnie jakieś mają, ale chyba nie tak zaawansowane, jak GCC.

Nieustannie zdajesz się poruszać w sferze nie wiem i nie chcę sprawdzić / nie wiem, ale wydaje mi się, jak gdybyś bał się na własne oczy przekonać, że nie masz racji i Twoje makra jedynie niepotrzebnie zaciemniają kod.

Sprawdź, nie zgaduj oraz nie optymalizuj bez potrzeby (http://wiki.c2.com/?PrematureOptimization).


edytowany 3x, ostatnio: Patryk27
AN
  • Rejestracja:prawie 19 lat
  • Ostatnio:około 4 godziny
0

Nieustannie zdajesz się poruszać w sferze nie wiem i nie chcę sprawdzić / nie wiem, ale wydaje mi się, jak gdybyś bał się na własne oczy przekonać, że nie masz racji i Twoje makra jedynie niepotrzebnie zaciemniają kod.

Sprawdź, nie zgaduj oraz nie optymalizuj bez potrzeby (http://wiki.c2.com/?PrematureOptimization).

Tak to może wyglądać, ale jak będę potrzebować, to zajrzę do dokumentacji i wykonam testy, nie jest to teraz dla mnie najwyższy priorytet.

enedil
  • Rejestracja:ponad 11 lat
  • Ostatnio:około godziny
  • Postów:1027
0
andrzejlisek napisał(a):

Nieustannie zdajesz się poruszać w sferze nie wiem i nie chcę sprawdzić / nie wiem, ale wydaje mi się, jak gdybyś bał się na własne oczy przekonać, że nie masz racji i Twoje makra jedynie niepotrzebnie zaciemniają kod.

Sprawdź, nie zgaduj oraz nie optymalizuj bez potrzeby (http://wiki.c2.com/?PrematureOptimization).

Tak to może wyglądać, ale jak będę potrzebować, to zajrzę do dokumentacji i wykonam testy, nie jest to teraz dla mnie najwyższy priorytet.

W takim razie śmieszy nas, że postanowiłeś nas takim pytaniem zadręczać.

elwis
  • Rejestracja:ponad 18 lat
  • Ostatnio:9 dni
1

Jak chcesz zobaczyć jak daleko posunięte są optymalizacje automatyczne, skompiluj bardziej zaawansowany kawałek kodu w GCC z flagami -g -O3 (symbole dla debugowania, najmocniejsza optymalizacja pod kątem prędkości) i podebuguj. Zobaczysz, że potrafi skakać między kilkoma funkcjami jak mu się podoba. Zauważyłem też, na przykład, taki fenomen, że lepiej jest przekazać strukturę przez wartość niż przez wskaźnik. W tej materii intuicja jest słabym doradcą — jak chyba we wszystkim co jest bardzo złożone. Z drugiej strony, oczywiście też nie jest tak, że wszystko jest optymalnie napisane. Wręcz przeciwnie i widać to po tym jak oprogramowanie dościga swoim brakiem wydajności co raz szybszy sprzęt. Przecież nie może być tak, żeby nowy soft chodził na starym komputerze, to by psuło sprzedaż. Hehe… Wiem, trochę przesadzam i upraszczam, ale jednak. W Embedded pewnie takie optymalizowanie ma sens.
Ja też uczyłem się programować jak były większe ograniczenia sprzętowe więc rozumiem twoje podejście i zdaję sobie sprawę z tego, że trochę przesadzam, choć nie wiem czy nowocześniejsi programiści nie przeginają bardziej. To mało istotne.
Co do preprocesora, to ma on zbyt wiele wad. Przede wszystkim utrudnia debugowanie, zaciemnia komunikaty o błędach kompilatora, nie pilnuje typów. Przy bardziej złożonych makrach można sobie napytać niezłej biedy. To, że nie ma go w innych językach nie jest dziwne. To co robi się makrami w C, przeważnie można zrobić znacznie lepiej szablonami (template), polimorfizmem, itd. — programując w językach wyższego poziomu nigdy nie tęsknię za preprocesorem. Żeby było zabawniej to nawet nie jest kwestia czasu czy zasobów, jak mi się kiedyś zdawało, LISP, choć starszy od C ma makra, które rzeczywiście nie mają sobie równych, a przy tym są integralną częścią języka, a nie szybkim hackiem, jak preprocesor.
A i jeszcze jedno, w poważnych projektach używa się preprocesora, bo w C nie da się inaczej, w C++ już praktycznie nie ma potrzeby.


edytowany 2x, ostatnio: elwis
hauleth
Moderator
  • Rejestracja:około 17 lat
  • Ostatnio:11 dni
1

Jeśli chodzi o optymalizacje w kompilatorach, to polecam prezentację Matta Godbolta (tego od Compiler Explorer).


Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)