Haben Sie jemals versucht, alle Zahlen von 1 bis 2.000.000 in Ihrer bevorzugten Programmiersprache zusammenzufassen? Das Ergebnis kann leicht manuell berechnet werden: 2.000.001.000.000, was etwa 900-mal größer ist als der Maximalwert einer 32-Bit-Ganzzahl ohne Vorzeichen.
C # druckt aus -1453759936
- 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 aussieht, und wurde der Absturz von Ariane 5 nicht durch einen solchen Überlauf verursacht?
Also: Was sind die Entwurfsentscheidungen hinter solch einem gefährlichen Verhalten?
Bearbeiten:
Die ersten Antworten auf diese Frage drücken die übermäßigen Überprüfungskosten aus. Führen wir ein kurzes C # -Programm aus, 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 das Dreifache der ursprünglichen Zeit). Bei den 10.000.000.000 Wiederholungen beträgt der Zeitaufwand für eine Überprüfung 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, einige davon sind auffällig) mit dem /p:CheckForOverflowUnderflow="false"
Parameter neu kompiliert (normalerweise schalte ich die Überlaufprüfung ein) und auf einem Gerät implementiert. Die Nagios-Überwachung zeigt, dass die durchschnittliche CPU-Auslastung bei 17% blieb.
Dies bedeutet, dass der im obigen Beispiel festgestellte Leistungstreffer für unsere Anwendung völlig irrelevant ist.
quelle
checked { }
Abschnitt verwenden, um die Teile des Codes zu markieren, die arithmetische Überlaufprüfungen durchführen sollen. Dies ist auf die Leistung zurückzuführen(1..2_000_000).sum #=> 2000001000000
. Eine andere meiner Lieblings Sprachen:sum [1 .. 2000000] --=> 2000001000000
. Nicht mein Favorit:Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000
. (Um fair zu sein, der letzte betrügt.)Integer
in Haskell ist willkürlich genau. Es kann eine beliebige Zahl gespeichert werden, solange Ihnen nicht der zuweisbare Arbeitsspeicher ausgeht.But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.
Das ist ein Hinweis darauf, dass die Schleife optimiert wird. Auch dieser Satz widerspricht früheren Zahlen, die mir sehr zutreffend erscheinen.Antworten:
Dafür gibt es 3 Gründe:
Die Kosten für die Überprüfung auf Überläufe (für jede einzelne arithmetische Operation) zur Laufzeit sind zu hoch.
Die Komplexität des Nachweises, dass eine Überlaufprüfung zur Kompilierungszeit entfallen kann, ist zu hoch.
In einigen Fällen (z. B. bei CRC-Berechnungen, großen Bibliotheken usw.) ist "Wrap-on-Overflow" für Programmierer praktischer.
quelle
unsigned int
sollte nicht in den Sinn kommen, da eine Sprache mit Überlaufprüfung standardmäßig alle Integer-Typen prüfen sollte . Du solltest schreiben müssenwrapping unsigned int
.didOverflow()
Inline-Funktion oder sogar eine globale Variable__carry
, die den Zugriff auf das Übertragsflag ermöglicht, würde keine CPU-Zeit kosten, wenn Sie sie nicht verwenden.ADD
setzt nicht den Carry (du brauchstADDS
). Itanium nicht einmal hat einen Carry - Flag. Und selbst auf x86 hat AVX keine Carry-Flags.unchecked
einfach genug ist; aber Sie überschätzen möglicherweise, wie oft Überlauf wichtig ist.adds
ist derselbe Preis wieadd
(es ist nur ein Anweisungs-1-Bit-Flag, das auswählt, ob das Übertrags-Flag aktualisiert wird). MIPSsadd
Anweisung fängt bei Überlauf ab - Sie müssen stattdessen fragen , ob Sie bei Überlauf nicht abfangen möchten, indem Sie verwendenaddu
!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 zur Generierung 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 jeder Code würde sich bei Überläufen unregelmäßig verhalten. Die Käfer wären nicht gutartig. Datenkorruption ist wahrscheinlich, Sicherheitsprobleme sind möglich.
Wenn ich die Leistung benötige, was manchmal der Fall ist, deaktiviere ich die Überlaufprüfung
unchecked {}
auf granularer Basis. Wenn ich darauf hinweisen möchte, dass ich mich auf eine Operation verlasse, die nicht überläuft, kann ichchecked {}
den Code redundant ergänzen , um diese Tatsache zu dokumentieren. Ich achte auf Überläufe, muss mich aber nicht unbedingt an die Überprüfung halten.Ich glaube, das C # -Team hat die falsche Wahl getroffen, als es beschlossen hat, den Überlauf nicht standardmäßig zu überprüfen, aber diese Wahl ist jetzt aufgrund starker Kompatibilitätsprobleme ausgeschlossen. Beachten Sie, dass diese Wahl um das Jahr 2000 getroffen wurde. Die 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. Aus diesem Grund verfügt es über unsicheren Code, Strukturen und hervorragende native Aufruffähigkeiten, die Java nicht bietet.
Je schneller unsere Hardware wird und je intelligenter die Compiler sind, desto attraktiver ist standardmäßig die Überlaufprüfung.
Ich glaube auch, dass Überlaufprüfungen oft besser sind als Zahlen mit unbegrenzter Größe. Zahlen mit unbegrenzter Größe haben einen noch höheren Leistungsaufwand, der (meiner Meinung nach) schwerer zu optimieren ist, und sie eröffnen die Möglichkeit eines unbegrenzten Ressourcenverbrauchs.
Die Art und Weise, wie JavaScript mit Überlauf umgeht, ist noch schlimmer. JavaScript-Zahlen sind Gleitkommadoppelwerte. Ein "Überlauf" äußert sich darin, dass der vollständig genaue Satz von ganzen Zahlen übrig bleibt. Es treten leicht falsche Ergebnisse auf (z. B. um eins versetzt - dies kann endliche Schleifen in unendliche verwandeln).
Für einige Sprachen wie C / C ++ ist eine Überlaufprüfung standardmäßig eindeutig ungeeignet, da die Art von Anwendungen, die in diesen Sprachen geschrieben werden, eine Bare-Metal-Performance erfordert. Dennoch gibt es Bemühungen, C / C ++ zu einer sichereren Sprache zu machen, indem es ermöglicht wird, sich für einen sichereren Modus zu entscheiden . Dies ist lobenswert, da 90-99% des Codes kalt sind. Ein Beispiel ist die
fwrapv
Compiler-Option, die den Komplementumbruch von 2 erzwingt. Dies ist eine "Qualität der Implementierung" -Funktion des Compilers, nicht der Sprache.Haskell hat keine logische Aufrufliste und keine festgelegte Auswertungsreihenfolge. Dies führt dazu, dass an unvorhersehbaren Punkten Ausnahmen auftreten. In
a + b
ihm ist nicht spezifiziert , oba
oderb
zuerst ausgewertet wird und ob diese Ausdrücke überhaupt nicht oder nicht beenden. Daher ist es für Haskell sinnvoll, die meiste Zeit unbegrenzte ganze Zahlen 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 in Haskells Sprachentwurf. 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 die Sprache bietet.Eine Alternative zu Überlaufausnahmen sind Giftwerte, die durch undefinierte Operationen erstellt werden und sich durch Operationen ausbreiten (wie der Gleitkommawert
NaN
). Das scheint viel teurer als Überlaufprüfung und macht alle Vorgänge langsamer, nicht nur diejenigen , die (abgesehen von Hardware - Beschleunigung , die gewöhnlich schwimmt haben und Ints haben häufig nicht - obwohl ausfallen können Itanium NaT hat, das „kein Ding“ ). Ich sehe auch nicht ganz den Sinn, das Programm mit schlechten Daten weiter hinken zu lassen. Es ist wieON ERROR RESUME NEXT
. Es verbirgt Fehler, hilft aber nicht dabei, korrekte Ergebnisse zu erzielen. supercat weist darauf hin, dass es manchmal eine Leistungsoptimierung ist, dies zu tun.quelle
unsigned
ganze Zahlen. Das Verhalten des Überlaufs von Ganzzahlen mit Vorzeichen ist in C und C ++ tatsächlich undefiniert. Ja, undefiniertes Verhalten . Es kommt einfach so vor, dass fast jeder es als 2-Komplement-Überlauf implementiert. C # macht es offiziell, anstatt es wie C / C ++gcc -O2
fürx + 1 > x
(wox
eine istint
). Siehe auch gcc.gnu.org/onlinedocs/gcc-6.3.0/gcc/… . Das 2s-Komplement-Verhalten bei signiertem Überlauf in C ist optional , auch in realen Compilern, undgcc
ignoriert es standardmäßig in normalen Optimierungsstufen.Weil es ein schlechter Kompromiss macht alle Berechnungen sehr viel teurer , um automatisch den seltenen Fall zu fangen , die ein Überlauf tut auftreten. Es ist viel besser, den Programmierer mit dem Erkennen der seltenen Fälle zu belasten, in denen dies ein Problem ist, und spezielle Vorsichtsmaßnahmen hinzuzufügen, als alle Programmierer den Preis für die Funktionalität zahlen zu lassen , die sie nicht verwenden.
quelle
"Erzwingen Sie nicht, dass Benutzer eine Leistungsstrafe für eine Funktion zahlen, die sie möglicherweise nicht benötigen."
Es ist eine der grundlegendsten Prinzipien im Design von C und C ++ und stammt aus einer anderen Zeit, in der Sie durch lächerliche Verzerrungen mussten, um für Aufgaben, die heutzutage als trivial gelten, eine kaum ausreichende Leistung zu erzielen.
Neuere Sprachen brechen mit dieser Einstellung für viele andere Funktionen, wie z. B. die Überprüfung der 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.
quelle
checked
undunchecked
Syntax zum lokalen Wechseln zwischen ihnen und Befehlszeilenoptionen (und Projekteinstellungen in VS) hinzugefügt, um sie global zu ändern. Sie stimmen möglicherweise nicht mitunchecked
der Standardeinstellung überein (das tue ich), aber all dies ist eindeutig sehr beabsichtigt.Erbe
Ich würde sagen, dass das Problem wahrscheinlich im Erbe verwurzelt ist. In C:
Dies wurde getan, 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 in C (und in der Erweiterung C ++) abwechselnd kein Überlauf erkannt werden muss, bedeutet, dass die Überlaufprüfung nur schleppend durchgeführt wird.
Hardware ist hauptsächlich für C / C ++ ausgelegt (im Ernst, x86 verfügt über eine
strcmp
Anweisung (auch bekannt als PCMPISTRI ab SSE 4.2)!), Und da es C egal ist, bieten herkömmliche CPUs keine effizienten Möglichkeiten zum Erkennen von Überläufen. In x86 müssen Sie nach jedem potenziell überlaufenden Vorgang ein Pro-Core-Flag überprüfen. wenn Sie wirklich wollen, ist eine "verdorbene" Flagge auf dem Ergebnis (ähnlich wie sich NaN ausbreitet). Und Vektoroperationen können noch problematischer sein. Einige neue Player können mit effizienter Überlaufbehandlung auf dem Markt erscheinen. aber für jetzt x86 und ARM ist das egal.Compiler-Optimierer sind nicht gut darin, Überlaufprüfungen zu optimieren oder sogar bei vorhandenen Überläufen zu optimieren. Einige Wissenschaftler wie John Regher beschweren sich über diesen Status , aber die Tatsache ist, dass, wenn die einfache Tatsache, dass Überläufe "Fehler" sind, Optimierungen verhindert, noch bevor die Baugruppe aufschlägt, die CPU lahmgelegt werden kann. Besonders wenn es die automatische Vektorisierung verhindert ...
Mit Kaskadeneffekten
In Ermangelung effizienter Optimierungsstrategien und einer effizienten CPU-Unterstützung ist die Überlaufprüfung daher kostspielig. Viel teurer als das Wickeln.
Fügen Sie einige störende Verhaltensweisen hinzu, z. B.
x + y - 1
einen Überlauf, wennx - 1 + y
dies nicht der Fall ist, was Benutzer berechtigterweise stören kann, und die Überlaufprüfung wird im Allgemeinen zugunsten des Umschließens (das dieses Beispiel und viele andere ordnungsgemäß behandelt) verworfen.Dennoch ist nicht alle Hoffnung verloren
Die Compiler von clang und gcc haben sich bemüht, "Desinfektionsprogramme" zu implementieren: Möglichkeiten, Binärdateien zu instrumentieren, um Fälle von undefiniertem Verhalten zu erkennen. Bei Verwendung
-fsanitize=undefined
wird ein signierter Überlauf erkannt und das Programm abgebrochen. sehr nützlich beim Testen.In der Rust- Programmiersprache ist die Überlaufprüfung standardmäßig im Debug-Modus aktiviert (aus Leistungsgründen wird im Release-Modus die Umbrucharithmetik verwendet).
Daher wächst die Sorge, dass Überlaufprüfungen und die Gefahren von falschen Ergebnissen nicht erkannt werden, und dies wird hoffentlich das Interesse der Forschungsgemeinschaft, der Compilergemeinschaft und der Hardware-Gemeinschaft wecken.
quelle
jo
's' und 's' berücksichtigt werden Weitere globale Auswirkungen der Verschmutzung tragen zum Status der Verzweigungsvorhersage und zur Erhöhung der Codegröße bei. Wenn diese Flagge klebrig wäre, würde sie ein echtes Potenzial bieten. Und dann können Sie es im vektorisierten Code immer noch nicht richtig machen.1..100
stattdessen die Pascal-ish- Typen - seien Sie explizit in Bezug auf die erwarteten Bereiche, anstatt in 2 ^ 31 "gezwungen" zu werden Kompilierzeit, gerade).x * 2 - 2
Überlauf kommen, wenn der Wertx
51 beträgt, obwohl das Ergebnis passt. Dies zwingt Sie dazu, Ihre Berechnung neu zu ordnen (manchmal auf unnatürliche Weise). Nach meiner Erfahrung habe ich festgestellt, dass ich die Berechnung im Allgemeinen lieber in einem größeren Typ durchführe und dann überprüfe, ob das Ergebnis passt oder nicht.x = x * 2 - 2
sollte er für alle funktionieren, beix
denen die Zuweisung eine gültige 1 ergibt). .100 Nummer). Das heißt, Operationen für den numerischen Typ haben möglicherweise eine höhere Genauigkeit als der Typ selbst, solange die Zuweisung passt. Dies wäre sehr nützlich in Fällen wie(a + b) / 2
die zu ignorieren (unsigned) überläuft kann die richtige Wahl sein.Sprachen, die versuchen, Überläufe zu erkennen, haben die zugehörige Semantik historisch so definiert, dass sie die sonst nützlichen Optimierungen stark einschränkte. Unter anderem ist es oft nützlich, Berechnungen in einer anderen Reihenfolge als der im Code angegebenen durchzuführen. Die meisten Sprachen, die Überläufe abfangen, garantieren jedoch, dass der angegebene Code wie folgt lautet:
Wenn der Startwert von x beim 47. Durchlauf durch die Schleife zu einem Überlauf führen würde, wird Operation1 47-mal ausgeführt, und Operation2 wird 46 ausgeführt. In Ermangelung einer solchen Garantie verwendet nichts anderes in der Schleife x und nichts Verwendet den Wert von x nach einer von Operation1 oder Operation2 ausgelösten Ausnahme. Der Code könnte ersetzt werden durch:
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:
Wenn man bedenkt, dass ein Großteil des Codes in der realen Welt aufwändigere Schleifen verwendet, ist es offensichtlich, dass die Optimierung des Codes unter Beibehaltung der Überlaufsemantik schwierig ist. Aufgrund von Caching-Problemen ist es außerdem durchaus möglich, dass das Gesamtprogramm durch die Erhöhung der Codegröße langsamer ausgeführt wird, obwohl auf dem gemeinsam ausgeführten Pfad weniger Vorgänge ausgeführt werden.
Um die Überlauferkennung kostengünstig zu machen, wäre ein definierter Satz von Semantiken für die Erkennung von Überläufen erforderlich, mit denen Code auf einfache Weise meldet, ob eine Berechnung ohne Überläufe durchgeführt wurde, die sich auf die Ergebnisse auswirken 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 das zur Erreichung des oben genannten Ziels erforderliche Minimum zu reduzieren, könnten die Kosten erheblich gesenkt werden, als dies in vorhandenen Sprachen der Fall ist. Es sind mir jedoch keine Bemühungen bekannt, eine effiziente Überlauferkennung zu ermöglichen.
(*) Wenn eine Sprache verspricht, dass alle Überläufe gemeldet werden, kann ein Ausdruck wie
x*y/y
nicht vereinfacht werden, esx
sei denn , esx*y
kann garantiert werden, dass kein Überlauf auftritt. Auch wenn das Ergebnis einer Berechnung ignoriert würde, muss eine Sprache, die verspricht, alle Überläufe zu melden, diese trotzdem ausführen, damit die Überlaufprüfung durchgeführt werden kann. Da ein Überlauf in solchen Fällen nicht zu einem arithmetisch falschen Verhalten führen kann, müsste ein Programm solche Prüfungen nicht durchführen, um sicherzustellen, dass keine Überläufe möglicherweise ungenaue Ergebnisse verursacht haben.Überläufe in C sind übrigens besonders schlimm. Obwohl fast jede Hardwareplattform, die C99 unterstützt, eine Silent-Wraparound-Semantik verwendet, ist es für moderne Compiler in Mode, Code zu generieren, der im Falle eines Überlaufs willkürliche Nebenwirkungen verursachen kann. Zum Beispiel mit etwas wie:
GCC generiert Code für test2, der einmal bedingungslos inkrementiert (* p) und unabhängig von dem an q übergebenen Wert 32768 zurückgibt. Nach seiner Überlegung würde die Berechnung von (32769 * 65535) & 65535u einen Überlauf verursachen, sodass der Compiler keine Fälle berücksichtigen muss, in denen (q | 32768) einen Wert größer als 32768 ergibt Damit bei der Berechnung von (32769 * 65535) & 65535u die oberen Bits des Ergebnisses berücksichtigt werden, verwendet gcc einen vorzeichenbehafteten Überlauf als Begründung für das Ignorieren der Schleife.
quelle
-fwrapv
ergibt sich ein definiertes Verhalten, wenn auch nicht das vom Fragesteller gewünschte. Zugegeben, die gcc-Optimierung macht aus jeder Art von C-Entwicklung eine gründliche Prüfung des Standards und des Compilerverhaltens.x+y > z
eine Weise auswerten muss , die niemals etwas anderes als 0 oder 1 ergibt, aber eines der beiden Ergebnisse im Falle eines Überlaufs gleichermaßen akzeptabel wäre, könnte ein Compiler, der diese Garantie bietet, häufig besseren Code für das generieren Ausdruckx+y > z
als jeder Compiler wäre in der Lage, für eine defensiv geschriebene Version des Ausdrucks zu generieren. Aus realistischer Sicht würde welcher Bruchteil nützlicher überlaufbezogener Optimierungen durch eine Garantie ausgeschlossen, dass andere Ganzzahlberechnungen als Division / Rest ohne Nebenwirkungen ausgeführt werden?-fwhatever-makes-sense
Patch nicht akzeptiert ", legt mir nahe, dass es noch mehr gibt dazu als launisch von ihrer Seite. Die üblichen Argumente, die ich gehört habe, sind, dass Code-Inlining (und sogar Makro-Erweiterung) davon profitiert, so viel wie möglich über die spezifische Verwendung eines Codekonstrukts abzuleiten, da beide Methoden üblicherweise eingefügten Code ergeben, der sich mit Fällen befasst, die nicht benötigt werden dazu, dass sich der umgebende Code als unmöglich "erweist".foo(i + INT_MAX + 1)
, sind Compiler-Autoren daran interessiert, Optimierungen für den Inline-foo()
Code vorzunehmen , bei denen die Richtigkeit davon abhängt , dass das Argument nicht negativ ist (vielleicht teuflische Divmod-Tricks). Unter Ihren zusätzlichen Einschränkungen können sie nur Optimierungen anwenden, deren Verhalten für negative Eingaben für die Plattform sinnvoll ist. Natürlich würde ich mich freuen, wenn dies eine-f
Option wäre, die sich-fwrapv
usw. einschaltet , und wahrscheinlich einige Optimierungen deaktivieren müsste, für die es kein Flag gibt. Aber es ist nicht so, dass ich mir die Mühe machen könnte, all diese Arbeiten selbst zu erledigen.Nicht alle Programmiersprachen ignorieren Ganzzahlüberläufe. Einige Sprachen bieten sichere Ganzzahloperationen für alle Zahlen (die meisten Lisp-Dialekte, Ruby, Smalltalk usw.) und andere über Bibliotheken - zum Beispiel gibt es verschiedene BigInt-Klassen für C ++.
Ob eine Sprache Integer 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 "Big Integer" ist keine Eins. Produktivitätssprachen wie Ruby können und bieten große ganze Zahlen von der Stange. Sprachen wie Java und C #, die irgendwo dazwischen liegen, sollten IMHO mit den sicheren Ganzzahlen aus dem Kasten gehen, indem sie es nicht tun.
quelle
Wie Sie gezeigt haben, wäre C # dreimal langsamer gewesen, wenn standardmäßig Überlaufprüfungen aktiviert gewesen wären (vorausgesetzt, Ihr Beispiel ist eine typische Anwendung für diese Sprache). Ich stimme zu, dass die Leistung nicht immer das wichtigste Merkmal ist, aber Sprachen / Compiler werden in der Regel in Bezug auf ihre Leistung bei typischen Aufgaben verglichen. Dies liegt zum Teil daran, 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 erreichen, selbst wenn die meisten Endbenutzer letztendlich mehr von Überlaufprüfungen profitieren würden als sie von höherer Leistung.
quelle
Abgesehen von den vielen Antworten, die eine mangelnde Überprüfung des Überlaufs auf der Grundlage der Leistung rechtfertigen, sind zwei verschiedene Arten von Berechnungen zu berücksichtigen:
Indizierungsberechnungen (Array-Indizierung und / oder Zeigerarithmetik)
andere Arithmetik
Wenn die Sprache eine Ganzzahlgröße verwendet, die mit der Zeigergröße identisch ist, läuft ein gut aufgebautes Programm bei Indexierungsberechnungen nicht über, da notwendigerweise nicht genügend Arbeitsspeicher vorhanden sein muss, bevor die Indexierungsberechnungen einen Überlauf verursachen.
Daher ist das Überprüfen der Speicherzuordnungen ausreichend, wenn mit Zeigerarithmetik und Indexausdrücken gearbeitet wird, die zugewiesene Datenstrukturen enthalten. Wenn Sie beispielsweise über einen 32-Bit-Adressraum verfügen, 32-Bit-Ganzzahlen verwenden und maximal 2 GB Heap (etwa die Hälfte des Adressraums) zuweisen, werden Index- / Zeigerberechnungen (im Grunde genommen) nicht überlaufen.
Darüber hinaus werden Sie möglicherweise überrascht sein, wie viel Addition / Subtraktion / Multiplikation eine Array-Indizierung oder eine Zeigerberechnung umfasst und somit in die erste Kategorie fällt. Objektzeiger-, Feldzugriffs- und Array-Manipulationen sind Indizierungsoperationen, und viele Programme führen nicht mehr arithmetische Berechnungen durch als diese! Dies ist im Wesentlichen der Hauptgrund dafür, dass Programme genauso gut funktionieren wie ohne Ganzzahlüberlaufprüfung.
Alle Nicht-Indexierungs- 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 Summierungsbeispiel).
Im letzteren Fall verwenden Programmierer häufig alternative Datentypen, wie z. B.
double
oder einigeBigInt
. Viele Berechnungen erforderndecimal
eher einen Datentyp alsdouble
z. B. finanzielle Berechnungen. Wenn sie dies nicht tun und bei Integer-Typen bleiben, müssen sie darauf achten, dass der Integer-Überlauf nicht erkannt wird. Andernfalls kann das Programm einen unerkannten Fehlerzustand erreichen, wie Sie darauf hinweisen.Als Programmierer müssen wir sensibel auf unsere Auswahl numerischer Datentypen und deren Konsequenzen in Bezug auf die Möglichkeiten eines Überlaufs reagieren, ganz zu schweigen von der Präzision. Im Allgemeinen (und insbesondere bei der Arbeit mit der C-Sprachfamilie mit dem Wunsch, schnelle Ganzzahltypen zu verwenden) müssen wir die Unterschiede zwischen Indexberechnungen und anderen berücksichtigen und berücksichtigen.
quelle
Die Sprache Rust bietet einen interessanten Kompromiss zwischen der Überprüfung auf Überläufe und nicht, indem die Überprüfungen für den Debugbuild hinzugefügt und in der optimierten Release-Version entfernt werden. Auf diese Weise können Sie die Fehler während des Testens finden und erhalten dennoch die volle Leistung in der endgültigen Version.
Da die Überlaufumgehung manchmal erwünscht ist, gibt es auch Versionen der Operatoren , die niemals auf Überlauf prüfen.
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 diesem Blog-Beitrag , einschließlich einer Liste von Fehlern , die mit dieser Funktion beim Auffinden von Fehlern geholfen haben.
quelle
checked_mul
, das überprüft , ob Überlauf aufgetreten ist, und kehrt hat ,None
wenn ja,Some
anders. Dies kann sowohl im Produktions- als auch im Debug-Modus verwendet werden: doc.rust-lang.org/std/primitive.i32.html#examples-15In Swift werden Integer-Überläufe standardmäßig erkannt und stoppen das Programm sofort. In Fällen, in denen Sie ein Umlaufverhalten 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 bringen :-)
Jetzt sind die Designer von Swift auch die Designer von LLVM und Clang, sodass sie sich ein oder zwei Mal mit Optimierung auskennen und in der Lage sind, unnötige Überlaufprüfungen zu vermeiden. Wenn alle Optimierungen aktiviert sind, trägt die Überlaufprüfung nicht wesentlich zur Codegröße und Ausführungszeit bei. Und da die meisten Überläufe zu absolut falschen Ergebnissen führen, sind Codegröße und Ausführungszeit sinnvoll.
PS. In C, C ++ ist der mit Objective-C vorzeichenbehaftete ganzzahlige arithmetische Überlauf undefiniertes Verhalten. Das heißt, was auch immer der Compiler im Fall eines vorzeichenbehafteten Ganzzahlüberlaufs tut, ist per Definition korrekt. Typische Möglichkeiten, mit einem vorzeichenbehafteten Integer-Überlauf umzugehen, bestehen darin, ihn zu ignorieren. Dabei wird das Ergebnis der CPU berücksichtigt, sodass der Compiler davon ausgeht, dass ein solcher Überlauf niemals auftreten wird Es wird davon ausgegangen, dass dies niemals passiert. Eine selten genutzte Möglichkeit besteht darin, zu überprüfen und abzustürzen, ob ein Überlauf auftritt, wie dies bei Swift der Fall ist.
quelle
x+1>x
ein Compiler als bedingungslos wahr behandelt wird, muss er keine "Annahmen" über x treffen, wenn er ganzzahlige Ausdrücke mit willkürlich größeren Typen so bequem wie möglich auswerten darf (oder sich so verhält, als ob dies der Fall wäre). Ein schlimmeres Beispiel für überlaufbasierte "Annahmen" wäre die Entscheidung, dassuint32_t mul(uint16_t x, uint16_t y) { return x*y & 65535u; }
ein Compilersum += mul(65535, x)
entscheidenx
kann , ob er nicht größer als 32768 sein darf [Verhalten, das die Leute, die die C89-Begründung verfasst haben, wahrscheinlich schockieren würde, was darauf hindeutet, dass einer der entscheidenden Faktoren. ..unsigned short
zusigned int
war die Tatsache, dass Silent-Wraparound-Implementierungen mit zwei Ergänzungen (dh die Mehrheit der damals verwendeten C-Implementierungen) Code wie oben beschrieben gleich behandeln würden, unabhängig davon, obunsigned short
zuint
oder heraufgestuft wurdeunsigned
. Der Standard erforderte keine Implementierungen auf Hardware, die sich aus zwei Komponenten zusammensetzt, um Code wie oben beschrieben zu behandeln, aber die Autoren des Standards hatten anscheinend damit gerechnet, dass sie dies dennoch tun würden.Tatsächlich ist die eigentliche Ursache dafür rein technisch / historisch: Die CPU ignoriert zum größten Teil das Vorzeichen. Im Allgemeinen gibt es nur einen einzigen Befehl zum Hinzufügen von zwei Ganzzahlen in Registern, und der CPU ist es gleichgültig, ob Sie diese beiden Ganzzahlen als vorzeichenbehaftet oder nicht vorzeichenbehaftet interpretieren. Das gleiche gilt für die Subtraktion und sogar für die Multiplikation. Die einzige Rechenoperation, die vorzeichenbewusst sein muss, ist die Division.
Der Grund, warum dies funktioniert, ist die 2er-Komplementdarstellung von vorzeichenbehafteten ganzen Zahlen, die von praktisch allen CPUs verwendet wird. In 4-Bit-2-Komplementen sieht die Addition von 5 und -3 beispielsweise folgendermaßen aus:
Beobachten Sie, wie das Umlaufverhalten beim Wegwerfen des Übertragsbits das richtige vorzeichenbehaftete Ergebnis liefert. Ebenso implementieren CPUs die Subtraktion normalerweise
x - y
wie folgtx + ~y + 1
:Dies implementiert die Subtraktion als eine Addition in der Hardware, wobei nur die Eingaben in die arithmetisch-logische Einheit (ALU) auf triviale Weise optimiert werden. Was könnte einfacher sein?
Da Multiplikation nichts anderes als eine Folge von Additionen ist, verhält es sich ähnlich gut. Das Ergebnis der Verwendung der Zweierkomplementdarstellung und des Ignorierens der Ausführung von arithmetischen Operationen ist eine vereinfachte Schaltungsanordnung und vereinfachte Befehlssätze.
Da C so konzipiert wurde, dass es in der Nähe des Metalls arbeitet, hat es offensichtlich genau dasselbe Verhalten wie das standardisierte Verhalten von vorzeichenloser Arithmetik angenommen, sodass nur vorzeichenbehaftete Arithmetik undefiniertes Verhalten liefert. Und diese Wahl wurde auf andere Sprachen wie Java und natürlich C # übertragen.
quelle
x==INT_MAX
, dannx+1
könnte er sich beim Compiler willkürlich entweder als +2147483648 oder -2147483648 verhalten Bequemlichkeit), aber ...x
undy
sinduint16_t
und Code auf einem 32-Bit-System berechnet,x*y & 65535u
wenny
65535 ist, ein Compiler davon ausgehen sollte, dass Code nie erreicht wird, wennx
größer als 32768.In einigen Antworten wurden die Überprüfungskosten 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 Beispiel) besteht eines der Prinzipien beim Entwerfen von Sprachen nicht darin, 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, Sie entscheiden sich jedoch dafür, mit der Sprache zu arbeiten, die dies kennt, und Sie 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 das Verhältnis zwischen Leistung und Risiko unterschiedlich ist.
An dieser Argumentation sind einige Dinge falsch:
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 hinsichtlich ihrer Leistung um Größenordnungen variieren. 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 Super-Computer-Cluster codiert, unerträglich langsam zu sein.
1 Nanosekunde scheint für ein Codesegment, das selten ausgeführt wird, nichts zu sein. Auf der anderen Seite kann jeder einzelne Bruchteil der Zeit, den Sie abschneiden können, einen großen Unterschied bewirken, wenn sich der Code in einer inneren Schleife einer Berechnung befindet, die die Hauptfunktion des Codes darstellt. 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.
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.
Vielleicht hast du recht. Aber auch dies ist eine Frage der Ziele einer bestimmten Sprache. In der Tat sind viele Sprachen so konzipiert, dass sie den Bedürfnissen der "meisten" gerecht werden oder die Sicherheit anderen Bedenken vorziehen. Andere wie C und C ++ legen Wert auf Effizienz. In diesem Zusammenhang verstößt es gegen das, was die Sprache zu erreichen versucht, wenn jeder eine Leistungsstrafe zahlen muss, nur weil die meisten Menschen sich nicht darum kümmern.
quelle
Es gibt gute Antworten, aber ich denke, hier gibt es einen vermissten Punkt: Die Auswirkungen eines Ganzzahlüberlaufs sind nicht unbedingt schlecht, und im Nachhinein ist es schwierig zu erkennen, ob der
i
Übergang vom SeinMAX_INT
zum SeinMIN_INT
auf ein Überlaufproblem zurückzuführen ist oder wenn dies absichtlich durch Multiplikation mit -1 geschehen ist.Wenn ich zum Beispiel alle darstellbaren ganzen Zahlen größer als 0 addieren möchte, würde ich einfach eine
for(i=0;i>=0;++i){...}
Additionsschleife verwenden - und wenn sie überläuft, stoppt sie die Addition, was das Zielverhalten ist (das Auslösen eines Fehlers würde bedeuten, dass ich umgehen muss ein willkürlicher Schutz, weil er die Standardarithmetik stört). Es ist eine schlechte Praxis, primitive Arithmetik einzuschränken, weil:quelle
INT_MAX
zuINT_MIN
wechseln, indem Sie mit -1 multiplizieren.for(i=0;i>=0;++i){...}
ist der Codestil, von dem ich in meinem Team abzuraten versuche: Er beruht auf Spezialeffekten / Nebenwirkungen und drückt nicht klar aus, was er soll. Trotzdem weiß ich Ihre Antwort zu schätzen, da sie ein anderes Programmierparadigma zeigt.i
es sich um einen 64-Bit-Typ handelt, selbst bei einer Implementierung mit einem konsistenten Verhalten, bei dem eine Milliarde Iterationen pro Sekunde ausgeführt werden, und bei der ein Zweierkomplement-Umlauf ausgeführt wird, kann sichergestellt werden, dass eine solche Schleife nur dann den größtenint
Wert findet, wenn sie ausgeführt werden darf Hunderte von Jahren. Auf Systemen, die kein konsistentes Verhalten bei unbeaufsichtigtem Zugriff versprechen, kann ein solches Verhalten nicht garantiert werden, unabhängig davon, wie lange Code angegeben wird.