Perché le lingue non sollevano errori sull’overflow dei numeri interi per impostazione predefinita?

In diversi linguaggi di programmazione moderni (inclusi C ++, Java e C #), la lingua consente l’ overflow dei numeri interi a verificarsi in fase di esecuzione senza generare alcun tipo di condizione di errore.

Ad esempio, si consideri questo metodo (criptato) C #, che non tiene conto della possibilità di overflow / underflow. (Per brevità, anche il metodo non gestisce il caso in cui l’elenco specificato è un riferimento null.)

//Returns the sum of the values in the specified list. private static int sumList(List list) { int sum = 0; foreach (int listItem in list) { sum += listItem; } return sum; } 

Se questo metodo è chiamato come segue:

 List list = new List(); list.Add(2000000000); list.Add(2000000000); int sum = sumList(list); 

Si verificherà un overflow nel metodo sumList() (poiché il tipo int in C # è un numero intero con sumList() a 32 bit e la sum dei valori nell’elenco supera il valore del numero intero con sumList() massimo a 32 bit). La variabile sum avrà un valore di -294967296 (non un valore di 4000000000); questo molto probabilmente non è ciò che lo sviluppatore (ipotetico) del metodo sumList intendeva.

Ovviamente, ci sono varie tecniche che possono essere utilizzate dagli sviluppatori per evitare la possibilità di overflow di interi, come l’utilizzo di un tipo come BigInteger di Java, o la parola chiave checked e /checked compilatore /checked in C #.

Tuttavia, la domanda a cui sono interessato è il motivo per cui questi linguaggi sono stati progettati in modo predefinito per consentire l’overflow dei numeri interi in primo luogo, anziché, ad esempio, sollevare un’eccezione quando un’operazione viene eseguita in fase di esecuzione che comporterebbe un errore troppo pieno. Sembra che un simile comportamento possa aiutare a evitare i bug nei casi in cui uno sviluppatore trascuri di considerare la possibilità di un overflow durante la scrittura di codice che esegue un’operazione aritmetica che potrebbe causare un overflow. (Questi linguaggi potrebbero includere qualcosa come una parola chiave “non controllata” che potrebbe designare un blocco in cui è consentito che l’overflow di un intero si verifichi senza che venga sollevata un’eccezione, nei casi in cui tale comportamento è esplicitamente voluto dallo sviluppatore; )

La risposta si riduce semplicemente alle prestazioni: i progettisti del linguaggio non volevano che i loro rispettivi linguaggi avessero operazioni aritmetiche “lente” in cui il runtime avrebbe dovuto eseguire un lavoro extra per verificare se si verificava un overflow, su ogni aritmetica applicabile operazione – e questa considerazione della prestazione ha superato il valore di evitare errori “silenziosi” nel caso in cui si verificasse un sovraccarico involontario?

Ci sono altri motivi per questa decisione sul design del linguaggio, oltre a considerazioni sulle prestazioni?

In C #, era una questione di prestazioni. In particolare, benchmarking out-of-box.

Quando C # era nuovo, Microsoft sperava che molti sviluppatori C ++ potessero passare ad esso. Sapevano che molti C ++ pensavano che il C ++ fosse veloce, soprattutto più veloce delle lingue che “sprecavano” tempo nella gestione automatica della memoria e simili.

Sia i potenziali utenti che i revisori delle riviste potrebbero ottenere una copia del nuovo C #, installarlo, creare un’app banale che nessuno scriverà mai nel mondo reale, eseguirla in un ciclo chiuso e misurare il tempo necessario. Poi prenderebbero una decisione per la loro azienda o pubblicherebbero un articolo basato su quel risultato.

Il fatto che il loro test abbia dimostrato che C # è più lento del C ++ compilato in modo nativo è il tipo di cosa che potrebbe allontanare rapidamente le persone da C #. Il fatto che l’app C # rileverà automaticamente l’overflow / underflow è il tipo di cosa che potrebbero mancare. Quindi, è spento per impostazione predefinita.

Penso sia ovvio che il 99% delle volte che vogliamo / controllato sia attivo. È un compromesso sfortunato.

Penso che la performance sia una buona ragione. Se consideri ogni istruzione in un programma tipico che incrementa un intero, e se invece del semplice op aggiunga 1, doveva controllare ogni volta che l’aggiunta di 1 avrebbe overflow il tipo, quindi il costo nei cicli extra sarebbe piuttosto grave.

Lavorate partendo dal presupposto che l’overflow di un intero è sempre un comportamento indesiderato.

A volte l’overflow di interi è un comportamento desiderato. Un esempio che ho visto è la rappresentazione di un valore di intestazione assoluto come numero di punto fisso. Dato un int unsigned, 0 è 0 o 360 gradi e il numero intero senza segno massimo a 32 bit (0xffffffff) è il valore massimo appena inferiore a 360 gradi.

 int main() { uint32_t shipsHeadingInDegrees= 0; // Rotate by a bunch of degrees shipsHeadingInDegrees += 0x80000000; // 180 degrees shipsHeadingInDegrees += 0x80000000; // another 180 degrees, overflows shipsHeadingInDegrees += 0x80000000; // another 180 degrees // Ships heading now will be 180 degrees cout << "Ships Heading Is" << (double(shipsHeadingInDegrees) / double(0xffffffff)) * 360.0 << std::endl; } 

Ci sono probabilmente altre situazioni in cui l'overflow è accettabile, simile a questo esempio.

È probabile una performance del 99%. Su x86 dovrebbe controllare il flag di overflow su ogni operazione che sarebbe un enorme successo in termini di prestazioni.

L’altro 1% coprirà quei casi in cui le persone stanno facendo manipolazioni di bit elaborate o essendo ‘imprecise’ nel mischiare operazioni firmate e non firmate e vogliono la semantica di overflow.

Perché il controllo dell’overflow richiede tempo. Ciascuna operazione matematica primitiva, che normalmente si traduce in una singola istruzione di assemblaggio, dovrebbe includere un controllo per l’overflow, risultante in più istruzioni di assemblaggio, potenzialmente con conseguente un programma che è più volte più lento.

C / C ++ non impone mai il comportamento di trappola. Anche l’ovvia divisione per 0 è un comportamento indefinito in C ++, non un tipo specifico di trap.

Il linguaggio C non ha alcun concetto di trapping, a meno che non si contino i segnali.

C ++ ha un principio di progettazione che non introduce un sovraccarico non presente in C a meno che non lo chieda. Quindi Stroustrup non avrebbe voluto imporre che gli interi si comportino in un modo che richiede un controllo esplicito.

Alcuni dei primi compilatori e le implementazioni leggere per hardware limitato non supportano eccezioni e le eccezioni possono spesso essere disabilitate con le opzioni del compilatore. Obbligare le eccezioni per i built-in linguistici sarebbe problematico.

Anche se il C ++ avesse controllato gli interi, il 99% dei programmatori nei primi giorni si sarebbe spento se non fosse stato per l’aumento delle prestazioni …

La retrocompatibilità è grande. Con C, si presumeva che prestassi sufficiente attenzione alla dimensione dei tuoi tipi di dati che, se si verificava un over / underflow, era quello che volevi. Quindi con C ++, C # e Java, molto poco cambiato con il modo in cui funzionavano i tipi di dati “integrati”.

La mia comprensione del motivo per cui gli errori non vengono generati di default durante il runtime si riduce all’eredità di desiderare di creare linguaggi di programmazione con un comportamento simile a ACID. In particolare, il principio che qualsiasi cosa che si codifica per fare (o non codificare), lo farà (o non farà). Se non hai codificato alcun gestore di errori, allora la macchina “assumerà” in assenza di un gestore di errori, che vuoi veramente fare la cosa ridicola e soggetta a incidenti che stai dicendo di fare.

(Riferimento ACID: http://en.wikipedia.org/wiki/ACID )