Perché C non ha float senza segno?


131

Lo so, la domanda sembra essere strana. I programmatori a volte pensano troppo. Per favore continua a leggere ...

In CI uso signede unsignednumeri interi molto. Mi piace il fatto che il compilatore mi avverta se faccio cose come l'assegnazione di un numero intero con segno a una variabile senza segno. Ricevo degli avvisi se confronto gli interi con segno e molto altro.

Mi piacciono questi avvertimenti. Mi aiutano a mantenere il mio codice corretto.

Perché non abbiamo lo stesso lusso per i galleggianti? Una radice quadrata sicuramente non restituirà mai un numero negativo. Ci sono anche altri posti in cui un valore float negativo non ha significato. Candidato perfetto per un galleggiante non firmato.

A proposito - Non sono davvero entusiasta del singolo extra di precisione che potrei ottenere rimuovendo il bit di segno dai float. Sono molto contento di floatcome sono adesso. Vorrei solo contrassegnare un float come non firmato a volte e ottenere lo stesso tipo di avvisi che ricevo con numeri interi.

Non sono a conoscenza di alcun linguaggio di programmazione che supporti numeri in virgola mobile non firmati.

Qualche idea sul perché non esistano?


MODIFICARE:

So che la FPU x87 non ha istruzioni per gestire i float senza segno. Consente solo di utilizzare le istruzioni float firmate. L'uso improprio (es. Andando sotto lo zero) potrebbe essere considerato un comportamento indefinito allo stesso modo in cui l'overflow degli interi con segno non è definito.


4
Interessante, puoi pubblicare un esempio di un caso in cui il controllo del tipo di firma è stato utile?

litb, il tuo commento è stato rivolto a me? in tal caso, non capisco

Iraimbilanja yeah :) fabs non può restituire un numero negativo, perché restituisce il valore assoluto del suo argomento
Johannes Schaub - litb

Giusto. Non ho chiesto come un ipotetico float senza segno potesse aiutare la correttezza. Quello che ho chiesto è stato: in quale situazione pipenbrinck ha trovato utile il controllo dei caratteri della firma Int (portandolo a cercare lo stesso meccanismo per i float). per quanto riguarda la sicurezza dei caratteri

