it-swarm.com.de

Warum wird der arithmetische Überlauf ignoriert?

Haben Sie jemals versucht, alle Zahlen von 1 bis 2.000.000 in Ihrer bevorzugten Programmiersprache zusammenzufassen? Das Ergebnis ist einfach manuell zu berechnen: 2.000.001.000.000, was etwa 900-mal größer ist als der Maximalwert einer vorzeichenlosen 32-Bit-Ganzzahl.

C # druckt -1453759936 Aus - ein negativer Wert! Und ich denke Java macht das gleiche.

Das bedeutet, dass es einige gängige Programmiersprachen gibt, die den arithmetischen Überlauf standardmäßig ignorieren (in C # gibt es versteckte Optionen, um dies zu ändern). Das ist ein Verhalten, das für mich sehr riskant erscheint, und wurde der Absturz von Ariane 5 nicht durch einen solchen Überlauf verursacht?

Also: Was sind die Designentscheidungen hinter solch einem gefährlichen Verhalten?

Bearbeiten:

Die ersten Antworten auf diese Frage drücken die übermäßigen Kosten für die Überprüfung aus. Lassen Sie uns ein kurzes C # -Programm ausführen, um diese Annahme zu testen:

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);

Auf meinem Computer dauert die aktivierte Version 11015 ms, während die nicht aktivierte Version 4125 ms dauert. Das heißt, Die Überprüfungsschritte dauern fast doppelt so lange wie das Hinzufügen der Zahlen (insgesamt dreimal so viel wie die ursprüngliche Zeit). Bei den 10.000.000.000 Wiederholungen beträgt die für eine Überprüfung benötigte Zeit jedoch immer noch weniger als 1 Nanosekunde. Es kann Situationen geben, in denen dies wichtig ist, aber für die meisten Anwendungen spielt dies keine Rolle.

Bearbeiten 2:

Ich habe unsere Serveranwendung (ein Windows-Dienst, der Daten analysiert, die von mehreren Sensoren empfangen wurden, wobei einige Zahlen verarbeitet wurden) mit dem Parameter /p:CheckForOverflowUnderflow="false" Neu kompiliert (normalerweise schalte ich die Überlaufprüfung ein) und sie auf einem Gerät bereitgestellt. Die Nagios-Überwachung zeigt, dass die durchschnittliche CPU-Auslastung bei 17% blieb.

Dies bedeutet, dass der im obigen Beispiel gefundene Leistungseinbruch für unsere Anwendung völlig irrelevant ist.

77
Bernhard Hiller

Dafür gibt es 3 Gründe:

  1. Die Kosten für die Überprüfung auf Überläufe (für jede einzelne arithmetische Operation) zur Laufzeit sind zu hoch.

  2. Die Komplexität des Nachweises, dass eine Überlaufprüfung zur Kompilierungszeit weggelassen werden kann, ist zu hoch.

  3. In einigen Fällen (z. B. CRC-Berechnungen, Bibliotheken mit großen Zahlen usw.) ist "Wrap-on-Overflow" für Programmierer bequemer.

86
Brendan

Wer sagt, dass es ein schlechter Kompromiss ist?!

Ich führe alle meine Produktions-Apps mit aktivierter Überlaufprüfung aus. Dies ist eine C # -Compileroption. Ich habe dies tatsächlich verglichen und konnte den Unterschied nicht feststellen. Die Kosten für den Zugriff auf die Datenbank zum Generieren von HTML (ohne Spielzeug) überschatten die Kosten für die Überlaufprüfung.

Ich schätze die Tatsache, dass ich weiß, dass in der Produktion keine Vorgänge überlaufen. Fast der gesamte Code würde sich bei Überläufen unregelmäßig verhalten. Die Käfer wären nicht gutartig. Datenkorruption ist wahrscheinlich, Sicherheitsprobleme eine Möglichkeit.

