Czy kiedykolwiek próbowałeś zsumować wszystkie liczby od 1 do 2 000 000 w swoim ulubionym języku programowania? Wynik można łatwo obliczyć ręcznie: 2 000 001 000 000, czyli około 900 razy więcej niż maksymalna wartość 32-bitowej liczby całkowitej bez znaku.
Drukuje się C # -1453759936
- wartość ujemna! I chyba Java robi to samo.
Oznacza to, że istnieje kilka popularnych języków programowania, które domyślnie ignorują przepełnienie arytmetyczne (w języku C # istnieją ukryte opcje zmiany tego). To zachowanie wydaje mi się bardzo ryzykowne, a czy awaria Ariane 5 nie była spowodowana takim przepełnieniem?
Więc: jakie decyzje projektowe kryją się za tak niebezpiecznym zachowaniem?
Edytować:
Pierwsze odpowiedzi na to pytanie wyrażają nadmierne koszty sprawdzania. Uruchommy krótki program w języku C #, aby przetestować to założenie:
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);
Na moim komputerze wersja sprawdzona zajmuje 11015 ms, a wersja niezaznaczona 4125 ms. Tzn. Kroki sprawdzania trwają prawie dwa razy dłużej niż dodawanie liczb (w sumie 3 razy pierwotny czas). Ale przy 10 000 000 000 powtórzeń czas sprawdzania jest nadal krótszy niż 1 nanosekunda. Może się zdarzyć, że jest to ważne, ale w przypadku większości aplikacji nie będzie to miało znaczenia.
Edycja 2:
Zrekompilowałem naszą aplikację serwerową (usługa Windows analizująca dane otrzymane z kilku czujników, dość sporo kradzieży danych) z /p:CheckForOverflowUnderflow="false"
parametrem (zwykle włączam kontrolę przepełnienia) i wdrożyłem ją na urządzeniu. Monitorowanie Nagios pokazuje, że średnie obciążenie procesora pozostało na poziomie 17%.
Oznacza to, że uderzenie wydajności znalezione w powyższym przykładzie jest zupełnie nieistotne dla naszej aplikacji.
(1..2_000_000).sum #=> 2000001000000
. Jeszcze jedno z moich ulubionych językach: sum [1 .. 2000000] --=> 2000001000000
. Nie mój ulubiony: Array.from({length: 2000001}, (v, k) => k).reduce((acc, el) => acc + el) //=> 2000001000000
. (Szczerze mówiąc, ostatni oszukuje.)
Integer
w Haskell ma dowolną precyzję, będzie przechowywać dowolną liczbę, o ile nie zabraknie przydzielonej pamięci RAM.
But with the 10,000,000,000 repetitions, the time taken by a check is still less than 1 nanosecond.
wskazuje to na optymalizację pętli. Również to zdanie jest sprzeczne z poprzednimi liczbami, które wydają mi się bardzo ważne.
checked { }
sekcji do oznaczenia części kodu, które powinny wykonywać sprawdzanie przepełnienia arytmetycznego. Wynika to z występu