1
Esiste una micro-ottimizzazione non firmata per il controllo point-in-range: ((unsigned) (p-min)) <(max-min), che ha solo un ramo, ma, come sempre, è meglio profilare per vedere se aiuta davvero (l'ho usato principalmente su 386 core, quindi non so come facciano le CPU moderne).
Skizz,

Risposte:


114

Il motivo per cui C ++ non supporta i float senza segno è perché non ci sono operazioni di codice macchina equivalenti per l'esecuzione della CPU. Quindi sarebbe molto inefficiente supportarlo.

Se il C ++ lo supportasse, a volte useresti un float senza segno e non ti rendi conto che la tua performance è stata appena eliminata. Se C ++ lo supportava, ogni operazione in virgola mobile dovrebbe essere verificata per vedere se è firmata o meno. E per i programmi che eseguono milioni di operazioni in virgola mobile, questo non è accettabile.

Quindi la domanda sarebbe perché gli implementatori hardware non lo supportano. E penso che la risposta sia che non esisteva uno standard float senza segno definito originariamente. Dal momento che alle lingue piace essere retrocompatibili, anche se fossero state aggiunte le lingue non potevano farne uso. Per vedere le specifiche in virgola mobile dovresti guardare lo standard IEEE 754 in virgola mobile .

Puoi aggirare il fatto di non avere un tipo a virgola mobile senza segno creando una classe float senza segno che incapsula un float o double e genera avvisi se si tenta di passare un numero negativo. Questo è meno efficiente, ma probabilmente se non li usi intensamente non ti importerai di quella leggera perdita di prestazioni.

Sicuramente vedo l'utilità di avere un float senza segno. Ma C / C ++ tende a scegliere l'efficienza che funziona meglio per tutti rispetto alla sicurezza.


17
C / C ++ non richiede operazioni specifiche sul codice macchina per implementare la lingua. I primi compilatori C / C ++ potevano generare un codice a virgola mobile per il 386 - una CPU senza FPU! Il compilatore genererebbe chiamate in libreria per emulare le istruzioni FPU. Pertanto, è possibile eseguire una float senza il supporto della CPU
Skizz,

10
Skizz, sebbene sia corretto, Brian ha già affrontato questo aspetto: poiché non esiste un codice macchina equivalente, le prestazioni saranno orribili al confronto.
Anthony,

2
@Brian R. Bondy: Ti ho perso qui: "perché non ci sono operazioni di codice macchina equivalenti per la CPU da eseguire ...". Puoi per favore spiegare, in termini più semplici?
Lazer,

2
Il motivo per cui OP ha richiesto il supporto per float senza segno era per i messaggi di avviso, quindi in realtà non ha nulla a che fare con la fase di generazione del codice del compilatore - ha a che fare solo con il modo in cui esegue il controllo del tipo in anticipo - quindi il supporto per loro nel codice macchina è irrilevante e (come è stato aggiunto alla fine della domanda) le normali istruzioni in virgola mobile potrebbero essere utilizzate per l'esecuzione effettiva.
Joe F,

2
Non sono sicuro di capire perché ciò dovrebbe influire sulle prestazioni. Proprio come con int's, tutto il controllo del tipo relativo ai segni potrebbe avvenire in fase di compilazione. Il PO suggerisce che unsigned floatverrebbero implementati regolarmente floatcon controlli in fase di compilazione per garantire che determinate operazioni non significative non vengano mai eseguite. Il codice macchina e le prestazioni risultanti potrebbero essere identici, indipendentemente dal fatto che i float siano firmati o meno.
xanderflood,

14

Esiste una differenza significativa tra numeri interi con e senza segno in C / C ++:

value >> shift

i valori con segno lasciano invariato il bit superiore (segno di estensione), i valori senza segno cancellano il bit superiore.

Il motivo per cui non esiste un float senza segno è che si verificano rapidamente tutti i tipi di problemi se non ci sono valori negativi. Considera questo:

float a = 2.0f, b = 10.0f, c;
c = a - b;

Che valore ha c? -8. Ma cosa significherebbe in un sistema senza numeri negativi. FLOAT_MAX - 8 forse? In realtà, ciò non funziona come FLOAT_MAX - 8 è FLOAT_MAX a causa degli effetti di precisione, quindi le cose sono ancora più complicate. E se fosse parte di un'espressione più complessa:

float a = 2.0f, b = 10.0f, c = 20.0f, d = 3.14159f, e;
e = (a - b) / d + c;

Questo non è un problema per gli interi a causa della natura del sistema del complemento di 2.

Considera anche le funzioni matematiche standard: sin, cos e tan funzionerebbero solo per metà dei loro valori di input, non potresti trovare il registro dei valori <1, non puoi risolvere equazioni quadratiche: x = (-b +/- root ( bb - 4.ac)) / 2.a e così via. In effetti, probabilmente non funzionerebbe per nessuna funzione complessa in quanto queste tendono ad essere implementate come approssimazioni polinomiali che utilizzerebbero valori negativi da qualche parte.

Quindi, i float senza segno sono piuttosto inutili.

Ciò non significa che una classe che controlla i valori float non sia utile, potresti voler bloccare i valori su un determinato intervallo, ad esempio i calcoli RGB.


@Skizz: se la rappresentazione è un problema, vuoi dire se qualcuno è in grado di escogitare un metodo per archiviare i galleggianti tanto efficiente quanto 2's complement, non ci saranno problemi con avere float non firmati?
Lazer,

3
value >> shift for signed values leave the top bit unchanged (sign extend) Sei sicuro di questo? Ho pensato che fosse un comportamento definito dall'implementazione, almeno per i valori con segno negativo.
Dan,

@Dan: Ho appena guardato lo standard recente e in realtà afferma che è definita l'implementazione - suppongo che sia solo nel caso in cui ci sia una CPU che non ha alcun turno a destra con istruzioni di estensione del segno.
Skizz,


1
la virgola mobile tradizionalmente satura (a - / + Inf) invece di avvolgimento. Potresti aspettarti che l'overflow di sottrazione senza segno si saturi 0.0o, eventualmente, Inf o NaN. O semplicemente essere undefined Behaviour, come l'OP suggerito in una modifica alla domanda. Ri: funzioni trig: quindi non definire le versioni di input senza segno di sine così via e assicurarsi di considerare il loro valore di ritorno come firmato. La domanda non proponeva la sostituzione di float con float senza segno, ma solo l'aggiunta unsigned floatcome nuovo tipo.
Peter Cordes,

9

(A parte, Perl 6 ti lascia scrivere

subset Nonnegative::Float of Float where { $_ >= 0 };

e quindi puoi usare Nonnegative::Floatcome faresti con qualsiasi altro tipo.)

Non esiste supporto hardware per operazioni in virgola mobile senza segno, quindi C non lo offre. C è principalmente progettato per essere un "assemblaggio portatile", ovvero il più vicino possibile al metallo senza essere legato a una piattaforma specifica.

[modificare]

C è come un assemblaggio: ciò che vedi è esattamente ciò che ottieni. Un implicito "Controllerò che questo galleggiante non sia negativo per te" va contro la sua filosofia di design. Se lo vuoi davvero, puoi aggiungere assert(x >= 0)o simili, ma devi farlo esplicitamente.



8

Credo che l'int non firmato sia stato creato a causa della necessità di un margine di valore maggiore di quello che l'int firmato potrebbe offrire.

Un float ha un margine molto più grande, quindi non c'è mai stato un bisogno "fisico" di un float senza segno. E mentre fai notare te stesso nella tua domanda, la precisione aggiuntiva a 1 bit non è nulla per cui uccidere.

Modifica: dopo aver letto la risposta di Brian R. Bondy , devo modificare la mia risposta: ha sicuramente ragione sul fatto che le CPU sottostanti non avevano operazioni float senza segno. Tuttavia, continuo a credere che questa sia stata una decisione di progettazione basata sui motivi che ho indicato sopra ;-)