Wenn ich die Leistung benötige, was manchmal der Fall ist, deaktiviere ich die Überlaufprüfung mit unchecked {} Auf granularer Basis. Wenn ich darauf hinweisen möchte, dass ich mich auf eine nicht überlaufende Operation verlasse, füge ich dem Code möglicherweise redundant checked {} Hinzu, um diese Tatsache zu dokumentieren. Ich bin mir der Überläufe bewusst, aber ich muss nicht unbedingt dank der Überprüfung sein.

Ich glaube, das C # -Team hat die falsche Wahl getroffen, als es sich entschieden hat, nicht Überlauf standardmäßig zu überprüfen, aber diese Wahl ist jetzt aufgrund starker Kompatibilitätsbedenken versiegelt. Beachten Sie, dass diese Wahl um das Jahr 2000 getroffen wurde. Hardware war weniger leistungsfähig und .NET hatte noch nicht viel Traktion. Vielleicht wollte .NET auf diese Weise Java und C/C++ - Programmierer ansprechen. .NET soll auch in der Lage sein, nah am Metall zu sein. Deshalb hat es unsicheren Code, Strukturen und großartige native Anruffähigkeiten, die alle Java nicht haben).

Je schneller unsere Hardware wird und je intelligenter die Compiler sind, desto attraktiver ist standardmäßig die Überlaufprüfung.

Ich glaube auch, dass die Überlaufprüfung oft besser ist als unendlich große Zahlen. Zahlen mit unendlicher Größe haben noch höhere Leistungskosten, sind (glaube ich) schwerer zu optimieren und eröffnen die Möglichkeit eines unbegrenzten Ressourcenverbrauchs.

Die Art und Weise, wie JavaScript mit Überlauf umgeht, ist noch schlimmer. JavaScript-Zahlen sind Gleitkomma-Doppel. Ein "Überlauf" manifestiert sich darin, dass der vollständig genaue Satz von ganzen Zahlen verlassen wird. Etwas Es treten falsche Ergebnisse auf (z. B. um eins ausgeschaltet - dies kann endliche Schleifen in unendliche verwandeln).

Für einige Sprachen wie C/C++ ist die Überlaufprüfung standardmäßig eindeutig ungeeignet, da die Arten von Anwendungen, die in diesen Sprachen geschrieben werden, eine Bare-Metal-Leistung erfordern. Es gibt jedoch Bemühungen, C/C++ zu einer sichereren Sprache zu machen, indem opt in in einen sichereren Modus versetzt wird. Dies ist lobenswert, da 90-99% des Codes kalt sind. Ein Beispiel ist die Compileroption fwrapv, die das Komplement-Wrapping von 2 erzwingt. Dies ist eine Funktion zur "Qualität der Implementierung" des Compilers, nicht der Sprache.

Haskell hat keinen logischen Aufrufstapel und keine angegebene Auswertungsreihenfolge. Dies führt dazu, dass Ausnahmen an unvorhersehbaren Punkten auftreten. In a + b Ist nicht angegeben, ob a oder b zuerst ausgewertet wird und ob diese Ausdrücke überhaupt enden oder nicht. Daher ist es für Haskell sinnvoll, die meiste Zeit unbegrenzte Ganzzahlen zu verwenden. Diese Auswahl eignet sich für eine rein funktionale Sprache, da Ausnahmen in den meisten Haskell-Codes wirklich unangemessen sind. Und die Division durch Null ist in der Tat ein problematischer Punkt im Sprachdesign von Haskells. Anstelle von unbegrenzten Ganzzahlen hätten sie auch Ganzzahlen mit fester Breite verwenden können, aber das passt nicht zum Thema "Fokus auf Korrektheit", das in der Sprache enthalten ist.

