Domanda

In diversi linguaggi di programmazione moderni (inclusi C ++, Java e C #), il linguaggio consente overflow di interi verificarsi in fase di esecuzione senza generare alcun tipo di condizione di errore.

Ad esempio, considera questo metodo C # (inventato), che non tiene conto della possibilità di overflow / underflow. (Per brevità, il metodo non gestisce anche 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<int> list)
{
    int sum = 0;
    foreach (int listItem in list)
    {
        sum += listItem;
    }
    return sum;
}

Se questo metodo viene chiamato come segue:

List<int> list = new List<int>();
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 segno a 32 bit e la somma dei valori nel elenco supera il valore dell'intero con segno massimo a 32 bit). La variabile somma avrà un valore di -294967296 (non un valore di 4000000000); questo molto probabilmente non è quello che intendeva lo sviluppatore (ipotetico) del metodo sumList.

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

Tuttavia, la domanda che mi interessa è perché queste lingue sono state progettate per impostazione predefinita per consentire in primo luogo l'overflow di numeri interi, anziché, ad esempio, sollevare un'eccezione quando viene eseguita un'operazione in fase di runtime che provocare un overflow. Sembra che tale comportamento contribuirebbe ad evitare bug nei casi in cui uno sviluppatore trascura di tenere conto della possibilità di overflow durante la scrittura di codice che esegue un'operazione aritmetica che potrebbe causare overflow. (Queste lingue avrebbero potuto includere qualcosa di simile a una parola chiave "non selezionata" che potrebbe designare un blocco in cui è consentito che si verifichi un overflow di numeri interi senza che venga sollevata un'eccezione, nei casi in cui tale comportamento è esplicitamente inteso dallo sviluppatore; C # in realtà ha questo .)

La risposta si riduce semplicemente alle prestazioni: i progettisti di lingue non volevano che le loro rispettive lingue fossero impostate per impostazione predefinita su "lento" operazioni intere aritmetiche in cui il runtime avrebbe bisogno di fare un lavoro extra per verificare se si è verificato un overflow, su ogni operazione aritmetica applicabile - e questa considerazione delle prestazioni ha superato il valore di evitare "silenzioso"; errori nel caso in cui si verifichi un overflow involontario?

Ci sono anche altri motivi per questa decisione di progettazione del linguaggio, oltre alle considerazioni sulle prestazioni?

È stato utile?

Soluzione

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

Quando C # era nuovo, Microsoft sperava che molti sviluppatori C ++ lo passassero. Sapevano che molte persone del C ++ pensavano che il C ++ fosse veloce, specialmente più veloce delle lingue che "sprecavano". tempo sulla gestione automatica della memoria e simili.

È probabile che sia i potenziali utenti che i revisori di riviste possano ottenere una copia del nuovo C #, installarlo, creare un'app banale che nessuno avrebbe mai scritto nel mondo reale, eseguirlo in un ciclo ristretto e misurare per quanto tempo ha preso. Quindi prenderebbero una decisione per la loro azienda o pubblicheranno un articolo basato su quel risultato.

Il fatto che il loro test abbia mostrato che C # è più lento del C ++ compilato in modo nativo è il tipo di cosa che spegne rapidamente le persone in C #. Il fatto che la tua app C # rileverà automaticamente overflow / underflow è il tipo di cosa che potrebbe perdere. Quindi, è disattivato per impostazione predefinita.

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

Altri suggerimenti

Penso che le prestazioni siano una buona ragione. Se consideri ogni istruzione in un tipico programma che incrementa un numero intero e se invece della semplice operazione di aggiungere 1, dovesse controllare ogni volta se l'aggiunta di 1 avrebbe traboccare il tipo, allora il costo in cicli extra sarebbe piuttosto grave.

Lavori sul presupposto che l'overflow di numeri interi sia sempre un comportamento indesiderato.

A volte l'overflow dei numeri interi è il comportamento desiderato. Un esempio che ho visto è la rappresentazione di un valore di prua assoluto come un numero in virgola fissa. Dato un int senza segno, 0 è 0 o 360 gradi e il numero intero senza segno massimo a 32 bit (0xffffffff) è il valore più grande appena sotto i 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;

}

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

C / C ++ non impone mai il comportamento della trap. 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 progettuale che non introduce overhead non presenti in C a meno che non lo richiediate. Quindi Stroustrup non avrebbe voluto imporre che gli interi si comportassero in un modo che richiedesse un controllo esplicito.

Alcuni primi compilatori e implementazioni leggere per hardware limitato non supportano affatto le eccezioni e le eccezioni possono spesso essere disabilitate con le opzioni del compilatore. Mandare eccezioni per gli incorporamenti del linguaggio sarebbe problematico.

Anche se il C ++ avesse verificato i numeri interi, il 99% dei programmatori nei primi giorni si sarebbe spento se disattivato per l'aumento delle prestazioni ...

Perché il controllo dell'overflow richiede tempo. Ogni operazione matematica primitiva, che normalmente si traduce in una singola istruzione di assemblaggio, dovrebbe includere un controllo per il trabocco, risultando in più istruzioni di assemblaggio, risultando potenzialmente in un programma che è più volte più lento.

Probabilmente è il 99% delle prestazioni. Su x86 dovrebbe controllare il flag di overflow su ogni operazione che sarebbe un enorme successo di prestazioni.

L'altro 1% coprirebbe quei casi in cui le persone eseguono manipolazioni di bit fantasiose o sono "imprecise" nel mescolare operazioni firmate e non firmate e desiderano la semantica di overflow.

La retrocompatibilità è importante. Con C, si presumeva che prestassi abbastanza attenzione alla dimensione dei tuoi tipi di dati che, se si verificava un overflow / underflow, era quello che volevi. Quindi con C ++, C # e Java, molto poco è cambiato con il modo in cui il "incorporato" tipi di dati hanno funzionato.

La mia comprensione del perché gli errori non verrebbero generati di default in fase di esecuzione si riduce all'eredità di voler creare linguaggi di programmazione con un comportamento simile all'ACID. In particolare, il principio che tutto ciò che lo codifichi per fare (o non codificare), lo farà (o non lo farà). Se non hai codificato alcuni gestori di errori, la macchina " assume " in virtù di nessun gestore di errori, che vuoi davvero fare la cosa ridicola e soggetta a crash che gli stai dicendo di fare.

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

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top