2
Inoltre, l'aggiunta e la sottrazione di numeri interi è lo stesso con segno o senza segno - virgola mobile, non così tanto. Chi farebbe il lavoro extra per supportare sia i float firmati che quelli non firmati data l'utilità marginale relativamente bassa di tale caratteristica?
effimero

7

Penso che Treb sia sulla buona strada. È più importante per gli interi che tu abbia un tipo corrispondente senza segno. Quelli sono quelli usati nello spostamento dei bit e usati nelle bitmap . Un piccolo segno si mette in mezzo. Ad esempio, spostando a destra un valore negativo, il valore risultante è l'implementazione definita in C ++. Farlo con un numero intero senza segno o traboccare tale ha una semantica perfettamente definita perché non esiste un tale bit sulla strada.

Quindi, almeno per i numeri interi, la necessità di un tipo senza segno separato è più forte della semplice segnalazione. Non è necessario considerare tutti i punti precedenti per i galleggianti. Quindi, penso, non c'è davvero bisogno di supporto hardware per loro, e C non li supporterà già a quel punto.


5

Una radice quadrata sicuramente non restituirà mai un numero negativo. Ci sono anche altri posti in cui un valore float negativo non ha significato. Candidato perfetto per un galleggiante non firmato.

C99 supporta numeri complessi e una forma generica di tipo sqrt, quindi sqrt( 1.0 * I)sarà negativa.


I commentatori hanno evidenziato una leggera lucentezza sopra, in quanto mi riferivo alla sqrtmacro di tipo generico anziché alla funzione, e restituirà un valore in virgola mobile scalare troncando il complesso al suo componente reale:

#include <complex.h>
#include <tgmath.h>

int main () 
{
    complex double a = 1.0 + 1.0 * I;

    double f = sqrt(a);

    return 0;
}

Contiene anche una scoreggia cerebrale, poiché la parte reale del sqrt di qualsiasi numero complesso è positiva o zero e sqrt (1.0 * I) è sqrt (0,5) + sqrt (0,5) * I non -1.0.


Sì, ma chiami una funzione con un nome diverso se lavori con numeri complessi. Anche il tipo di ritorno è diverso. Un buon punto però!
Nils Pipenbrinck,

4
Il risultato di sqrt (i) è un numero complesso. E poiché i numeri complessi non sono ordinati, non si può dire che un numero complesso sia negativo (ovvero <0)
quinmars

1
quinmars, sicuro che non sia csqrt? o parli di matematica invece di C? sono comunque d'accordo che sia un buon punto :)
Johannes Schaub - litb

In effetti, stavo parlando di matematica. Non ho mai affrontato i numeri complessi in c.
Quinmars,

1
"la radice quadrata sicuramente non restituirà mai un numero negativo." -> sqrt(-0.0)produce spesso -0.0. Naturalmente -0.0 non è un valore negativo .
chux - Ripristina Monica il

4

Immagino che dipenda dal fatto che le specifiche IEEE in virgola mobile siano firmate e che la maggior parte dei linguaggi di programmazione le utilizzi.