Eine Alternative zu Überlaufausnahmen sind Giftwerte, die durch undefinierte Operationen erstellt werden und sich durch Operationen verbreiten (wie der float NaN -Wert). Das scheint weitaus teurer zu sein als die Überlaufprüfung und verlangsamt alle Vorgänge, nicht nur diejenigen, die fehlschlagen können (abgesehen von Hardwarebeschleunigungen, die normalerweise schweben, und Ints, die normalerweise nicht vorhanden sind - obwohl Itanium hat NaT, was "Not a Thing" ist) " ). Ich sehe auch nicht ganz den Sinn, das Programm dazu zu bringen, zusammen mit schlechten Daten weiter zu hinken. Es ist wie ON ERROR RESUME NEXT. Es verbirgt Fehler, hilft aber nicht, korrekte Ergebnisse zu erzielen. supercat weist darauf hin, dass dies manchmal eine Leistungsoptimierung ist.

64
usr

Weil es ein schlechter Kompromiss ist, alle Berechnungen viel teurer zu machen, um automatisch den seltenen Fall zu erfassen, dass ein Überlauf tut auftritt. Es ist viel besser, den Programmierer damit zu belasten, die seltenen Fälle zu erkennen, in denen dies ein Problem darstellt, und besondere Vorsichtsmaßnahmen hinzuzufügen, als alle Programmierer den Preis für Funktionen zahlen zu lassen, die sie nicht verwenden.

30
Kilian Foth

was sind die Entwurfsentscheidungen hinter solch einem gefährlichen Verhalten?

"Zwingen Sie Benutzer nicht, eine Leistungsstrafe für eine Funktion zu zahlen, die sie möglicherweise nicht benötigen."

Es ist einer der grundlegendsten Grundsätze im Design von C und C++ und stammt aus einer anderen Zeit, als Sie lächerliche Verrenkungen durchlaufen mussten, um für Aufgaben, die heute als trivial gelten, kaum ausreichende Leistung zu erzielen.

Neuere Sprachen brechen mit dieser Einstellung für viele andere Funktionen, wie z. B. die Überprüfung von Array-Grenzen. Ich bin mir nicht sicher, warum sie es nicht für die Überlaufprüfung getan haben. es könnte einfach ein Versehen sein.

20

Vermächtnis

Ich würde sagen, dass das Problem wahrscheinlich im Erbe begründet ist. In C:

  • der signierte Überlauf ist ein undefiniertes Verhalten (Compiler unterstützen Flags, damit er umbrochen wird).
  • vorzeichenloser Überlauf ist definiertes Verhalten (es wird umbrochen).

Dies wurde durchgeführt, um die bestmögliche Leistung zu erzielen, und zwar nach dem Prinzip, dass der Programmierer weiß, was er tut.

Führt zu Statu-Quo

Die Tatsache, dass C (und damit auch C++) nicht abwechselnd die Erkennung eines Überlaufs erfordert, bedeutet, dass die Überprüfung des Überlaufs nur schleppend erfolgt.

