RISOLTO Strano comportamento semplice codice C++

Stato
Discussione chiusa ad ulteriori risposte.

M1n021

Nuovo Utente
143
68
Ciao a tutti, qualcuno saprebbe spiegarmi perché compilando il seguente codice
C++:
#include <iostream>
#include <vector>

using namespace std;

struct A
{
    bool flag;
    vector<unsigned int> v;
};

int main()
{
    A m[3][3] = {{{false, {2, 8, 5}}, {true,  {}},        {false, {1}}},
                 {{true,  {}       }, {false, {2, 8, 5}}, {false, {11, 18}}},
                 {{true,  {}       }, {true,  {}},        {false, {1, 5, 2, 8, 5}}}};

    for(unsigned int i = 0; i < 3 * 3 && ((*m)[i].flag || (*m)[i].v.size()); ++i)
    {
        cout << i << endl;
    }
}
col flag O3, ottengo il seguente output
Codice:
0
1
2
3
4
5
6
7
8
9
10
11

Process returned 0 (0x0)   execution time : 0.185 s
Press any key to continue.
? 😕

Compilando senza il flag O3 funziona bene, nel senso che giustamente stampa fino al numero 8.
 
  • Mi piace
Reazioni: BrutPitt

bigendian

Utente Attivo
718
410
OS
Linux
banalmente, senza pantomime accademiche:
quando ottimizzi (-O3) il compilatore decide come meglio organizzare lo stack, non puoi assumere che *m sia sequenziale.

Hai 2 modi,
1) usare gli indici, sempre miglior soluzione

Codice:
for (unsigned int q = 0; q < 3; ++q) {
        for (unsigned int i = 0; i < 3 && \
            (m[q][i].flag || m[q][i].v.size()); ++i) {
            cout << "flag " <<  m[q][i].flag << "\n";
            cout << i << endl;
        }
}

2)
inizializzare un puntatore, da cui il compilatore capisce che accederai
sequenzialmente
Codice:
A *p = (A*)m;

for (unsigned int i = 0; i < 3 * 3 && (p[i].flag || p[i].v.size()); ++i) {
    cout << "flag " <<  p[i].flag << "\n";
    cout << i << endl;
}
 
  • Mi piace
Reazioni: DispatchCode e BAT

M1n021

Nuovo Utente
143
68
@bigendian grazie per la risposta, che con i due indici funzionasse già lo sapevo, mentre la soluzione con l'inizializzazione di un puntatore mi è nuova.
Premesso che non sono sicuro di aver capito bene in cosa consiste l'ottimizzazione con O3 in questo caso, mi sembra comunque che il comportamento del compilatore sia alquanto anomalo e forse eccessivamente "presuntuoso"...
Detto ciò, mi sorgono un paio di dubbi:
- questo comportamento vale per tutti i compilatori?
- dal momento che identificare nel programma che sto scrivendo il problema qui brevemente riportato mi ha fatto penare non poco, in quanto fondamentalmente si trattava di qualcosa di abbastanza "inaspettato", mi chiedevo in quali altri frangenti potrei aspettarmi questi "scherzetti" da parte del compilatore?!

Facendo alcune ricerche mi sono imbattuto nella keyword "volatile":
C++:
#include <iostream>
#include <vector>

using namespace std;

struct A
{
    bool flag;
    vector<unsigned int> v;
};

int main()
{
    A m[3][3] = {{{false, {2, 8, 5}}, {true,  {}},        {false, {1}}},
                 {{true,  {}       }, {false, {2, 8, 5}}, {false, {11, 18}}},
                 {{true,  {}       }, {true,  {}},        {false, {1, 5, 2, 8, 5}}}};

    for(volatile unsigned int i = 0; i < 3 * 3 && ((*m)[i].flag || (*m)[i].v.size()); ++i)
    {
        cout << i << endl;
    }
}
Potrebbe la suddetta essere considerata una valida soluzione?



P.S.

Nella seconda soluzione da te prospettata, invece di
Codice:
A *p = (A*)m;
non sarebbe sufficiente scrivere
Codice:
A *p = *m;
??
 

