Perché l'overflow aritmetico viene ignorato?

75

Hai mai provato a riassumere tutti i numeri da 1 a 2.000.000 nel tuo linguaggio di programmazione preferito? Il risultato è facile da calcolare manualmente: 2.000.001.000.000, che è circa 900 volte più grande del valore massimo di un intero a 32 bit senza segno.

C # stampa -1453759936 - un valore negativo! E immagino che Java faccia lo stesso.

Questo significa che ci sono alcuni linguaggi di programmazione comuni che ignorano l'overflow aritmetico di default (in C # ci sono opzioni nascoste per cambiarlo). Questo è un comportamento che mi sembra molto rischioso, e non è stato il crash di Ariane 5 causato da un tale trabocco?

Quindi: quali sono le decisioni di progettazione dietro un comportamento così pericoloso?

Modifica:

Le prime risposte a questa domanda esprimono i costi eccessivi del controllo. Eseguiamo un breve programma C # per testare questa ipotesi:

Stopwatch watch = Stopwatch.StartNew();
checked
{
    for (int i = 0; i < 200000; i++)
    {
        int sum = 0;
        for (int j = 1; j < 50000; j++)
        {
            sum += j;
        }
    }
}
watch.Stop();
Console.WriteLine(watch.Elapsed.TotalMilliseconds);