Hardware ist hauptsächlich für C/C++ geeignet (im Ernst, x86 hat eine strcmp Anweisung (auch bekannt als [~ # ~] pcmpistri [~ # ~] ab SSE 4.2)!) Und da es C egal ist, bieten gängige CPUs keine effizienten Möglichkeiten zur Erkennung von Überläufen. In x86 müssen Sie nach jedem potenziell überfüllten Vorgang ein Pro-Core-Flag überprüfen. wenn Sie wirklich eine "verdorbene" Flagge auf dem Ergebnis haben möchten (ähnlich wie sich NaN ausbreitet). Und Vektoroperationen können noch problematischer sein. Einige neue Spieler erscheinen möglicherweise mit effizienter Überlaufbehandlung auf dem Markt. aber im Moment ist es x86 und ARM egal.

Compiler-Optimierer sind nicht gut darin, Überlaufprüfungen oder sogar Überläufe zu optimieren. Einige Wissenschaftler wie John Regher beschweren sich über dieses Statu-Quo , aber Tatsache ist, dass wenn die einfache Tatsache, Überläufe zu "Fehlern" zu machen, Optimierungen verhindert, noch bevor die Assembly die CPU erreicht lähmend sein. Besonders wenn es die automatische Vektorisierung verhindert ...

Mit Kaskadeneffekten

In Ermangelung effizienter Optimierungsstrategien und effizienter CPU-Unterstützung ist die Überlaufprüfung daher kostspielig. Viel teurer als das Verpacken.

Fügen Sie ein störendes Verhalten hinzu, z. B. x + y - 1 Kann überlaufen, wenn x - 1 + y Nicht überläuft, was die Benutzer zu Recht stören kann, und die Überlaufprüfung wird im Allgemeinen zugunsten des Umbruchs verworfen (dies behandelt dieses Beispiel und viele andere anmutig).

Dennoch ist nicht alle Hoffnung verloren

Die Clang- und GCC-Compiler haben sich bemüht, "Desinfektionsmittel" zu implementieren: Möglichkeiten, Binärdateien zu instrumentieren, um Fälle von undefiniertem Verhalten zu erkennen. Bei Verwendung von -fsanitize=undefined Wird ein signierter Überlauf erkannt und das Programm abgebrochen. sehr nützlich beim Testen.

In der Programmiersprache Rust ist die Überlaufprüfung aktiviert. standardmäßig im Debug-Modus (im Release-Modus wird die Umbrucharithmetik für verwendet Leistungsgründe).

Es besteht also wachsende Besorgnis darüber, dass Überlaufprüfungen und die Gefahren von falschen Ergebnissen unentdeckt bleiben, und dies wird hoffentlich wiederum spark Interesse an der Forschungsgemeinschaft, der Compilergemeinschaft und der Hardwaregemeinschaft wecken.

19
Matthieu M.

Sprachen, die versuchen, Überläufe zu erkennen, haben die zugehörige Semantik in der Vergangenheit so definiert, dass die ansonsten nützlichen Optimierungen stark eingeschränkt wurden. Während es oft nützlich ist, Berechnungen in einer anderen Reihenfolge als der im Code angegebenen durchzuführen, garantieren die meisten Sprachen, die Überläufe abfangen, diesen gegebenen Code wie:

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

wenn der Startwert von x beim 47. Durchgang durch die Schleife zu einem Überlauf führen würde, wird Operation1 47 Mal und Operation2 46 Mal ausgeführt. Ohne eine solche Garantie verwendet x nichts in der Schleife und nichts verwendet den Wert von x nach einer ausgelösten Ausnahme durch Operation1 oder Operation2, Code könnte ersetzt werden durch:

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

Leider ist es schwierig, solche Optimierungen durchzuführen und gleichzeitig die korrekte Semantik in Fällen zu gewährleisten, in denen ein Überlauf innerhalb der Schleife aufgetreten wäre. Dies erfordert im Wesentlichen Folgendes:

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();
  }
}

Wenn man bedenkt, dass viele reale Codes Schleifen verwenden, die stärker involviert sind, wird es offensichtlich sein, dass die Optimierung des Codes unter Beibehaltung der Überlaufsemantik schwierig ist. Aufgrund von Caching-Problemen ist es außerdem durchaus möglich, dass durch die Erhöhung der Codegröße das Gesamtprogramm langsamer ausgeführt wird, obwohl weniger Operationen auf dem häufig ausgeführten Pfad ausgeführt werden.

Was erforderlich wäre, um die Überlauferkennung kostengünstig zu machen, wäre ein definierter Satz einer lockeren Überlauferkennungssemantik, die es dem Code erleichtern würde, zu melden, ob eine Berechnung ohne Überläufe durchgeführt wurde, die die Ergebnisse beeinflusst haben könnten (*), jedoch ohne Belastung der Compiler mit Details darüber hinaus. Wenn sich eine Sprachspezifikation darauf konzentrieren würde, die Kosten für die Überlauferkennung auf ein Minimum zu reduzieren, das zur Erreichung des oben genannten Ziels erforderlich ist, könnte dies wesentlich kostengünstiger sein als in vorhandenen Sprachen. Ich bin mir jedoch keiner Bemühungen bewusst, eine effiziente Überlauferkennung zu ermöglichen.

(*) Wenn eine Sprache verspricht, dass alle Überläufe gemeldet werden, dann ein Ausdruck wie x*y/y kann nicht zu x vereinfacht werden, es sei denn x*y kann garantiert werden, dass es nicht überläuft. Selbst wenn das Ergebnis einer Berechnung ignoriert würde, muss eine Sprache, die verspricht, alle Überläufe zu melden, diese trotzdem ausführen, damit sie die Überlaufprüfung durchführen kann. Da ein Überlauf in solchen Fällen nicht zu einem arithmetisch inkorrekten Verhalten führen kann, müsste ein Programm solche Überprüfungen nicht durchführen, um sicherzustellen, dass keine Überläufe zu möglicherweise ungenauen Ergebnissen geführt haben.

Überläufe in C sind übrigens besonders schlimm. Obwohl fast jede Hardwareplattform, die C99 unterstützt, eine Silent-Wraparound-Semantik mit zwei Komplementen verwendet, ist es für moderne Compiler in Mode, Code zu generieren, der im Falle eines Überlaufs beliebige Nebenwirkungen verursachen kann. Zum Beispiel gegeben etwas wie:

#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 generiert Code für test2, der (* p) bedingungslos einmal erhöht und 32768 zurückgibt, unabhängig von dem an q übergebenen Wert. Nach seiner Überlegung würde die Berechnung von (32769 * 65535) & 65535u einen Überlauf verursachen, und der Compiler muss daher keine Fälle berücksichtigen, in denen (q | 32768) einen Wert größer als 32768 ergeben würde Da sich bei der Berechnung von (32769 * 65535) & 65535u die oberen Bits des Ergebnisses berücksichtigen sollten, verwendet gcc einen vorzeichenbehafteten Überlauf als Rechtfertigung für das Ignorieren der Schleife.

10
supercat

Nicht alle Programmiersprachen ignorieren Ganzzahlüberläufe. Einige Sprachen bieten sichere Ganzzahloperationen für alle Zahlen (die meisten LISP-Dialekte, Ruby, Smalltalk, ...) und andere über Bibliotheken - zum Beispiel gibt es verschiedene BigInt-Klassen für C++.

Ob eine Sprache die Ganzzahl standardmäßig vor Überlauf schützt oder nicht, hängt von ihrem Zweck ab: Systemsprachen wie C und C++ müssen kostengünstige Abstraktionen bereitstellen, und "große Ganzzahl" ist keine. Produktivitätssprachen wie Ruby können und liefern sofort große Ganzzahlen. Sprachen wie Java und C #, die irgendwo dazwischen liegen, sollten IMHO mit den sicheren Ganzzahlen aus der Box gehen, da sie dies nicht tun.

9

Wie Sie gezeigt haben, wäre C # dreimal langsamer gewesen, wenn die Überlaufprüfungen standardmäßig aktiviert wären (vorausgesetzt, Ihr Beispiel ist eine typische Anwendung für diese Sprache). Ich bin damit einverstanden, dass die Leistung nicht immer das wichtigste Merkmal ist, aber Sprachen/Compiler werden in der Regel bei typischen Aufgaben auf ihre Leistung verglichen. Dies ist teilweise auf die Tatsache zurückzuführen, dass die Qualität der Sprachmerkmale etwas subjektiv ist, während ein Leistungstest objektiv ist.

Wenn Sie eine neue Sprache einführen würden, die in den meisten Aspekten C # ähnelt, aber dreimal langsamer ist, wäre es nicht einfach, einen Marktanteil zu erhalten, selbst wenn die meisten Ihrer Endbenutzer am Ende mehr von Überlaufprüfungen profitieren würden als sie von höherer Leistung.

7

Abgesehen von den vielen Antworten, die eine fehlende Überlaufprüfung auf der Grundlage der Leistung rechtfertigen, sind zwei verschiedene Arten von Arithmetik zu berücksichtigen:

  1. indizierungsberechnungen (Array-Indizierung und/oder Zeigerarithmetik)

  2. andere Arithmetik

Wenn die Sprache eine Ganzzahlgröße verwendet, die der Zeigergröße entspricht, läuft ein gut aufgebautes Programm bei Indexierungsberechnungen nicht über, da es notwendigerweise nicht genügend Speicher haben muss, bevor die Indexierungsberechnungen einen Überlauf verursachen würden.

Daher ist die Überprüfung der Speicherzuordnungen ausreichend, wenn mit Zeigerarithmetik und Indizierungsausdrücken mit zugewiesenen Datenstrukturen gearbeitet wird. Wenn Sie beispielsweise über einen 32-Bit-Adressraum verfügen und 32-Bit-Ganzzahlen verwenden und maximal 2 GB Heap zuweisen (etwa die Hälfte des Adressraums), werden Indexierungs-/Zeigerberechnungen (im Grunde genommen) nicht überlaufen.

Darüber hinaus könnten Sie überrascht sein, wie viel Addition/Subtraktion/Multiplikation Array-Indizierung oder Zeigerberechnung beinhaltet und somit in die erste Kategorie fällt. Objektzeiger-, Feldzugriffs- und Array-Manipulationen sind Indizierungsoperationen, und viele Programme führen keine arithmetischeren Berechnungen durch als diese! Dies ist im Wesentlichen der Hauptgrund dafür, dass Programme so gut funktionieren wie ohne Prüfung auf Ganzzahlüberlauf.

Alle Nicht-Indizierungs- und Nicht-Zeiger-Berechnungen sollten entweder als solche klassifiziert werden, die einen Überlauf wünschen/erwarten (z. B. Hashing-Berechnungen), oder als solche, die dies nicht tun (z. B. Ihr Summationsbeispiel).

Im letzteren Fall verwenden Programmierer häufig alternative Datentypen wie double oder einige BigInt. Viele Berechnungen erfordern einen decimal -Datentyp anstelle von double, z. finanzielle Berechnungen. Wenn dies nicht der Fall ist und sich an ganzzahlige Typen halten, müssen sie darauf achten, dass ein ganzzahliger Überlauf vorliegt. Andernfalls kann das Programm einen unerkannten Fehlerzustand erreichen, wie Sie hervorheben.

Als Programmierer müssen wir uns unserer Auswahl an numerischen Datentypen und deren Konsequenzen hinsichtlich der Möglichkeiten eines Überlaufs bewusst sein, ganz zu schweigen von der Präzision. Im Allgemeinen (und insbesondere bei der Arbeit mit der C-Sprachfamilie mit dem Wunsch, die schnellen Ganzzahltypen zu verwenden) müssen wir die Unterschiede zwischen Indexierungsberechnungen und anderen berücksichtigen und berücksichtigen.

5
Erik Eidt

In Swift werden standardmäßig alle Ganzzahlüberläufe erkannt und das Programm sofort gestoppt. In Fällen, in denen Sie ein Rundum-Verhalten benötigen, gibt es verschiedene Operatoren & +, & - und & *, die dies erreichen. Und es gibt Funktionen, die eine Operation ausführen und feststellen, ob ein Überlauf aufgetreten ist oder nicht.

Es macht Spaß zu sehen, wie Anfänger versuchen, die Collatz-Sequenz zu bewerten und ihren Code zum Absturz zu bringen :-)