Andretti60

Utente Èlite
6,440
5,091
Il tuo problema è che la tua struttura ha una lunghezza variabile, che è il numero degli elementi del vettore, la dichiari nello stack ma “dove” e come sia poi allocata dipende dal compilatore (e quindi anche dai flag del suddetto). Codice del genere nella mia azienda è un “no, no” categorico e viene richiede di essere riscritto per essere affidabile (tutto il nostro codice di produzione va sotto peer review prima di essere inglobato).
Una possibile soluzione è dichiarare nella struttura solo il puntatore del vettore, in modo da renderla a lunghezza costante, e allocare poi il vettore dinamicamente. Ma la soluzione migliore è di accedere la matrice mediante i suoi propri indici, e non usando puntatori ballerini.
Ed evitare quel condizionale nel ciclo for() che rende il codice assolutamente illeggibile. Per me è un errore anche utilizzare la lunghezza del vettore come se fosse un valore booleano, non lo è, è un intero, e ci si affida al compilatore di considerare qualsiasi valore che non sia zero come “true” (in linguaggi fortemente dichiarativi quella istruzione viene considerata un errore già in compilazione)
 

M1n021

Nuovo Utente
143
68
Il tuo problema è che la tua struttura ha una lunghezza variabile, che è il numero degli elementi del vettore
Scusa, ma non penso sia questo il fattore chiave in questo caso. Infatti sempre compilando con O3 ottengo:

C++:
#include <iostream>

using namespace std;

struct A
{
    bool flag;
    unsigned int v[10];
};

int main()
{
    A m[3][3] = {{{false, {2, 8, 5}}, {true , {}       }, {false, {1}            }},
                 {{true , {}       }, {false, {2, 8, 5}}, {false, {11, 18}       }},
                 {{true , {}       }, {true , {}       }, {false, {1, 5, 2, 8, 5}}}};

    for(unsigned int i = 0; i < 3 * 3 && ((*m)[i].flag || (*m)[i].v[0]); ++i)
    {
        cout << i << endl;
    }
}
Codice:
0
1
2

Process returned 0 (0x0)   execution time : 0.188 s
Press any key to continue.
o anche
C++:
#include <iostream>

using namespace std;

int main()
{
    int m[3][3] = {{1, 2, 3},
                   {4, 5, 6},
                   {7, 8, 9}};

    for(unsigned int i = 0; i < 3 * 3 && (*m)[i]; ++i)
    {
        cout << i << endl;
    }
}
Codice:
0
1
2

Process returned 0 (0x0)   execution time : 0.148 s
Press any key to continue.

Per il resto non metto assolutamente in discussione quanto dici relativamente alle pratiche di buona programmazione e agli standard qualitativi richiesti in ambito lavorativo, dico solo che ogni linguaggio ha uno standard e nel caso specifico mi sembra che il codice da me scritto, per quanto deprecabile, lo rispetti in pieno.


Qualcuno potrebbe chiarirmi anche l'eventuale utilizzo della keyword "volatile"? In particolare, andrebbe applicata alla matrice m o all'indice i?
 

Andretti60

Utente Èlite
6,440
5,091
… dico solo che ogni linguaggio ha uno standard e nel caso specifico mi sembra che il codice da me scritto, per quanto deprecabile, lo rispetti in pieno.
In effetti, no, non lo stai rispettando in quanto dichiari una variabile a due indici (ossia un vettore multidimensionale) ma poi la usi come se fosse un puntatore (non lo è), e usi una sola coppia di parentesi. In quelle condizioni ti metti alla mercé del compilatore, che non mi stupisce ottieni risultati diversi a seconda dei flag di compilazione. Purtroppo il linguaggio C permette questo (e ben altro) e molti mal di testa sono causati proprio da codice come il tuo. E te lo dico per esperienza personale.

Per quanto riguarda la keyword “volatile”, è una brutta bestia e ti suggerisco di aprire una discussione a parte (anche se trovi qualche decente spiegazione in rete), in pratica dice al compilatore di NON ottimizzare il codice che riguarda quella variabile, in quanto può variare anche se sembra non cambi affatto.
 