Sulla mia macchina, la versione controllata prende 11015 ms, mentre la versione deselezionata prende 4125 ms. Cioè i passaggi di controllo richiedono quasi il doppio della somma dei numeri (in totale 3 volte l'ora originale). Ma con le 10.000.000.000 di ripetizioni, il tempo impiegato da un controllo è ancora inferiore a 1 nanosecondo. Potrebbero esserci situazioni in cui ciò è importante, ma per la maggior parte delle applicazioni ciò non avrà importanza.

Modifica 2:

Ho ricompilato la nostra applicazione server (un servizio di Windows che analizzava i dati ricevuti da diversi sensori, con un certo numero di crunch coinvolti) con il parametro /p:CheckForOverflowUnderflow="false" (normalmente, ho attivato il controllo di overflow) e lo ho distribuito su un dispositivo. Il monitoraggio di Nagios mostra che il carico medio della CPU è rimasto al 17%.

Questo significa che il colpo di performance trovato nell'esempio inventato sopra è totalmente irrilevante per la nostra applicazione.

    
posta Bernhard Hiller 08.05.2017 - 12:06
fonte

14 risposte

85

Ci sono 3 motivi per questo:

  1. Il costo del controllo degli overflow (per ogni singola operazione aritmetica) in fase di esecuzione è eccessivo.

  2. La complessità di dimostrare che un controllo di overflow può essere omesso in fase di compilazione è eccessivo.

  3. In alcuni casi (ad es. calcoli CRC, librerie di numeri grandi, ecc.) "wrap on overflow" è più conveniente per i programmatori.

risposta data 08.05.2017 - 12:34
fonte
64

Chi dice che è un compromesso ?!

Eseguo tutte le mie app di produzione con il controllo di overflow abilitato. Questa è un'opzione del compilatore C #. In realtà ho fatto un benchmark e non sono stato in grado di determinare la differenza. Il costo di accesso al database per generare HTML (non giocattolo) oscura i costi di controllo dell'overflow.

Apprezzo il fatto che conosco che nessuna operazione trabocca nella produzione. Quasi tutto il codice si comporterebbe in modo irregolare in presenza di overflow. Gli insetti non sarebbero benigni. La corruzione dei dati è probabile, le possibilità di sicurezza sono una possibilità.

Nel caso in cui avessi bisogno delle prestazioni, che a volte è il caso, disabilito il controllo di overflow utilizzando unchecked {} su base granulare. Quando voglio dire che faccio affidamento su un'operazione non traboccante, potrei aggiungere in modo ridondante checked {} al codice per documentare tale fatto. Sono consapevole degli overflow, ma non necessariamente devo essere grazie al controllo.

Credo che il team C # abbia fatto la scelta sbagliata quando ha scelto di non di verificare l'overflow per impostazione predefinita, ma quella scelta è ora sigillata a causa di forti problemi di compatibilità. Si noti che questa scelta è stata fatta intorno all'anno 2000. L'hardware era meno capace e .NET non aveva ancora molta trazione. Forse .NET ha voluto fare appello ai programmatori Java e C / C ++ in questo modo. .NET è anche pensato per essere in grado di essere vicino al metallo. Ecco perché ha codice non sicuro, strutture e grandi capacità di chiamata native che Java non ha.

Più veloce diventa il nostro hardware e i compilatori più intelligenti ottengono il controllo di overflow più attraente per impostazione predefinita.

Credo anche che il controllo dell'overflow sia spesso meglio di numeri infiniti. I numeri infiniti hanno un costo in termini di prestazioni ancora più elevato, più difficili da ottimizzare (credo) e aprono la possibilità di un consumo illimitato di risorse.

Il modo in cui JavaScript ha a che fare con l'overflow è ancora peggio. I numeri JavaScript sono doppi in virgola mobile. Un "overflow" si manifesta come lasciando il set completamente preciso di numeri interi. I risultati leggermente si verificheranno (come essere spenti di uno - questo può trasformare i loop finiti in infiniti).

Per alcuni linguaggi come il controllo di overflow C / C ++ di default è chiaramente inappropriato perché i tipi di applicazioni che vengono scritte in queste lingue richiedono prestazioni bare metal. Tuttavia, ci sono sforzi per rendere C / C ++ in un linguaggio più sicuro, consentendo di optare in una modalità più sicura. Questo è lodevole dal momento che il 90-99% del codice tende a essere freddo. Un esempio è l'opzione del compilatore fwrapv che forza il wrapping del complemento a 2. Questa è una caratteristica di "qualità dell'implementazione" del compilatore, non dal linguaggio.

Haskell non ha stack di chiamate logiche e nessun ordine di valutazione specificato. Questo fa sì che le eccezioni si verifichino in punti imprevedibili. In a + b non è specificato se a o b viene valutato per primo e se tali espressioni terminano o meno. Pertanto, ha senso per Haskell utilizzare gli interi non vincolati il più delle volte. Questa scelta è adatta a un linguaggio puramente funzionale perché le eccezioni sono veramente inappropriate nella maggior parte del codice Haskell. E la divisione per zero è davvero un punto problematico nella progettazione del linguaggio Haskells. Invece di numeri interi non vincolati, potevano usare anche interi interi a larghezza fissa, ma questo non si adattava al tema "focus sulla correttezza" che caratterizza la lingua.

Un'alternativa alle eccezioni di overflow sono valori di veleno creati da operazioni non definite e propagati tramite operazioni (come il valore float NaN ). Ciò sembra molto più costoso del controllo di overflow e rende tutte le operazioni più lente, non solo quelle che possono fallire (salvo l'accelerazione hardware che normalmente fluttua e gli inti comunemente non hanno - sebbene Itanium ha NaT che è "Not a Thing" ). Inoltre, non vedo il punto di fare in modo che il programma continui a zoppicare insieme a dati errati. È come ON ERROR RESUME NEXT . Nasconde gli errori ma non aiuta a ottenere risultati corretti. supercat sottolinea che a volte è un ottimizzazione delle prestazioni per fare questo.

    
risposta data 08.05.2017 - 17:45
fonte
30

Perché è un cattivo compromesso rendere i calcoli tutti molto più costosi per catturare automaticamente il caso raro che si verifichi un di overflow. È molto meglio appesantire il programmatore riconoscendo i rari casi in cui si tratta di un problema e aggiungere misure di prevenzione speciali piuttosto che far sì che i tutti programmatori paghino il prezzo per funzionalità che non usano.

    
risposta data 08.05.2017 - 12:35
fonte
20

what are the design decisions behind such a dangerous behavior?

"Non costringere gli utenti a pagare una penalizzazione delle prestazioni per una funzione di cui potrebbero non aver bisogno."

È uno dei principi più basilari nella progettazione di C e C ++ e deriva da un momento diverso in cui dovevi passare attraverso ridicole contorsioni per ottenere prestazioni appena adeguate per compiti che oggi sono considerati banali.

