Visualizzazione dei risultati da 1 a 6 su 6
  1. #1
    Utente bannato
    Registrato dal
    Apr 2012
    Messaggi
    510

    [C++]Non ho capito la strict aliasing rule

    Se ho una cosa del genere:

    codice:
    char buffer[N];
    int* ptr=(int*)buffer;
    Tenendo conto della dimensione che ha un intero sulla mia macchina rispetto a quella dei char, posso tranquillamente iterare su ptr e assegnare dei valori, purché non vada fuori dall' array.
    Ora però vengo a sapere che secondo la string aliasing rule non si può fare.Però non ho capito bene il motivo.Dice che in questo modo il compilatore non si deve preoccupare di controllare che un' area di memoria non sia stata cambiata da qualche puntatore.Però ice che l' alternativa è usare le unioni, ma se ho un' unione come questa:

    codice:
    typedef union
    {
        char buffer[N];
        int ptr[N/sizeof(int)*sizeof(char)];
    }myunion;
    Anche in questo caso potrei cambiare la stessa area di memoria attraverso più membri: .buffer e .ptr .
    Inoltre tutto questo farebbe inutile l' uso dei puntatori, spero veramente che sia stato io ad avere capito male la regola, perché altrimenti non avrebbe più senso usare un puntatore, li ho sempre usati così.

  2. #2

    Re: [C++]Non ho capito la strict aliasing rule

    Originariamente inviato da Who am I
    Dice che in questo modo il compilatore non si deve preoccupare di controllare che un' area di memoria non sia stata cambiata da qualche puntatore.
    Il motivo è esattamente questo. In generale, il fatto che il C consenta di usare in maniera libera e massiccia i puntatori complica il lavoro di ottimizzazione: infatti se io ho un codice di questo genere:
    codice:
    int funzione(int * ptr1, int * ptr2)
    {
        int a = 20;
        *ptr2=15;
        a+=*ptr1;
        ... tante altre operazioni con ptr1 e ptr2 ...
        return a;
    }
    nel caso in cui ptr1 e ptr2 puntano ad elementi diversi il compilatore potrebbe semplicemente piazzare *ptr1 e *ptr2 in due registri diversi e ottimizzare così buona parte delle operazioni che seguono, copiando dai registri in memoria i valori "definitivi" di *ptr1 e *ptr2 giusto alla fine della funzione.
    Il problema si ha quando ptr2 e ptr1 puntano alla stessa cosa (si ha cioè aliasing): in tal caso piazzare *ptr1 e *ptr2 in due registri e trattarli come due oggetti separati produce risultati sbagliati:
    codice:
    *ptr1=*ptr2=50;
    *ptr1+=50;
    *ptr2+=50;
    se *ptr1 e *ptr2 puntano alla stessa roba il risultato finale sarà 150, ma se il compilatore assume che puntino a due oggetti distinti e fa un'ottimizzazione del tipo spiegato sopra si otterrà che la variabile a cui puntano sia ptr1 che ptr2 di fatto conterrà 100, risultato ovviamente sbagliato.

    Per questo motivo di fatto prima che venga riletto *ptr1 (o *ptr2) anche l'altro deve essere scritto in memoria, per assicurarsi che il codice funzioni correttamente anche in caso di aliasing.
    Questo ovviamente in certi casi dà delle penalizzazioni sensibili in termini di performance, dato che continuare a scrivere e leggere in memoria è molto più lento che usare semplicemente due registri - e tutto per coprire il caso (che potrebbe essere estremamente raro o addirittura non accadere mai) in cui ptr1 e ptr2 puntano alla stessa roba.

    Per ovviare a questo "problema" si possono dare al compilatore delle garanzie su dove non ci può essere aliasing, in modo che possa ottimizzare al meglio:
    • praticamente ogni compilatore ha una qualche genere di estensione del linguaggio per marcare un parametro come "senza aliasing" (garanzia che si può fornire ad esempio controllando da una funzione esterna che i due parametri siano diversi e poi richiamare la funzione vera e propria, gestendo separatamente il caso con aliasing);
    • credo che buona parte degli optimizer attuali siano sensibili a check del tipo if(ptr1==ptr2) return; o roba del genere, per cui sanno che da lì in poi su ptr1 e ptr2 sicuramente non hanno aliasing;
    • ma a monte di tutti questi dettagli implementativi, lo standard fornisce una garanzia fondamentale ai compilatori perché possano in generale semplificarsi la vita, ovvero la strict aliasing rule: se due puntatori sono di tipi diversi, allora non è consentito che puntino alla stessa roba (con un'eccezione per char * e unsigned char *, dato che sono spesso usati come buffer per storage/serializzazione/trasporto & co.) (in realtà le regole sono al solito più puntigliose e coprono diversi corner case, vedi [C++11, basic.lval, ¶10] per maggiori dettagli; nota che in C è leggermente diversa, si parla di "tipi compatibili");

    Però ice che l' alternativa è usare le unioni, ma se ho un' unione come questa:
    L'alternativa in teoria non è usare le unioni; su praticamente ogni compilatore questo uso delle union è consentito, ma per lo standard è undefined behavior, poiché è consentito solo leggere dall'ultimo membro in cui si è scritto (le union in effetti nascono per poter avere qualche genere di polimorfismo in C, non per "reinterpretare" tipi in altra maniera).

    Sostanzialmente l'unico metodo consentito dallo standard per "reinterpretare" un tipo come un altro senza avere aliasing è passare per i char *, e quindi di fatto si passa per la memcpy:
    codice:
    #if sizeof(int)!=sizeof(float)
    #error Panico e morte!
    #endif
    float f=0.5;
    int i;
    memcpy((char *)&i, (char *)&f, sizeof(i));
    Per il tuo caso specifico di uso di un buffer di char come "backing storage" per un array di int, la questione dal punto di vista "legalese" è curiosamente pelosa, perché è vero che c'è un'eccezione alla strict aliasing rule per accedere tramite un char * ad altra roba, ma non dovrebbe valere l'opposto; per essere veramente in linea con le regole credo che piuttosto dovrebbe essere:
    codice:
    int ptr[N/4];
    char * buffer= (char *)&ptr;
    che dovrebbe dare sostanzialmente lo stesso effetto senza violare la strict aliasing rule.
    Amaro C++, il gusto pieno dell'undefined behavior.

  3. #3
    Utente bannato
    Registrato dal
    Apr 2012
    Messaggi
    510

    Re: Re: [C++]Non ho capito la strict aliasing rule

    Originariamente inviato da MItaly
    Per il tuo caso specifico di uso di un buffer di char come "backing storage" per un array di int, la questione dal punto di vista "legalese" è curiosamente pelosa, perché è vero che c'è un'eccezione alla strict aliasing rule per accedere tramite un char * ad altra roba, ma non dovrebbe valere l'opposto; per essere veramente in linea con le regole credo che piuttosto dovrebbe essere:
    codice:
    int ptr[N/4];
    char * buffer= (char *)&ptr;
    che dovrebbe dare sostanzialmente lo stesso effetto senza violare la strict aliasing rule.
    Perché questo è consentito? Forse perché il tipo char ha dimensione minore di int?
    E a partire da che standard è valida questa regola in C/C++?

  4. #4

    Re: Re: Re: [C++]Non ho capito la strict aliasing rule

    Originariamente inviato da Who am I
    Perché questo è consentito? Forse perché il tipo char ha dimensione minore di int?
    No, perché lo standard dice che un char * può essere alias di qualunque roba, ma non viceversa.
    La questione comunque è dibattuta.
    In ogni caso, sarebbe sempre più saggio usare l'array "di base" del tipo giusto ed eventualmente poi accedervi via char * quando necessario, sia per la strict aliasing rule, sia per evitare problemi di alignment.
    E a partire da che standard è valida questa regola in C/C++?
    Da sempre (con leggere differenze relative a peculiarità delle varie versioni dei linguaggi).
    Amaro C++, il gusto pieno dell'undefined behavior.

  5. #5
    Utente bannato
    Registrato dal
    Apr 2012
    Messaggi
    510
    Ma si può scavalcare questa cosa? Hai parlato di keyword fatte per specificare l' aliasing, e se io dichiaro il puntatore di tipo register?

    codice:
    char buffer[100];
    register int* ptr=(int*)&buffer;
    Perché in certi casi farebbe comodo avere un puntatore a int a un buffer di char.

    E a proposito delle unioni:

    L'alternativa in teoria non è usare le unioni; su praticamente ogni compilatore questo uso delle union è consentito, ma per lo standard è undefined behavior, poiché è consentito solo leggere dall'ultimo membro in cui si è scritto (le union in effetti nascono per poter avere qualche genere di polimorfismo in C, non per "reinterpretare" tipi in altra maniera).
    codice:
    typedef union
    {
        int arr_i[4];
        double arr_d[2];
    }myunion;
    Supponendo che sulla mia macchina go gli interi di 4B e i double di 8B, se scirvo:

    codice:
    myunion un;
    un.add_d[0]=4.0;
    Da questo momento in poi non posso mai leggere arr_i[0]? Come scavalcare questa cosa? A volte usare la memcpy porta inefficienze più grandi se ho un buffer lunghissimo.

  6. #6
    Originariamente inviato da Who am I
    Ma si può scavalcare questa cosa? Hai parlato di keyword fatte per specificare l' aliasing, e se io dichiaro il puntatore di tipo register?
    register non c'entra una mazza (e tra l'altro ormai sui compilatori recenti è praticamente un no-op), semmai volatile concettualmente potrebbe c'entrare, ma dal punto di vista dello standard è assolutamente irrilevante, non ci sono eccezioni sostanziali alla regola oltre ai char *.
    Perché in certi casi farebbe comodo avere un puntatore a int a un buffer di char.
    Nella mia esperienza una cosa del genere comunque funziona, dato che il "type punning" fatto in quella maniera su un buffer di char si fa spesso e i compilatori sono tolleranti a questa roba - ma attenzione ai problemi di alignment! Su architettura x86 si fa presto a disinteressarsene (dato che un accesso non allineato si traduce semplicemente in un calo di prestazioni), ma su molte altre architetture cercare di leggere un intero da un buffer di char non allineato correttamente produce un'eccezione hardware.
    Da questo momento in poi non posso mai leggere arr_i[0]?
    Dovresti prima scrivere in arr_i[0]; ma di nuovo, in genere i compilatori consentono il type punning via union, leggiti ad esempio la documentazione dell'opzione -fstrict-aliasing di g++.
    Amaro C++, il gusto pieno dell'undefined behavior.

Permessi di invio

  • Non puoi inserire discussioni
  • Non puoi inserire repliche
  • Non puoi inserire allegati
  • Non puoi modificare i tuoi messaggi
  •  
Powered by vBulletin® Version 4.2.1
Copyright © 2025 vBulletin Solutions, Inc. All rights reserved.