M1n021

Nuovo Utente
143
68
@Andretti60 con <<rispetto dello standard>> mi riferivo semplicemente al fatto che in C/C++ un array multidimensionale deve occupare un unico blocco contiguo di memoria, e quindi di conseguenza non vedo perché accedere ai suoi elementi mediante l'utilizzo di un puntatore semplice dovrebbe andare contro lo "standard".
Il fatto poi che sia una pratica deprecabile, in quanto di fatto ti mette alla mercé del compilatore, è un'altra questione...
Per curiosità, tutti i compilatori si comportano in questo modo o solo alcuni?
 

BAT

Moderatore
Staff Forum
Utente Èlite
22,662
11,445
CPU
1-Neurone
Dissipatore
Ventaglio
RAM
Scarsa
Net
Segnali di fumo
OS
Windows 10000 BUG
Per curiosità, tutti i compilatori si comportano in questo modo o solo alcuni?
credo tutti
in ogni caso il codice va scritto in modo che sia indipendente dal compilatore a prescindere dal grado di ottimizzazione, meglio programmare "pulito"
 

bigendian

Utente Attivo
718
410
OS
Linux
"volatile" non serve come l'hai utilizzato, la variabile i comunque incrementa. E spesso volatile e' utilizzato a sproposito. Si usa principalmente per letture e scritture su indirizzi I/O, perche' il contenuto della memoria I/O varia senza che il programma la modifichi, quindi "volatile" informa il compilatore che quell'indirzzo va ri-letto sempre.
 

DispatchCode

Moderatore
Staff Forum
Utente Èlite
2,208
1,845
CPU
Intel I9-10900KF 3.75GHz 10x 125W
Dissipatore
Gigabyte Aorus Waterforce X360 ARGB
Scheda Madre
Asus 1200 TUF Z590-Plus Gaming ATX DDR4
HDD
1TB NVMe PCI 3.0 x4, 1TB 7200rpm 64MB SATA3
RAM
DDR4 32GB 3600MHz CL18 ARGB
GPU
Nvidia RTX 3080 10GB DDR6
Audio
Integrata 7.1 HD audio
Monitor
LG 34GN850
PSU
Gigabyte P850PM
Case
Phanteks Enthoo Evolv X ARGB
Periferiche
MSI Vigor GK30, mouse Logitech
Net
FTTH Aruba, 1Gb (effettivi: ~950Mb / ~480Mb)
OS
Windows 10 64bit / OpenSUSE Tumbleweed
Concordo con quanto già detto dagli altri, aggiungo solo una cosa riguardo ai tuoi codici (il primo e quello con volatile).

Premesso che non sono sicuro di aver capito bene in cosa consiste l'ottimizzazione con O3 in questo caso, mi sembra comunque che il comportamento del compilatore sia alquanto anomalo e forse eccessivamente "presuntuoso"...

Guardando da un disassembler, il tuo codice senza O3 si presenta così:

senza_o3.png

la locazione [rbp+1C0h+var_44] è la variabile "i". Come noti viene inizializzata a 0, viene incrementata (ADD...) e successivamente (ultime 2 istruzioni) viene fatto il confronto con il valore 8 ("se è maggiore di 8, salta").

Quando compili con O3, per le ragioni già esposte sopra dagli altri, il compilatore decide che al tuo codice in fondo... il confronto che fai con la variabile i, non serve, quindi lo toglie:

senza_volatile.png

qui la variabile è rappresentata dal registro r12d: viene inizializzato a 0 ad inizio screenshot (xor r12d, r12d) e viene incrementato l'ultima volta che lo si vede evidenziato. Il check è stato rimosso.

Puoi verificare che il problema è questo rimuovendo dal tuo codice il confronto "i < 3*3", vedrai che otterrai il medesimo output errato, proprio perchè che nel codice sia presente o meno, il compilatore non lo emetterà comunque.

Quando utilizzi volatile:

volatile_o3.png