Articolo di Wikipedia sui numeri in virgola mobile IEEE-754

Modifica: inoltre, come notato da altri, la maggior parte dell'hardware non supporta i float non negativi, quindi il tipo normale di float è più efficiente da eseguire poiché esiste un supporto hardware.


La C fu introdotta molto prima che apparisse lo standard IEEE-754
phuclv

@phuclv Nessuno dei due era un hardware a virgola mobile banale. Fu adottato nello standard C "pochi" anni dopo. Probabilmente c'è della documentazione fluttuante su Internet al riguardo. (Inoltre, l'articolo di Wikipedia menziona C99).
Tobias Wärre,

Non capisco cosa intendi. Non c'è "hardware" nella tua risposta, e IEEE-754 è nato dopo C, quindi i tipi a virgola mobile in C non possono dipendere
dallo

@phuclv C è / era anche conosciuto come assembly portatile, quindi può essere abbastanza vicino all'hardware. Le lingue acquisiscono funzionalità nel corso degli anni, anche se (prima del mio tempo) il float era implementato in C, probabilmente era un'operazione basata su software e piuttosto costosa. Al momento di rispondere a questa domanda, ovviamente avevo una comprensione migliore di ciò che stavo cercando di spiegare di quanto non faccia ora. E se guardi la risposta accettata potresti capire perché ho citato lo standard IEE754. Quello che non capisco è che hai puntualizzato una risposta di 10 anni che non è quella accettata?
Tobias Wärre,

3

Penso che il motivo principale sia che i float senza segno avrebbero usi davvero limitati rispetto agli ints senza segno. Non compro l'argomento secondo cui l'hardware non lo supporta. I processori meno recenti non avevano alcuna capacità in virgola mobile, era tutto emulato nel software. Se i float senza segno fossero utili sarebbero stati implementati prima nel software e l'hardware avrebbe seguito l'esempio.


4
Il PDP-7, la prima piattaforma di C, aveva un'unità hardware a virgola mobile opzionale. Il PDP-11, la piattaforma successiva di C, aveva un float a 32 bit nell'hardware. 80x86 venne una generazione dopo, con una tecnologia che era una generazione dietro.
effimero

3

I tipi interi senza segno in C sono definiti in modo da obbedire alle regole di un anello algebrico astratto. Ad esempio, per qualsiasi valore X e Y, l'aggiunta di XY a Y produrrà X. I tipi interi senza segno sono garantiti per obbedire a queste regole in tutti i casi che non comportano la conversione da o verso qualsiasi altro tipo numerico [o tipi senza segno di dimensioni diverse] e tale garanzia è una delle caratteristiche più importanti di tali tipi. In alcuni casi, vale la pena rinunciare alla capacità di rappresentare numeri negativi in ​​cambio delle garanzie extra che solo i tipi non firmati possono fornire. I tipi a virgola mobile, siano essi firmati o meno, non possono rispettare tutte le regole di un anello algebrico [ad esempio, non possono garantire che X + YY sarà uguale a X], e infatti IEEE non lo fa ' consentire loro anche di rispettare le regole di una classe di equivalenza [richiedendo che determinati valori siano ineguali rispetto a se stessi]. Non credo che un tipo a virgola mobile "senza segno" possa rispettare qualsiasi assioma che un normale tipo a virgola mobile non potrebbe, quindi non sono sicuro di quali vantaggi offrirebbe.


1

IHMO è perché il supporto di tipi a virgola mobile sia firmati che non firmati in hardware o software sarebbe troppo problematico

Per i tipi di numeri interi possiamo utilizzare la stessa unità logica per le operazioni di numero intero sia con segno che senza segno nella maggior parte delle situazioni usando la bella proprietà del complemento di 2, perché il risultato è identico in quei casi per operazioni di add, sub, non allargamento e operazioni più bit a bit. Per le operazioni che distinguono tra versione firmata e non firmata, possiamo ancora condividere la maggior parte della logica . Per esempio

  • L'aritmetica e lo spostamento logico richiedono solo una leggera modifica nel riempimento per i bit superiori
  • L'ampliamento della moltiplicazione può utilizzare lo stesso hardware per la parte principale e quindi una logica separata per regolare il risultato per modificare la significatività . Non che sia utilizzato in moltiplicatori reali, ma è possibile farlo
  • Il confronto con segno può essere convertito in confronto senza segno e viceversa facilmente alternando il bit superiore o aggiungendoloINT_MIN . Anche teoricamente possibile, probabilmente non viene utilizzato sull'hardware, ma è utile su sistemi che supportano solo un tipo di confronto (come 8080 o 8051)