Le lingue più recenti si interrompono con questo atteggiamento per molte altre funzionalità, come il controllo dei limiti dell'array. Non sono sicuro del motivo per cui non l'hanno fatto per il controllo di overflow; potrebbe essere semplicemente una svista.

    
risposta data 08.05.2017 - 15:08
fonte
20

Legacy

Direi che il problema è probabilmente radicato nell'eredità. In C:

  • overflow con segno è un comportamento non definito (i compilatori supportano i flag per renderlo a capo),
  • l'overflow non firmato è un comportamento definito (viene eseguito il wrapping).

Questo è stato fatto per ottenere le migliori prestazioni possibili, seguendo il principio che il programmatore sa cosa sta facendo .

conduce a Statu-Quo

Il fatto che C (e per estensione C ++) non richiedano il rilevamento di overflow in virata significa che il controllo di overflow è lento.

L'hardware si rivolge principalmente a C / C ++ (seriamente, x86 ha un'istruzione strcmp (alias PCMPISTRI as di SSE 4.2)!), e poiché C non interessa, le CPU comuni non offrono metodi efficienti per rilevare gli overflow. In x86, devi controllare un flag per core dopo ogni operazione potenzialmente traboccante; quando ciò che vuoi veramente è una bandiera "contaminata" sul risultato (proprio come si diffonde il NaN). E le operazioni vettoriali potrebbero essere ancora più problematiche. Alcuni nuovi giocatori possono apparire sul mercato con un'efficiente gestione del trabocco; ma per ora x86 e ARM non mi interessa.

Gli ottimizzatori del compilatore non sono in grado di ottimizzare i controlli di overflow, o addirittura di ottimizzare in presenza di overflow. Alcuni accademici come John Regher si lamentano di questo statu quo , ma il fatto è che quando il semplice fatto di fare overflow " errori "previene le ottimizzazioni anche prima che l'assembly colpisca la CPU possa essere paralizzante. Soprattutto quando impedisce l'auto-vettorizzazione ...

Con effetti a cascata

Quindi, in assenza di strategie di ottimizzazione efficienti e di un supporto CPU efficiente, il controllo overflow è costoso. Molto più costoso del wrapping.

L'aggiunta di un comportamento fastidioso, come x + y - 1 può eccedere quando x - 1 + y non lo fa, il che può legittimamente infastidire gli utenti, e il controllo di overflow viene generalmente scartato a favore del wrapping (che gestisce questo esempio e molti altri con garbo ).

Tuttavia, non tutte le speranze sono perse

Nei compilatori clang e gcc è stato fatto uno sforzo per implementare "disinfettanti": modi per utilizzare strumenti binari per rilevare casi di comportamento indefinito. Quando si utilizza -fsanitize=undefined , viene rilevato un overflow con segno e il programma viene interrotto; molto utile durante i test.

Il Rust linguaggio di programmazione ha il controllo di overflow abilitato di default in modalità Debug (usa wrapping arithmetic in modalità Release per motivi di prestazioni).

Quindi, cresce la preoccupazione per il controllo eccessivo e i pericoli dei risultati falsi che non vengono rilevati, e si spera che questo a sua volta susciti interesse nella comunità dei ricercatori, nella comunità dei compilatori e nella comunità dell'hardware.

    
risposta data 08.05.2017 - 17:15
fonte
10

Le lingue che tentano di rilevare gli overflow hanno storicamente definito la semantica associata in modo da limitare severamente quelle che altrimenti sarebbero state utili ottimizzazioni. Tra le altre cose, mentre spesso è utile eseguire calcoli in una sequenza diversa da quella specificata nel codice, la maggior parte delle lingue che intrappolano gli overflow garantiscono un determinato codice come:

for (int i=0; i<100; i++)
{
  Operation1();
  x+=i;
  Operation2();
}

se il valore iniziale di x causerebbe un overflow sul 47 ° passare attraverso il ciclo, Operation1 eseguirà 47 volte e Operation2 eseguirà 46. In assenza di tale garanzia, se non altro all'interno del ciclo usa x, e niente utilizzerà il valore di x che segue un'eccezione generata da Operation1 o Operation2, il codice potrebbe essere sostituito con:

x+=4950;
for (int i=0; i<100; i++)
{
  Operation1();
  Operation2();
}

