[Megoldva] Kioptimalizálódhat-e a függvényhívás?

Fórumok

Nézzük az alábbit:

#define INTERRUPT_test_and_run(PIE, PIR, MASK, FNC) (((PIE) & (PIR) & (MASK)) ?\
                                                        (FNC, true) : false)

void __interrupt() INTERRUPT_InterruptManager(void) {

    bool ready;

    ready = false;
    ready |= INTERRUPT_test_and_run(PIE3, PIR3, _PIR3_TMR0IF_MASK, tmr0_isr());
    ready |= INTERRUPT_test_and_run(PIE4, PIR4, _PIR4_U1RXIF_MASK, rx_isr());
    ready |= INTERRUPT_test_and_run(PIE4, PIR4, _PIR4_U1TXIF_MASK, tx_isr());
    if (!ready) RESET();                                                        // unhandled interrupt
}

Gondolhatja-e a fordító, hogy a RESET() hívásához szükséges ready változó helyes előállításához szükségtelen a makróban hivatkozott FNC meghívása? Mert ha ezt gondolja, akkor épp az interrupt handlert fogja kispórolni, és sovány vigasz lesz annak a néhány byte-nak a megspórolása. :) Veszélyes-e ebből a szempontból ez a makró?

Szerk.: Nem azt állítom, hogy most rosszul fordítja - még nem próbáltam ki -, hanem az a kérdés, hogy vajon ez egy annyira ügyetlen makró, amitől állandóan ott a fejem felett a pallos, hogy egyszer csak kispórolja a fordító az IT handler hívását, vagy ez így teljesen jó, netán teljesen rossz, vagy picit módosítani kellene rajta?

Megjegyzés:

Annak, aki esetleg érdekesnek gondolja, úgy tűnik, az eredeti makró - itt fentebb - hibás, s a fordító nem érzi szükségesnek a vessző operátor előtti függvény hívását, hiszen a kifejezés anélkül is kiértékelhető. Akkor viszont nem hívódik az interrupt handler.

Hozzászólások

Azt szerintem ki kell számítania, mert különben nem tudná a végén, hogy reset legyen, vagy sem. Itt az a bajom, hogy az (FNC, true) az az FNC hívása nélkül is true, tehát FNC hívása elhagyható a kifejezés eredményének szempontjából, viszont épp az FNC a katarzis ebben a dologban. :)

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Igen, vessző operátor. Ezt láttam oda a legalkalmasabbnak. Balról jobbra, és a leggyengébben „köt”.

Szerk.: Csak ugye FNC nélkül is tudható, hogy (FNC, true) az true, tehát nem kell FNC-t hívni. Szóval sajnos ez így nem jó, már látom.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Attól tekintsünk el, hogy a ready változó neve kifejezőbb volna, ha átnevezném done-ra. :)

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Részben megvan a válasz:

warning: (759) expression generates no code

Szomorú vagyok, át kell alakítani. Az előbb nem vettem észre, csak most. :(

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Ezt ki kellene vizsgálni, hogy pontosan mire mondja és miért.

Hacsak az nem, hogy a INTERRUPT_test_and_run első három paramétere fordítási időben ismert, ezért tudja a fordító, hogy a FNC mindig meghívódik, és a ready már háromszorosan nemnulla a függvény végére, tehát a RESET hívására sosincs szükség.

Ebben az a esetben viszont az a kérdés, hogy mi másnak kellene történnie?

Szerk: a vesszőoperátornak nem az a szemantikája, hogy 'hajts végre a vessző utáni részt', hanem az, hogy 'hajtsd végre a vessző előtti részt (mellékhatásokkal együtt), azután hajtsd végre a vessző utáni részt, a kimenet ennek a második résznek az eredménye'.

Szerk2: most talán jobban értem, hogy kifejezetten a vesszőoperátorra vonatkozott a kérdés. Szerencsére az előző kiegészítésben ezt is leírtam; most csak annyit tennék hozzá, hogy a függvényhívásnál a paramétereket elválasztó vessző nem vesszőoperátor, vagyis a paraméterek kiértékelése tetszőleges sorrendben történhet, akár futásonként változhat (tehát ne építsünk arra, hogy pl. az előző futásnál balról-jobbra volt).

Nem tudhatja. Ha engedélyezve van egy negyedik periféria, amelyik IT-t kér, akkor egyik maszkra sem lesz illeszkedés, és a végén a RESET()-nek kell futnia. Ha kiegyszerűsíthető lenne fordítási időben, nem írtam volna bele. Jó, tudom, rezeg a léc, mert alant NULL-ra vizsgálok, amikor sohasem hívom a függvényt NULL-al, de szerintem az azért úgy szép. :)