Jetzt sind die Designer von Swift auch die Designer von LLVM und Clang, sodass sie ein oder zwei Kenntnisse über die Optimierung haben und in der Lage sind, unnötige Überlaufprüfungen zu vermeiden. Wenn alle Optimierungen aktiviert sind, können Überlaufprüfungen durchgeführt werden erhöht nicht viel zur Codegröße und Ausführungszeit. Und da die meisten Überläufe zu absolut falschen Ergebnissen führen, sind Codegröße und Ausführungszeit gut angelegt.

PS. In C, C++ ist der arithmetische Überlauf von Objective-C mit Vorzeichen ein undefiniertes Verhalten. Das bedeutet, dass alles, was der Compiler im Falle eines vorzeichenbehafteten Ganzzahlüberlaufs tut, per Definition korrekt ist. Typische Möglichkeiten, mit einem vorzeichenbehafteten Ganzzahlüberlauf umzugehen, bestehen darin, ihn zu ignorieren, das von der CPU angegebene Ergebnis zu übernehmen und im Compiler Annahmen zu treffen, dass ein solcher Überlauf niemals auftreten wird (und beispielsweise zu schließen, dass n + 1> n immer wahr ist, da ein Überlauf vorliegt Es wird angenommen, dass dies niemals geschieht. Eine Möglichkeit, die nur selten genutzt wird, besteht darin, zu überprüfen und abzustürzen, ob ein Überlauf auftritt, wie dies bei Swift) der Fall ist.