Sfortunatamente, l'esecuzione di tali ottimizzazioni pur garantendo una semantica corretta nei casi in cui si verificherebbe un overflow all'interno del ciclo difficile - in sostanza richiede qualcosa come:

if (x < INT_MAX-4950)
{
  x+=4950;
  for (int i=0; i<100; i++)
  {
    Operation1();
    Operation2();
  }
}
else
{
  for (int i=0; i<100; i++)
  {
    Operation1();
    x+=i;
    Operation2();
  }
}

Se si considera che un sacco di codice del mondo reale utilizza cicli che sono di più coinvolto, sarà ovvio che l'ottimizzazione del codice preservando la semantica di overflow è difficile. Inoltre, a causa dei problemi di memorizzazione nella cache, è del tutto possibile che l'aumento delle dimensioni del codice renderebbe il programma generale più lento anche se ci sono meno operazioni sul percorso comunemente eseguito.

Cosa sarebbe necessario per rendere economico il rilevamento di overflow a insieme definito di semantica di overflow-detection più lento che renderebbe facile per il codice riportare se un calcolo è stato eseguito senza overflow che avrebbe potuto influenzare i risultati (*), ma senza gravare sul compilatore con dettagli oltre a questo. Se le specifiche di una lingua si focalizzassero sulla riduzione del costo del rilevamento dell'overflow al minimo necessario per raggiungere quanto sopra, esso potrebbe essere reso molto meno costoso di quanto lo sia nelle lingue esistenti. Tuttavia, non sono a conoscenza di alcuno sforzo per facilitare il rilevamento di overflow efficiente.

(*) Se un linguaggio promette di riportare tutti gli overflow, un'espressione come x*y/y non può essere semplificata a x a meno che non si possa garantire che x*y non superi l'overflow. Allo stesso modo, anche se il risultato di un calcolo sarebbe ignorato, una lingua che promette di segnalare tutti gli overflow dovrà comunque eseguirla in modo che possa eseguire il controllo di overflow. Poiché l'overflow in tali casi non può comportare un comportamento aritmeticamente scorretto, un programma non dovrebbe eseguire tali controlli per garantire che nessun overflow abbia causato risultati potenzialmente inaccurati.

Per inciso, gli overflow in C sono particolarmente cattivi. Sebbene quasi tutte le piattaforme hardware che supportano C99 utilizzino la semantica del wrapping silenzioso a due complementi, è di moda che i compilatori moderni generino codice che potrebbe causare effetti collaterali arbitrari in caso di overflow. Ad esempio, dato qualcosa di simile:

#include <stdint.h>
uint32_t test(uint16_t x, uint16_t y) { return x*y & 65535u; }
uint32_t test2(uint16_t q, int *p)
{
  uint32_t total=0;
  q|=32768;
  for (int i = 32768; i<=q; i++)
  {
    total+=test(i,65535);
    *p+=1;
  }
  return total;
}

GCC genererà il codice per test2 che incrementa incondizionatamente (* p) una volta e restituisce 32768 indipendentemente dal valore passato in q. Con il suo ragionamento, il calcolo di (32769 * 65535) & 65535u causerebbe un overflow e quindi non è necessario che il compilatore consideri i casi in cui (q | 32768) produrrebbe un valore maggiore di 32768. Anche se non vi è alcun motivo per il calcolo di (32769 * 65535) & 65535u dovrebbe preoccuparsi dei bit superiori del risultato, gcc userà overflow firmato come giustificazione per ignorare il ciclo.

    
risposta data 09.05.2017 - 01:57
fonte
9

Non tutti i linguaggi di programmazione ignorano gli overflow integer. Alcune lingue forniscono operazioni integer sicure per tutti i numeri (la maggior parte dei dialoghi Lisp, Ruby, Smalltalk, ...) e altri tramite le librerie - ad esempio ci sono varie classi BigInt per C ++.