Igen, ez fontos kérdés. Tehát a vessző operátor esetén mindenképpen végre lesz hajtva a vessző mindkét oldalán található kifejezés? Akkor tehát mégis majdnem jó volt az eredeti makróm?

Igen, azt tudom, hogy függvényhívás esetén nem csinálhatom például ezt: fnc(x, x++); mert ennek csúnya vége lehet.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

> Ha engedélyezve van egy negyedik periféria, amelyik IT-t kér, akkor egyik maszkra sem lesz illeszkedés, és a végén a RESET()-nek kell futnia.

Off: Hát ezért lenne jó, ha az emberi olvasó meg tudná tippelné, hogy  a PIE, PIR, MASK szimbolumok melyike #define-olt konstants, és melyike változó.

> Tehát a vessző operátor esetén mindenképpen végre lesz hajtva a vessző mindkét oldalán található kifejezés? Akkor tehát mégis majdnem jó volt az eredeti makróm?

Most zavarba jöttem, milyen lenne a "nem mindenképpen"? Mitől függene, hogy megcsinálja-e? (Jó, ha olyan speciális eset, hogy látja a fordító, hogy nincs mellékhatása, akkor lehet belőle egy 'useless calcluation' warning, de a függvényhívás az nem ilyen [bár ha a függvény is ugyanabban a source-ben van, és mondjuk üres a törzse, akkor akár ez is mehet].)

A PIE az a regiszter abszolút címmel, amelyiknek az egyes bitjei egyes perifériák IT kérését engedélyezik. Azért kell makró, mert van egy rakás ilyen regiszter, hiszen 8 bites az MCU, de több, mint 8 IT-t kérő periféria van. A PIR az a regiszter, amelyben 1-be billen a PIE-vel azonos helyiértéken lévő bit, ha teljesül az IT kérés feltétele, például soros porton beérkezett egy karakter. Ez akkor is bebillen 1-be, ha amúgy a PIE által tiltva van az IT kérés, csak ez utóbbi esetben nem keletkezik megszakítás. Azért kell mégis összemaszkolni PIE-t és PIR-t, mert olyan is lehet, hogy egy másik periféria kér IT-t, amiatt megyünk bele ebbe az IT rutinba - itt csak egyetlen IT vektor van -, és ha PIE tiltottsága ellenére csak PIR-t vizsgálnánk, tévesen kiszolgálnánk az amúgy tiltott IT-jű perifériát.

A MASK pedig kiszedi azt a bitet, amelyik perifériához tartozik a PIE és PIR a nyolcból.

Zavarosan fogalmaztam. Olyasmire gondoltam, mint például az || vagy && operátorok esetében, hogy amikor már lehet tudni az eredményt, végre sem hajtódik a további része ezzel időt spórolva, de ezt például pointereknél vaskosan ki is használjuk:

char c, *p;

while (p && *p && *p != c) p++;

Tehát akkor a vessző operátor esetében mindkét oldal mindenképpen kiértékelésre, végrehajtásra kerül, ha jól értem.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Várj, az más téma.

Az az egyik kérdés, hogy a sizeof kiértékeli-e az operandusát, vagy sem.

Az egy másik kérdés, hogy milyen környezetben konvertálódik a tömb típusú kifejezés a tömb első elemére mutató mutatóvá.