4
gnasher729

Die Sprache Rust bietet einen interessanten Kompromiss zwischen der Überprüfung auf Überläufe und nicht, indem die Überprüfungen für den Debugging-Build hinzugefügt und in der optimierten Release-Version entfernt werden. Auf diese Weise können Sie die Fehler während des Testens finden, während Sie in der endgültigen Version immer noch die volle Leistung erhalten.

Da das Überlauf-Wraparound manchmal gewünscht wird, gibt es auch Versionen der Operatoren , die niemals nach Überlauf suchen.

Weitere Informationen zu den Gründen für die Auswahl finden Sie im RFC für die Änderung. Es gibt auch viele interessante Informationen in dieser Blog-Beitrag , einschließlich einer Liste der Fehler , die diese Funktion beim Abfangen geholfen hat.

3
Hjulle

Tatsächlich ist die eigentliche Ursache dafür rein technisch/historisch: CPUs ignorieren Zeichen zum größten Teil. Im Allgemeinen gibt es nur einen einzigen Befehl zum Hinzufügen von zwei Ganzzahlen in Registern, und der CPU ist es egal, ob Sie diese beiden Ganzzahlen als vorzeichenbehaftet oder vorzeichenlos interpretieren. Gleiches gilt für die Subtraktion und sogar für die Multiplikation. Die einzige arithmetische Operation, die vorzeichenbewusst sein muss, ist die Division.