I sistemi che usano il complemento di 1 hanno anche solo bisogno di una piccola modifica alla logica perché è semplicemente il bit di riporto avvolto nel bit meno significativo. Non sono sicuro dei sistemi di grandezza dei segni ma sembra che utilizzino il complemento di 1 internamente, quindi vale la stessa cosa

Sfortunatamente non abbiamo quel lusso per i tipi a virgola mobile. Liberando semplicemente il bit di segno avremo la versione non firmata. Ma allora per cosa dovremmo usare quel bit?

  • Aumenta l'intervallo aggiungendolo all'esponente
  • Aumenta la precisione aggiungendola alla mantissa. Questo è spesso più utile, poiché in genere è necessaria una maggiore precisione rispetto alla portata

Ma entrambe le scelte richiedono un sommatore più grande per adattarsi alla gamma di valori più ampia. Ciò aumenta la complessità della logica mentre la parte superiore dell'adder rimane inutilizzata per la maggior parte del tempo. Saranno necessari ulteriori circuiti per moltiplicazioni, divisioni o altre operazioni complesse

Sui sistemi che utilizzano software in virgola mobile sono necessarie 2 versioni per ogni funzione che non era prevista durante il tempo in cui la memoria era molto costosa, o dovresti trovare un modo "complicato" per condividere parti delle funzioni firmate e non firmate

Tuttavia l' hardware in virgola mobile esisteva molto prima dell'invenzione di C , quindi credo che la scelta in C fosse dovuta alla mancanza di supporto hardware a causa del motivo di cui sopra

Detto questo, esistono diversi formati a virgola mobile non firmati specializzati , principalmente per scopi di elaborazione delle immagini, come il tipo a virgola mobile a 10 e 11 bit del gruppo Khronos


0

Ho il sospetto che sia perché i processori sottostanti presi di mira dai compilatori C non hanno un buon modo di gestire i numeri in virgola mobile senza segno.


I processori sottostanti avevano un buon modo di gestire i numeri in virgola mobile firmati? C stava diventando popolare quando i processori ausiliari in virgola mobile erano idiosincratici e quasi universali.
David Thornley,

1
Non conosco tutte le linee temporali storiche, ma c'era un supporto hardware emergente per i float firmati, anche se raro come si sottolinea. I progettisti linguistici potevano incorporare il supporto per esso mentre i backend del compilatore avevano livelli di supporto variabili a seconda dell'architettura mirata.
Brian Ensink,

0

Buona domanda.

Se, come dici tu, è solo per avvisi in fase di compilazione e nessuna modifica nel loro comportamento, altrimenti l'hardware sottostante non è interessato e come tale sarebbe solo una modifica C ++ / Compiler.

Ho vinto lo stesso in precedenza, ma la cosa è: non sarebbe di grande aiuto. Nella migliore delle ipotesi il compilatore può trovare incarichi statici.

unsigned float uf { 0 };
uf = -1f;

O minimalisticamente più lungo

unsigned float uf { 0 };
float f { 2 };
uf -= f;

Ma questo è tutto. Con i tipi interi senza segno si ottiene anche un wraparound definito, ovvero si comporta come un'aritmetica modulare.

unsigned char uc { 0 };
uc -= 1;

dopo questo 'uc' contiene il valore di 255.

Ora, cosa farebbe un compilatore con lo stesso scenario dato un tipo float senza segno? Se i valori non sono noti al momento della compilazione, sarebbe necessario generare il codice che esegue prima i calcoli e quindi esegue un controllo del segno. Ma cosa succederebbe quando il risultato di tale calcolo sarebbe "-5,5" - quale valore dovrebbe essere memorizzato in un float dichiarato senza segno? Si potrebbe provare l'aritmetica modulare come per i tipi integrali, ma questo ha i suoi problemi: il valore più grande è indiscutibilmente l'infinito .... che non funziona, non si può avere "infinito - 1". Cercare il più grande valore distinto che può contenere, inoltre, non funzionerà davvero quando ci si imbatte nella precissione. "NaN" sarebbe un candidato.

Infine, questo non sarebbe un problema con i numeri a virgola fissa in quanto il modulo è ben definito.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.