C99 6.3.2.1 Lvalues, arrays, and function designators, p3:

Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

A VLA egy nagy... megkönnyítés, igen, azoknak a kedvéért, akiknek az alloca bonyolult.

#include <stdio.h>

int main(int argc, char **argv) {
  int n= argc+3;
  int a[n];
  (void)argv;
  printf ("sizeof=%d\n", (int)(sizeof a));
  return 0;
}

Itt a sizeof futásidőben kérdez le valamit, természetesen nem az a elemeit, hanem az a előtt lévő nem publikus metainformációt.

Az alloca() nem szabványos (sem POSIX, sem ISO C; most legalábbis C11-ig próbáltam megnézni).

Én mondjuk semmilyen formában nem támogatom külső adattól függő méretű tömb foglalását "auto" tárolási osztályban (= vermen). Egyik írásmóddal (alloca vagy VLA) sem definiált, mi történik akkor, ha nincs elég hely.

C11-től kezdve egyébként opcionális a VLA támogatás; C11 6.10.8.3 Conditional feature macros:

The following macro names are conditionally defined by the implementation:

[...]

__STDC_NO_VLA__ The integer constant 1, intended to indicate that the implementation does not support variable length arrays or variably modified types.

a vesszőoperátornak [...] az, hogy

Érdemes idézni a szabványból:

The left operand of a comma operator is evaluated as a void expression; there is a sequence point after its evaluation. Then the right operand is evaluated; the result has its type and value. [...]

Valamint

a függvényhívásnál a paramétereket elválasztó vessző nem vesszőoperátor

itt is (mivel ez példaként szerepel, ezért nem normatív, hanem informatív):

As indicated by the syntax, the comma operator (as described in this subclause) cannot appear in contexts where a comma is used to separate items in a list (such as arguments to functions or lists of initializers). On the other hand, it can be used within a parenthesized expression or within the second expression of a conditional operator in such contexts.

Ebben az a szép, hogy a szintaxis (a nyelv formális nyelvtana) diktálja, hogy a "," karakter mikor mi. A fenti paragrafus olyan függvényhívást mutat példának, hogy

f(a, (t=3, t+2), c)

azzal a kommentárral, hogy

the function has three arguments, the second of which has the value 5.

De ami igazán szép szerintem, az a második megjegyzés: within the second expression of a conditional operator. (Kiemelés tőlem.) Saját példáim:

int a[] = { 1 , 2 ? 3 : 4 };
int b[] = { 1 ? 2 , 3 : 4 };
int c[] = { 1 ? 2 : 3 , 4 };

Az "a"-nak az elemei: { 1, 3 }.

A "b"-nek az elemei: { 3 }.

A "c"-nek az elemei: { 2, 4 }.

A "b"-nél a nyelvtan alapján a vessző nem tudja "félbevágni" a feltételes operátor második operandusát, az "a"-ban lévő vesszőnél viszont a feltételes operátort még el sem kezdtük, a "c-"ben lévő vesszőnél pedig már befejeztük.

A "b"-nek a szintaxisfája:

                                                   conditional-expression
                                                            |
        ----------------------------------------------------+----------------------------------------
        logical-OR-expression          ?                  expression       :   conditional-expression
                |                                            |                        |
         logical-AND-expression    --------------------------+---------------  logical-OR-expression
                |                  expression     ,     assignment-expression         |
        inclusive-OR-expression        |                      |                      ...
                |                assignment-expression       ...                      |
         exclusive-OR-expression       |                      |                       4
                |                conditional-expression       3
           AND-expression              |
                |                logical-OR-expression
          equality-expression          |
                |                     ...
        relational-expression          |
                |                      2
           shift-expression
                |
         additive-expression
                |
       multiplicative-expression
                |
          cast-expression
                |
         unary-expression
                |
         postfix-expression
                |
        primary-expression
                |
             constant
                |
          integer-constant
                |
----------------+-------------------
decimal-constant integer-suffix(opt)
       |                 |
   nonzero-digit         []
       |
       1