Se un linguaggio rende l'intero sicuro dall'overflow di default o meno dipende dal suo scopo: i linguaggi di sistema come C e C ++ devono fornire astrazioni a costo zero e il "grande intero" non è uno. I linguaggi di produttività, come Ruby, possono e forniscono grandi numeri interi fuori dalla scatola. Lingue come Java e C # che sono in qualche modo nel mezzo dovrebbero IMHO andare con gli interi sicuri fuori dalla scatola, non lo fanno.

    
risposta data 08.05.2017 - 15:23
fonte
7

Come hai mostrato, C # sarebbe stato 3 volte più lento se avesse i controlli di overflow abilitati di default (assumendo che il tuo esempio sia una tipica applicazione per quella lingua). Concordo sul fatto che le prestazioni non sono sempre la caratteristica più importante, ma le lingue / i compilatori vengono in genere confrontati con le loro prestazioni in attività tipiche. Ciò è dovuto in parte al fatto che la qualità delle caratteristiche linguistiche è in qualche modo soggettiva, mentre un test delle prestazioni è oggettivo.

Se dovessi introdurre una nuova lingua simile a C # nella maggior parte degli aspetti, ma 3 volte più lentamente, ottenere una quota di mercato non sarebbe facile, anche se alla fine la maggior parte degli utenti finali trarrebbe vantaggio dai controlli di overflow più di quanto lo sarebbero da prestazioni più elevate.

    
risposta data 08.05.2017 - 18:51
fonte
5