Der Grund, warum dies funktioniert, ist die 2-Komplement-Darstellung von vorzeichenbehafteten Ganzzahlen, die von praktisch allen CPUs verwendet wird. Zum Beispiel sieht in 4-Bit-2-Komplementen die Addition von 5 und -3 folgendermaßen aus:

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

Beobachten Sie, wie das Wrap-Around-Verhalten beim Wegwerfen des Übertragsbits das richtige vorzeichenbehaftete Ergebnis liefert. Ebenso implementieren CPUs normalerweise die Subtraktion x - y wie x + ~y + 1:

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

Dies implementiert die Subtraktion als Addition in die Hardware und optimiert nur die Eingaben in die arithmetisch-logische Einheit (ALU) auf triviale Weise. Was könnte einfacher sein?

Da die Multiplikation nichts anderes als eine Folge von Additionen ist, verhält sie sich ähnlich nett. Das Ergebnis der Verwendung der Komplementdarstellung von 2 und des Ignorierens der Ausführung von arithmetischen Operationen ist eine vereinfachte Schaltung und vereinfachte Befehlssätze.

Da C so konzipiert wurde, dass es nahe am Metall arbeitet, übernahm es offensichtlich genau das gleiche Verhalten wie das standardisierte Verhalten der vorzeichenlosen Arithmetik, sodass nur vorzeichenbehaftete Arithmetik ein undefiniertes Verhalten ergeben kann. Und diese Wahl wurde auf andere Sprachen wie Java und natürlich C # übertragen.