Kézzel, NEdit-ben. Egyik ablakban a fát rajzoltam / írtam, a másikban nyitva volt a C99 szabvány. (Az egyes operátoroknál tárgyalja a helyettesítési szabályokat, de a végén az Annex A (informative) Language syntax summary tartalmaz összefoglalót is, ld. A.2.1 Expressions.) NEdit-ben nagyon jól lehet diagramokat rajzolni; fejlett támogatása van téglalap kijelölésére (lehet egérrel kijelölni, mozgatni, kivágni); van "fill selection with char" makrója (pl. ezzel lehet nagyon jól függőleges vonalakat húzni). Az Emacs nyilván többet tud, de én kb. 25 éve NEdit felhasználó vagyok.

Most csinaltam egy ilyesmit:

$ cat x.c
#include <stdio.h>

static int funct(int x)
{
 printf("x=%d\n",x);
 return(0);
}

int main(void)
{
 int    a;
 a=(funct(8),6*7);
 printf("a=%d\n",a);
 return(0);
}

$ gcc -Wall -pedantic -ansi -O3 -o x x.c
$ ./x
x=8
a=42
$ gcc --version
gcc (Debian 12.2.0-14) 12.2.0
$ 

Ezalapjan olyba' tunik hogy kiertekeli, akkor is hogyha nem hasznalja fel az eredmenyt. 

Köszönöm!

Akkor lehet, visszatérek a makrós megoldáshoz, bár a static inline függvény is majdnem makró, csak nem a preprocesszor helyettesít még szövegesen, hanem a fordító teszi az adott függvény törzsét a hívó helyre, ami kb. ugyanazt eredményezi szerencsés esetben.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Egyre kevésbé értem. Ha az a ready volatile, akkor nem ad a fordító warningot azzal, hogy a kifejezésből nem fordult kód, viszont byte-ra pontosan ugyanolyan hosszúságú kód fordul. Másfelől, ha nem is hívja FNC-t, a kifejezést szerintem nem spórolhatja le, ellenben honnan tudhatná, hogy a végén kell-e a RESET() hívása, vagy sem? Azt elhiszem, hogy FNC hívást kispórolja, de a kifejezést muszáj lenne kiértékelnie.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Én inkább azt szoktam bizonytalanság esetén, hogy megkerülöm a problémát, és egy robusztus, mindenképp működő alakban írom le. Szerintem az inline függvényes megoldásomat nem tudja elszúrni, mert akkor már tényleg nem azt csinálja, ami oda van írva.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Nem úgy van, hogy 0-ás bittől lefoglal annyit, amennyit kérsz, a következő field-del folytatja, és így tovább? Gondolom, ha a szószélességnél - pl. 8 vagy 32 bit - több esetén folytatja a következő 8 vagy 32 bites szavon, illetve szintén gondolom, hogy egy field belsejébe nem esik szóhatár.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Azért ha egy unsigned counter: 5; átlóg az architektúra bitszélességének határn, majd csinálsz egy var.counter++; utasítást, akkor abba megbolondul a fél világ, akkora kóddá fog fordulni. De optimális esetben is kivadássza a két címről a megfelelő biteket megfelelően összeillesztve, növeli, majd az eredmény megfelelő töredékeit az eredeti helyre pl. egy xor, and, xor-ral visszateszi. Ja, persze két helyre a két töredéket, előtte shiftelések is vannak.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

>,Marmint oke hogy a C standard megengedo, de akkor ugye elegge oda a hordozhatosag.

A micsoda van oda? A bitfieldek soha a fájó életben nem voltak hordozhatóak. Ja meg semmi más sem, ami nem a méretével osztható címen van (padding), vagy ami egynél több bájtból áll (endianness).

Imigy?

#define INTERRUPT_test_and_run(ready,C1,C2,C3,funct) \
    do                          \
     {  if ( (C1)&(C2)&(C3) )   \
         {   funct();           \
             ready |= true;     \
         }                      \
     } while(0)