Oltre alle molte risposte che giustificano la mancanza di un controllo di overflow basato sulle prestazioni, ci sono due diversi tipi di aritmetica da considerare:

  1. calcoli di indicizzazione (indicizzazione dell'array e / o aritmetica del puntatore)

  2. altro aritmetico

Se la lingua utilizza una dimensione intera uguale alla dimensione del puntatore, allora un programma ben costruito non si riempirà eccessivamente di calcoli di indicizzazione perché dovrebbe necessariamente esaurire la memoria prima che i calcoli di indicizzazione causerebbero l'overflow.

Quindi, controllare le allocazioni di memoria è sufficiente quando si lavora con le espressioni aritmetiche e di indicizzazione del puntatore che coinvolgono strutture dati allocate. Ad esempio, se si dispone di uno spazio indirizzo a 32 bit e si utilizzano numeri interi a 32 bit e si consente di allocare un massimo di 2 GB di heap (circa metà dello spazio degli indirizzi), i calcoli di indicizzazione / puntatore (in pratica) non avranno un overflow.

Inoltre, potresti essere sorpreso di quanto di addizione / sottrazione / moltiplicazione implichi l'indicizzazione dell'array o il calcolo del puntatore, rientrando così nella prima categoria. Il puntatore dell'oggetto, l'accesso al campo e le manipolazioni dell'array sono operazioni di indicizzazione e molti programmi non eseguono più calcoli aritmetici di questi! Sostanzialmente, questo è il motivo principale per cui i programmi funzionano bene e senza il controllo di overflow di interi.

Tutti i calcoli non indicizzati e non puntatori devono essere classificati come quelli che vogliono / aspettano un overflow (ad esempio, calcoli di hashing) e quelli che non lo fanno (ad esempio l'esempio di sommatoria).

In quest'ultimo caso, i programmatori useranno spesso tipi di dati alternativi, come double o alcuni BigInt . Molti calcoli richiedono un tipo di dati decimal anziché double , ad es. calcoli finanziari. Se non lo fanno e si attaccano ai tipi interi, devono fare attenzione a controllare l'overflow dei numeri interi, altrimenti il programma può raggiungere una condizione di errore non rilevata mentre stai indicando.

Come programmatori, dobbiamo essere sensibili alle nostre scelte in tipi di dati numerici e alle loro conseguenze in termini di possibilità di overflow, per non parlare della precisione. In generale (e soprattutto quando si lavora con la famiglia di linguaggi C con il desiderio di utilizzare i tipi interi veloci) dobbiamo essere sensibili e consapevoli delle differenze tra i calcoli di indicizzazione rispetto ad altri.

    
risposta data 08.05.2017 - 18:37
fonte
3

La lingua Rust offre un interessante compromesso tra il controllo degli overflow e non, aggiungendo i controlli per eseguire il debug di build e rimuoverli nella versione di rilascio ottimizzata. Questo ti permette di trovare i bug durante i test, pur ottenendo prestazioni complete nella versione finale.

Poiché l'overflow wraparound è a volte un comportamento ricercato, ci sono anche versioni degli operatori che non controlla mai l'overflow.

Puoi leggere ulteriori informazioni sul ragionamento alla base della scelta nel RFC per il cambiamento. Ci sono anche molte informazioni interessanti in questo post del blog , incluso un elenco di bug che questa funzione ha aiutato con la cattura.

    
risposta data 08.05.2017 - 20:35
fonte
3

In Swift, qualsiasi overflow integer viene rilevato per impostazione predefinita e interrompe immediatamente il programma. Nei casi in cui è necessario un comportamento avvolgente, ci sono diversi operatori & +, & - e & * che lo raggiungono. E ci sono funzioni che eseguono un'operazione e dicono se c'è stato un overflow o meno.

È divertente vedere i principianti che provano a valutare la sequenza di Collatz e hanno il loro codice in crash: -)

Ora i progettisti di Swift sono anche i progettisti di LLVM e Clang, quindi sanno un po 'sull'ottimizzazione e sono in grado di evitare inutili controlli di overflow. Con tutte le ottimizzazioni abilitate, il controllo di overflow non aggiunge molto alla dimensione del codice e al tempo di esecuzione. E poiché la maggior parte degli overflow porta a risultati assolutamente errati, la dimensione del codice e il tempo di esecuzione sono ben spesi.

PS. In C, C ++, l'overflow aritmetico con numero intero con segno Objective-C è un comportamento non definito. Ciò significa che qualsiasi cosa faccia il compilatore nel caso di overflow di interi con segno è corretta, per definizione. I modi tipici per far fronte al sovraccarico di interi con segno è ignorarlo, prendendo qualsiasi risultato che la CPU ti dà, costruendo ipotesi nel compilatore che tale overflow non avverrà mai (e concludi per esempio che n + 1 > n è sempre vero, poiché overflow si presume che non accada mai), e una possibilità che viene usata raramente è quella di verificare e arrestare se si verifica un overflow, come fa Swift.

    
risposta data 09.05.2017 - 23:32
fonte
2

In realtà, la vera causa di ciò è puramente tecnico / storico: il segno ignora della CPU per la maggior parte. Di solito c'è solo una singola istruzione per aggiungere due interi nei registri, e la CPU non si cura un po 'se si interpretano questi due numeri interi come firmati o non firmati. Lo stesso vale per la sottrazione e persino per la moltiplicazione. L'unica operazione aritmetica che deve essere consapevole dei segni è la divisione.

Il motivo per cui questo funziona, è la rappresentazione del complemento a 2 degli interi con segno che viene utilizzata praticamente da tutte le CPU. Ad esempio, nel complemento a 4-bit 2 l'aggiunta di 5 e -3 ha il seguente aspetto:

  0101   (5)
  1101   (-3)
(11010)  (carry)
  ----
  0010   (2)

Osserva come il comportamento avvolgente di buttare via il bit di carry-out produce il risultato firmato corretto. Allo stesso modo, le CPU solitamente implementano la sottrazione x - y come x + ~y + 1 :

  0101   (5)
  1100   (~3, binary negation!)
(11011)  (carry, we carry in a 1 bit!)
  ----
  0010   (2)

Questo implementa la sottrazione come aggiunta nell'hardware, regolando solo gli input sull'unità aritmetica-logica (ALU) in modi banali. Cosa potrebbe essere più semplice?

Poiché la moltiplicazione non è altro che una sequenza di aggiunte, si comporta in modo altrettanto piacevole. Il risultato dell'utilizzo della rappresentazione del complemento a 2 e di ignorare l'esecuzione di operazioni aritmetiche è un circuito semplificato e serie di istruzioni semplificate.

Ovviamente, poiché C è stato progettato per funzionare vicino al metallo, ha adottato lo stesso identico comportamento del comportamento standardizzato dell'aritmetica senza segno, consentendo solo all'aritmetica firmata di produrre un comportamento non definito. E questa scelta è stata trasferita ad altri linguaggi come Java e, ovviamente, C #.

    
risposta data 10.05.2017 - 17:48
fonte
1

Alcune risposte hanno discusso il costo del controllo e hai modificato la tua risposta per contestare che questa sia una giustificazione ragionevole. Proverò ad affrontare questi punti.

In C e C ++ (come esempi), uno dei principi di progettazione delle lingue non è quello di fornire funzionalità che non sono state richieste. Questo è comunemente riassunto dalla frase "non pagare per ciò che non si usa". Se il programmatore vuole controllare l'overflow, allora può chiedere (e pagare la penalità). Questo rende la lingua più pericolosa da usare, ma tu scegli di lavorare con la lingua sapendo che, quindi accetti il rischio. Se non si desidera tale rischio o se si scrive codice in cui la sicurezza è di fondamentale importanza, è possibile selezionare una lingua più appropriata in cui il compromesso tra prestazioni e rischio è diverso.

But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.

Ci sono alcune cose sbagliate in questo ragionamento:

  1. Questo è specifico per l'ambiente. In genere, ha molto poco senso citare cifre specifiche come questa, perché il codice è scritto per tutti i tipi di ambienti che variano in base agli ordini di grandezza in termini di prestazioni. Il tuo 1 nanosecondo su una macchina desktop (presumo) potrebbe sembrare incredibilmente veloce per qualcuno che codifica per un ambiente embedded, e insopportabilmente lento a qualcuno che codifica per un cluster di super computer.

  2. 1 nanosecondo potrebbe sembrare nulla per un segmento di codice che viene eseguito di rado. D'altra parte, se si trova in un ciclo interno di alcuni calcoli che è la funzione principale del codice, allora ogni singola frazione di tempo che è possibile radere può fare una grande differenza. Se stai eseguendo una simulazione su un cluster, quelle frazioni salvate di un nanosecondo nel tuo ciclo interno possono tradursi direttamente in denaro speso per hardware ed elettricità.

  3. Per alcuni algoritmi e contesti, 10.000.000.000 di iterazioni potrebbero essere insignificanti. Di nuovo, in genere non ha senso parlare di scenari specifici che si applicano solo in determinati contesti.

There may be situation where that is important, but for most applications, that won't matter.

Forse hai ragione. Ma ancora una volta, si tratta di quali sono gli obiettivi di una determinata lingua. Molte lingue sono infatti progettate per soddisfare le esigenze del "maggior" o per favorire la sicurezza rispetto ad altre preoccupazioni. Altri, come C e C ++, danno priorità all'efficienza. In quel contesto, far pagare a tutti una penalità per le prestazioni semplicemente perché la maggior parte delle persone non si preoccupa, va contro ciò che la lingua sta cercando di ottenere.

    
risposta data 10.05.2017 - 22:36
fonte
-1

Ci sono buone risposte, ma penso che ci sia un punto mancato qui: gli effetti di un overflow di interi non sono necessariamente una cosa negativa, e dopo il fatto è difficile sapere se i sia passato da MAX_INT ad essere MIN_INT era dovuto a un problema di overflow o se era intenzionalmente fatto moltiplicando per -1.

Ad esempio, se voglio aggiungere tutti gli interi rappresentabili maggiori di 0 insieme, utilizzerei solo un ciclo di aggiunta for(i=0;i>=0;++i){...} - e quando si trabocca si ferma l'addizione, che è il comportamento dell'obiettivo (lanciare un errore significherebbe che devo eludere una protezione arbitraria perché interferisce con l'aritmetica standard). È una cattiva pratica limitare l'aritmetica primitiva, perché:

  • Sono usati in tutto - un rallentamento della matematica primitiva è un rallentamento in ogni programma funzionante
  • Se un programmatore ha bisogno di loro, può sempre aggiungerli
  • Se li hai e il programmatore non ne ha bisogno (ma ha bisogno di runtime più veloci), non possono rimuoverli facilmente per l'ottimizzazione
  • Se li hai e il programmatore ha bisogno che loro non siano lì (come nell'esempio sopra), il programmatore sta entrambi prendendo il successo run-time (che può essere o non essere rilevante) e il programmatore deve ancora investire tempo per rimuovere o aggirare la 'protezione'.
risposta data 08.05.2017 - 17:42
fonte

Leggi altre domande sui tag