qui la variabile "i" è rappresentata da "var_168". Anche qui, inizializzata a 0 ad inizio screenshot, assegnata al registro EAX, si vede che viene "comparata" con il valore 8 un paio di volte.



Ho visto usare volatile anche in un altro contesto. Si trattava di codice compilato che doveva essere poi successivamente "preso in pasto" da un altro programma per effettuare alcune elaborazioni sull'eseguibile, tipo spostare una funzione con lo scopo di emettere del codice per poi virtualizzarlo.
Vista la difficoltà nel determinare la lunghezza in bytes di una funzione, ho visto l'utilizzo di volatile per effettuare un check di fatto inutile, così da forzare poi l'inserimento di alcuni bytes raw direttamente nel codice macchina; una sorta di pattern insomma, che poi il programma che avrebbe preso in pasto l'eseguibile avrebbe cercato per determinare la fine della funzione (uno di questi articoli l'avevo linkato all'interno di una mia guida/articolo).
 

M1n021

Nuovo Utente
143
68
credo tutti
in ogni caso il codice va scritto in modo che sia indipendente dal compilatore a prescindere dal grado di ottimizzazione, meglio programmare "pulito"
Sono d'accordo, ma il punto è chi se lo aspettava che scorrere un array multidimensionale in quel modo mi avrebbe messo alla mercè del compilatore?!


"volatile" non serve come l'hai utilizzato, la variabile i comunque incrementa. E spesso volatile e' utilizzato a sproposito.
Nel senso che l'ho utilizzata male (a tal proposito in un precedente post ho chiesto se andrebbe applicata alla matrice m o all'indice i) o che in questo contesto non serve proprio a nulla?
Non capisco, per esempio in questo caso
C++:
#include <iostream>

using namespace std;

int main()
{
    int m[3][3] = {{1, 2, 3},
                   {4, 5, 6},
                   {7, 8, 9}};

    for(volatile unsigned int i = 0; i < 3 * 3 && (*m)[i]; ++i)
    {
        cout << i << endl;
    }
}
non funziona e i si ferma a 2.


@DispatchCode ti ringrazio per avermi chiarito un po' la questione. Ancora una volta mi hai dimostrato che se avessi un po' di dimestichezza con l'assembly potrei approfondire queste questioni con maggiore autonomia! 😅
In ogni caso devo ammettere che ancora non ho capito fino in fondo la ratio che si cela dietro le "ottimizzazioni" decise dal compilatore... per esempio nell'esempio postato in questo messaggio, dal momento che stampa fino a 2, non capisco quale sia la condizione che determini l'uscita dal ciclo... l'unica ipotesi che riesco ad avanzare è che la condizione i<9 sia stata tramutata in i<3, col valore 3 scelto perché in qualche modo rappresenta la "dimensione" di (*m)?! Non saprei...
 
Ultima modifica da un moderatore:

bigendian

Utente Attivo
718
410
OS
Linux
Certo che l'array che utilizzi e' un array complesso di oggetti che includono altri oggetti, e i vector utilizzano a loro volta il new(). Su queste cose a scanso d'equivoci per esperienza uso gli indici 2d, i , q.

Disassemblato di @DispatchCode aiuta certo a capire meglio. Ben fatta, io non ne avevo voglia.

Quindi la vera domanda e', il compilatore elimina il controllo di i, e considera che finito l'array ci sia un ulteriore elemento a 0. Perche ?
Cmq, aggiungendo un ulteriore elemento a zero, il tutto dovrebbe funzionare anche con -O3, cosi:

Codice:
    A m[4][3] = {{{false, {2, 8, 5}}, {true,  {}},        {false, {1}}},
                 {{true,  {}       }, {false, {2, 8, 5}}, {false, {11, 18}}},
                 {{true,  {}       }, {true,  {}},        {false, {1, 5, 2, 8, 5}}}};