void __interrupt() INTERRUPT_InterruptManager(void) 
{
 bool ready;

 ready = false;
 INTERRUPT_test_and_run(ready, PIE3, PIR3, _PIR3_TMR0IF_MASK, tmr0_isr);
 INTERRUPT_test_and_run(ready, PIE4, PIR4, _PIR4_U1RXIF_MASK, rx_isr);
 INTERRUPT_test_and_run(ready, PIE4, PIR4, _PIR4_U1TXIF_MASK, tx_isr);
 if (!ready) RESET();        // unhandled interrupt
}

Szerinted a (*p).member működne a p->member helyett?

$ cat x.c
#include <stdio.h>

struct obj
 {      int memb;
 } ;

int funct(struct obj *o)
{
 printf("x=%d\n",(*o).memb);
 return(0);
}

int main(void)
{
 struct obj someobject;
 (&someobject)->memb=6*7;
 funct(&someobject);
 return(0);
}

$ gcc -Wall -pedantic -ansi -O3 -o x x.c
$ ./x
x=42
$

Az mas kerdes hogy forditva csinalom es a obj.memb-ektol szabadulok es objptr->memb-eket hasznalok ahol csak lehet (azaz kb mindenhol :]).

Arra utaltam, hogy noha kényelmes, jól olvasható a struktúrára mutató pointer tagja -> jelölés, lényegében redundáns. Bár, amikor a struktúra tag is pointer, ami mutat egy tagra, akkor nem volna túl jó ez:

(*(*p).ptr).member

Lényegesen jobban olvasható a p->ptr->member
 

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Elengedtem a makrót, végül ezt csináltam:

typedef void (*isr_t)(void);

static inline bool interrupt_test_and_run(uint8_t pie, uint8_t pir, uint8_t mask, isr_t isr) {

    bool ret;

    ret = isr && (pie & pir & mask);
    if (ret) isr();
    return ret;
}

void __interrupt() INTERRUPT_InterruptManager(void) {

    bool done;

    done = false;
    done |= interrupt_test_and_run(PIE3, PIR3, _PIR3_TMR0IF_MASK, tmr0_isr);
    done |= interrupt_test_and_run(PIE4, PIR4, _PIR4_U1RXIF_MASK, rx_isr);
    done |= interrupt_test_and_run(PIE4, PIR4, _PIR4_U1TXIF_MASK, tx_isr);
    if (!done) RESET();                                                         // unhandled interrupt
}

Kérdés, hogy a függvénypointer átadásánál rájön-e arra, hogy ez fordítási időben eldől, mert már akkor tudható a cím, és közvetlenül egy hívássá fordítható mindenféle futásidejű címszámítás nélkül.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Igen, néha szoktam beleírni efféle védelmeket. NULL ellen van. :) Már csak azért is, mert sok függvényemet írom meg úgy, hogy ha kérek tőle valamit, akkor átadom a változó címét, ha nem, akkor NULL-t adok meg neki, persze ettől nem borul fel:

int8_t valami(uint16_t *var) {

if (var) *var = kifejezés;
return 0;
}

uint16_t result;

valami(NULL);

vagy

valami(&result);
 

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE

Szerkesztve: 2024. 12. 05., cs – 00:45

Újabb élmények. Megnéztem, mit fordít az xc8, de buta, pedig -O2. A static inline függvényt az inline ellenére nem mindannyiszor helyettesíti, hanem CALL-lal hívja, minekutána valóban a stack-en keresztül megvalósította a függvénypointeres hívást. Az egész felhajtás 212 byte-tal lett hosszabb - ha jól emlékszem -, mint a statikus megoldásnál, amikor is makróval a preprocessor helyettesít a fordítás előtt.

Szóval visszatértem a makrós megoldáshoz. Nem a 212 byte fáj igazán, hanem az interrupt rutin hatalmas overhead-je.

tr '[:lower:]' '[:upper:]' <<<locsemege
LOCSEMEGE