Vantaggi dell'accesso non volatile agli oggetti volatili non definiti?

5

Questa è una domanda su ISO C, che contiene questa frase:

If an attempt is made to refer to an object defined with a volatile-qualified type through use of an lvalue with non-volatile-qualified type, the behavior is undefined.

In ISO9899: 1999 (C99) questo appare in 6.7.3 Qualificatori di tipi .

In primo luogo, se abbiamo completamente rimosso questa frase dal testo, il comportamento sarebbe ancora indefinito? Vale a dire, questa ripresentazione di una mancanza esistente di un requisito, o sta rimuovendo esplicitamente un requisito che altrimenti potrebbe essere dedotto da altre parti del testo? (Immagina che sia stata aggiunta una frase che dice "ogni volta che due valori di tipo int vengono sommati insieme la cui somma aritmetica è quarantadue, il comportamento non è definito". Senza tale frase, il requisito per tale aggiunta di produrre un valore può (ed è) dedotto).

In secondo luogo, quali comportamenti utili consente a un'implementazione di fornire? Vale a dire, può un'implementazione fare qualcosa di utile che interrompe i programmi che accedono a qualcosa di volatile con un non- volatile lvalue e che quindi dipende dalla frase sopra? Esistono implementazioni che lo fanno, qualunque esso sia?

Ad esempio, è interessante e degno di nota il fatto che i programmi C possano accedere a const -qualificati con% nonconst lvalue, ma non memorizzarli. Rendere il comportamento del negozio indefinito è tangibilmente vantaggioso: le implementazioni possono posizionare le costanti nella memoria di sola lettura e in alcuni casi possono propagare costanti attraverso un'immagine del programma al momento della traduzione come se fossero costanti manifeste. Tuttavia, per funzionare è sufficiente l'accesso non% di% co_de. Non è necessario che gli accessi non const a const oggetti da lavorare siano presumibilmente una permissività che non porta vantaggi. Tuttavia, nel caso di const , è così rigoroso: non è richiesto nessun non- volatile -qualificato per funzionare. Qual è il vantaggio?

Può un oggetto che è definito come volatile dal programma C stesso essere dotato di proprietà utili che si interrompono se si verificano accessi di qualunque tipo a quell'oggetto attraverso un non- volatile lvalue?

(Sotto "utile", vorrei in particolare escludere la diagnosi. Terminare il programma dopo aver rilevato un tale accesso, senza un messaggio, o con un messaggio come "volatile acceduto come non volatile" non conta come utile; serve solo a far rispettare la mancanza di requisiti.)