Sconsiglio di utilizzare volatile se non ben chiaro il suo scopo, per altro, nella stragrande maggioranza dei casi e' inutile perche' il compilatore e' in grado di capire comunque se una variabile ha bisogno di essere considerata.
Dove lo trovi spesso, ma non unico utilizzo e' appuntio nelle macro ioread, in ogni archietettura. Si fa un cast a volatile per
fare leggere sempre e comunque una locazione di memoria I/O (ad esemlio un registro, che puo variare il suo contenuto spontaneamente, come un flag di stato. Tipo
Codice:
unsigned long ioread_l(volatile unsigned long *addr);
 
  • Mi piace
Reazioni: DispatchCode

BrutPitt

Utente Attivo
1,166
1,262
qui la variabile è rappresentata dal registro r12d: viene inizializzato a 0 ad inizio screenshot (xor r12d, r12d) e viene incrementato l'ultima volta che lo si vede evidenziato. Il check è stato rimosso.
Concordo sul discorso delle ottimizzazioni della struttura e sulla leggibilita' de codice... ma con che criterio il compilatore decide di rimuovere il check sulla i?


Insomma quel ciclo for deve terminare a 8 indipendentemente dalle comparazioni che si fanno dopo: c'e' un &&, quindi quando i == 9 quel test e' FALSO comunque, indipendentemente dal valore della struttura o di come la utilizzo (e questo non deve importare al compilatore).

Ed il compilatore ignora la comparazione anche se la variabile non viene utilizzata nell'indice, tipo questo codice:

C++:
for(unsigned int i = 0, p = 0; p < 3 * 3 && ((*m)[i].flag || (*m)[i].v.size()); ++i, p++)

// o anche nel caso utilizzassi gli indici
for( unsigned int i = 0, p = 0; (p < 9) && (m[i/3][i%3].flag || m[i/3][i%3].v.size()); ++i, p++)

Non credo debba essere io a dover dichiarare quella variabile "volatile", pensando che il compilatore possa interpretare male l'ottimizzazione.

P.S.
Risulta corretto in CLang 15.0 e 14.x ... ed anche in MSVC 19.x (/O2) e ICC 2021.X
 
Ultima modifica:

M1n021

Nuovo Utente
143
68
Non credo debba essere io a dover dichiarare quella variabile "volatile", pensando che il compilatore possa interpretare male l'ottimizzazione.

P.S.
Risulta corretto in CLang 15.0 e 14.x ... ed anche in MSVC 19.x (/O2) e ICC 2021.X
Quindi alla fine è corretto dire si tratta di un "bug", o comunque di un compilatore un po' troppo "intraprendente"? 😅

Non so se hai visto il semplice codice che ho postato nel precedente messaggio:
C++:
#include <iostream>

using namespace std;

int main()
{
    int m[3][3] = {{1, 2, 3},
                   {4, 5, 6},
                   {7, 8, 9}};

    for(unsigned int i = 0; i < 3 * 3 && (*m)[i]; ++i)
    {
        cout << i << endl;
    }
}
Senza la keyword "volatile" o con la keyword "volatile" applicata al''indice i del ciclo for, il programma stampa fino a 2, mentre se applico la keyword "volatile" alla matrice m stampa giustamente fino a 8.
Per quel che può valere anche io nel mio piccolo penso che non sia tanto normale che chi scrive il codice debba farsi carico dell'arbitrarietà del compilatore...


P.S.
Leggo che il mio precedente messaggio è stato modificato da un moderatore, avevo forse scritto qualcosa di sbagliato?
 
Ultima modifica da un moderatore:
  • Mi piace
Reazioni: BrutPitt

bigendian

Utente Attivo
718
410
OS
Linux
Concordo con @BrutPitt . Questo thread era interessante, andando a fondo si impara sempre.
Inutile impazzirci troppo su, sono assunzioni di g++ sul controllo del loop, che sembrano piu un bug che altro, che ce ne sono, potresti aggiungerlo a bugzilla cosi vediamo cosa dicono.

A proposito, bello questo

screenshot_202211211669039934.png
 
  • Mi piace
Reazioni: BrutPitt
Stato
Discussione chiusa ad ulteriori risposte.

Entra

oppure Accedi utilizzando
Discord Ufficiale Entra ora!