In einigen Antworten wurden die Kosten für die Überprüfung erörtert, und Sie haben Ihre Antwort bearbeitet, um zu bestreiten, dass dies eine vernünftige Rechtfertigung ist. Ich werde versuchen, diese Punkte anzusprechen.

In C und C++ (als Beispiele) besteht eines der Prinzipien des Sprachenentwurfs darin, keine Funktionen bereitzustellen, nach denen nicht gefragt wurde. Dies wird üblicherweise mit dem Satz "Zahlen Sie nicht für das, was Sie nicht verwenden" zusammengefasst. Wenn der Programmierer eine Überlaufprüfung wünscht, kann er danach fragen (und die Strafe bezahlen). Dies macht die Verwendung der Sprache gefährlicher, aber Sie entscheiden sich dafür, mit der Sprache zu arbeiten, die dies weiß, und akzeptieren das Risiko. Wenn Sie dieses Risiko nicht möchten oder wenn Sie Code schreiben, bei dem die Sicherheit von größter Bedeutung ist, können Sie eine geeignetere Sprache auswählen, bei der der Kompromiss zwischen Leistung und Risiko unterschiedlich ist.

Bei den 10.000.000.000 Wiederholungen beträgt die für eine Überprüfung benötigte Zeit jedoch immer noch weniger als 1 Nanosekunde.

Mit dieser Argumentation sind einige Dinge falsch:

  1. Dies ist umgebungsspezifisch. Es ist im Allgemeinen wenig sinnvoll, bestimmte Zahlen wie diese zu zitieren, da Code für alle Arten von Umgebungen geschrieben wird, die sich hinsichtlich ihrer Leistung um Größenordnungen unterscheiden. Ihre 1 Nanosekunde auf einem (ich nehme an) Desktop-Computer scheint für jemanden, der für eine eingebettete Umgebung codiert, erstaunlich schnell und für jemanden, der für einen Supercomputer-Cluster codiert, unerträglich langsam zu sein.

  2. 1 Nanosekunde scheint für ein Codesegment, das selten ausgeführt wird, nichts zu sein. Wenn es sich jedoch in einer inneren Schleife einer Berechnung befindet, die die Hauptfunktion des Codes darstellt, kann jeder einzelne Bruchteil der Zeit, den Sie rasieren können, einen großen Unterschied bewirken. Wenn Sie eine Simulation in einem Cluster ausführen, können diese gespeicherten Bruchteile einer Nanosekunde in Ihrer inneren Schleife direkt in Geld umgewandelt werden, das für Hardware und Strom ausgegeben wird.

  3. Für einige Algorithmen und Kontexte können 10.000.000.000 Iterationen unbedeutend sein. Auch hier ist es im Allgemeinen nicht sinnvoll, über bestimmte Szenarien zu sprechen, die nur in bestimmten Kontexten gelten.

Es kann Situationen geben, in denen dies wichtig ist, aber für die meisten Anwendungen spielt dies keine Rolle.

Vielleicht hast du recht. Aber auch dies ist eine Frage der Ziele einer bestimmten Sprache. Viele Sprachen sind in der Tat so konzipiert, dass sie den Bedürfnissen der "meisten" gerecht werden oder die Sicherheit anderen Anliegen vorziehen. Andere, wie C und C++, legen Wert auf Effizienz. In diesem Zusammenhang widerspricht es dem, was die Sprache erreichen will, wenn jeder eine Leistungsstrafe zahlt, nur weil die meisten Menschen nicht gestört werden.

1
Jon Bentley