(Inoltre, non sono interessato ai registri mappati in memoria e ad oggetti rigorosamente definiti dal programma in archivi statici, dinamici o automatici. L'uso di registri hardware è intrinsecamente non proporzionale.)

Tutto quello che posso pensare è che un% nonvolatile di accesso a un oggetto volatile potrebbe recuperare un valore non aggiornato, piuttosto che il valore che è stato archiviato l'ultima volta (correttamente, tramite volatile lvalue). Questo è un vantaggio economico nel modo seguente: se una scrittura avviene attraverso, ad esempio, un volatile lvalue, il compilatore non deve sospettare che questo abbia alcun effetto su qualsiasi valore volatile int lvalue. Questo porta a una migliore generazione del codice in funzioni che funzionano con miscele int e non volatile oggetti dello stesso tipo:

volatile int global;

void foo(int *p)
{
   int x = *p, y;
   global++; 
   y = *p;
}

Qui, il compilatore non deve considerare che volatile potrebbe essere un alias per *p , perché quell'utilizzo non è richiesto per funzionare secondo ISO C. Quindi può liberamente assumere che il valore di global non cambia tra i due riferimenti. Sia *p che x ricevono lo stesso valore, derivato da un singolo accesso a y .

Se l'aliasing era permesso, il compilatore avrebbe dovuto generare codice per ricaricare *p sul secondo accesso, in modo che rilevasse il valore più recente memorizzato in *p .

(Se rimuoviamo global dal frammento di programma sopra, allora questo è in realtà il caso, deve essere almeno sospettato, se non confermato. che volatile potrebbe essere un alias per *p e quindi global deve essere ricontrollato oppure deve essere generato codice che confronta il valore di runtime di *p con l'indirizzo di p e prende due percorsi diversi.)

Tuttavia, non introduciamo global nei programmi per ottenere questo tipo di oscuro beneficio di ottimizzazione; è usato per sconfiggere l'ottimizzazione. La parola chiave volatile può invece essere utilizzata per consentire all'implementazione di ragionare su lvalue non sottoposti ad aliasing. Sembra che quanto sopra non possa essere la logica.

    
posta Kaz 24.09.2015 - 22:25
fonte

6 risposte

1

La parola chiave volatile significa un sacco di cose diverse su molti compilatori diversi, ma uno dei loro usi più importanti è stato quello di rappresentare uno speciale hardware mappato in memoria. Quindi dichiarare il comportamento al riguardo indefinito consente ai compilatori di continuare a usarlo per rappresentare quell'hardware. La stessa ragione per cui right-shift, divide and modulus di un intero con segno negativo non è specificato esattamente: che lascia funzionare il set di istruzioni native di ogni CPU. Se vuoi un comportamento definito con precisione, ci sono atomics e div() .

    
risposta data 25.09.2015 - 00:18
fonte
1

Se sei interessato solo a "C portatile", troverai la parola chiave volatile per servire poco o per nulla, e quindi i requisiti sembreranno insoliti. L'intero scopo del linguaggio è consentire ai compilatori di esporre l'hardware mappato in memoria. Ottiene una vita minima se si aggiunge una libreria di threading [non portatile] nel mix, ma al suo centro, il suo compito è di fare comportamenti specifici di implementazione. Tanto che la specifica afferma:

An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. ... What constitutes an access to an object that has volatile-qualified type is implementation-defined. (ISO/IEC 9899:TC2 6.7.3.6)

Il valore nel negare l'accesso a un oggetto volatile tramite mezzi non volatili diventa evidente in quella linea. La specifica consente di implementare operazioni "volatili" in modo diverso rispetto alle operazioni "non volatili". Come esempio banale, una scrittura volatile potrebbe cercare un registro mappato in una mappatura, e poi scrivere sul registro, mentre la scrittura non volatile scrive semplicemente su quella memoria. Questo esempio potrebbe essere un po 'irrealistico, ma gli sviluppatori di C hanno fatto di tutto per supportare il software di scrittura in C su molte piattaforme, e oltre ad aggiungere una riga alle specifiche (quella che hai citato), non gli costava nulla aggiungere questo supporto alla lingua. In tal modo, tutti i tipi di situazioni di mappatura della memoria esotica sono resi molto più semplici.

Sulle moderne macchine x86, probabilmente non c'è motivo per il compilatore di trarne vantaggio. In effetti, non sarei sorpreso se i compilatori x86 "comportamento indefinito" in questo caso facessero semplicemente quello che pensate avrebbe dovuto essere fatto. Tuttavia, su piattaforme più anemiche, la capacità di non doversi preoccupare del caso in cui uno sviluppatore acceda a un volatile tramite un lval non volatile può rappresentare una sostanziale differenza nella generazione del codice del compilatore.

    
risposta data 24.11.2015 - 10:20
fonte
1

In un commento a Lorehead, Kaz osserva correttamente che questa regola è rilevante solo se il codice è altrimenti ben definito. Non è possibile rendere il codice più indefinito dopo tutto, UB è uno stato binario.

Quindi prendi gli usi portatili di volatile, cioè con i segnali e longjmp . In entrambi i casi il compilatore deve garantire che la semantica corretta sia rispettata. In particolare, l'aggiornamento dello stato globale visibile (volatile) deve essere come descritto nel codice. Lo stato non volatile può essere aggiornato in qualsiasi ordine.

Ora supponiamo che una scrittura attraverso int* possa influenzare lo stato volatile. Significa che qualsiasi scrittura indiretta non può più essere riordinata. Questo è un enorme impatto sulle prestazioni. Ancora peggio, tutte le letture effettuate tramite int* devono essere eseguite e non possono essere memorizzate nella cache in un registro.

Pertanto, questa regola consente significative ottimizzazioni, aumentando in modo massiccio il numero di operazioni di memoria che possono essere riordinate.

    
risposta data 24.11.2015 - 14:10
fonte
0

Immagina un'ipotetica macchina con una locazione di memoria magica 0xc0103 che, se scritta, imposta la luminosità di una luce intermittente sullo chassis della macchina sul valore memorizzato.

Potresti avere un programma come questo.

volatile uint32_t * pindicator = (volaitle uint32_t *) 0xc0103;

for (;;)
  {
    uint32_t amount = do_some_work();
    *pindicator = amount;
  }

Farà lampeggiare la luce in base alla quantità di lavoro attualmente gestita dal sistema. Anche se il programma non legge mai il valore di *pindicator , il compilatore non è autorizzato a ottimizzare alcun negozio ad esso perché è qualificato come volatile . Il compilatore non sa quali conseguenze potrebbe avere un negozio su *pindicator ma sai e puoi scrivere il tuo programma di conseguenza facendo affidamento sul fatto che il compilatore non si intrometta.

Tuttavia, se dovessimo accedervi tramite un puntatore non- volatile , il compilatore sarebbe libero di ottimizzare qualsiasi negozio che può dimostrare non ha alcun effetto collaterale visibile sul programma sotto as-if regola. In questo semplice esempio, il peggiore che ciò potrebbe fare è far lampeggiare la luce in modi irregolari o - più probabilmente - rimanere per sempre al buio. Ma lo standard C non conosce i blinkenlights e 0xc0103 . Non può dire quali conseguenze sarebbero ammissibili per l'accesso alla variabile attraverso un puntatore non- volatile . Un'altra variabile potrebbe fare riferimento a un registro "heartbeat" che deve essere incrementato almeno una volta al secondo e l'impossibilità di farlo potrebbe causare un meccanismo di sicurezza che attiva un fusibile che fa esplodere il dispositivo. Quindi, il modo standard per dire "tutto o niente potrebbe accadere" è dire: "il comportamento non è definito".

    
risposta data 24.11.2015 - 06:59
fonte
0

Molto semplicemente, l'accesso non volatile alle variabili volatili deve essere definito o indefinito. Quindi, se si desidera rimuovere la sezione che lo rende indefinito, è necessario definirlo. Sarei curioso di sapere come lo faresti. Ad esempio, basta definire il comportamento di

int* p = "address of variable which may or may not be volatile";
*p = 1;

in modo efficiente quando * p non è volatile e definito quando * p è volatile. E ricorda che gli più usi dei puntatori rientrano in questa categoria.

    
risposta data 24.11.2015 - 10:11
fonte
-1

L'accesso non volatile a una variabile è più veloce in quanto questo accesso può provenire dalla cache locale, ma ciò che è stato letto può essere cambiato altrimenti, quindi è pericoloso farlo a meno che non si sappia che il valore non cambia durante l'accesso.

Nel caso in cui ci siano 2 modifiche non volatili, la risoluzione di questo problema dipende dal compilatore e dall'hardware.

In che modo questo tipo di modifica dipende dall'hardware, quindi il modo più efficiente per gestirlo è di lasciarlo indefinito, in modo che il compilatore possa gestirlo nel miglior modo possibile mentre ti consente di accedere non volatile a variabili anche volatili quando sai cosa stai facendo per raccogliere velocità.

    
risposta data 24.11.2015 - 05:37
fonte

Leggi altre domande sui tag