2021-12-26 / Bartłomiej Kurek
Integralność danych: #2 - błędy liczbowe, typy. Arbitrary precision.

Podczas implementacji testów w jednym z poprzednich artykułów zostaliśmy zaskoczeni przez Django PositiveIntegerField, który nie był positive. Stąd inspiracja do tej serii artykułów.
W poprzedniej części nakreśliliśmy krótko charakterystykę i znaczenie błędów w programach oraz ich możliwe konsekwencje (rzeczywiste i hipotetyczne). W tym artykule rozwijamy myśl, przyglądamy się przykładom kodu. Analizujemy tutaj tylko liczby. Inne sprawy pojawią się w kilku następnych odsłonach.

Błędy

Trivia

Ten artykuł zaczynamy od języka C++, ale w prostych słowach. Zaczynamy od początku, a przykłady poniżej - mniej lub bardziej jawnie - będą się do tego odnosić.
Trywialny przykład (w C/C++, wszystko jedno).

int main()
{
    int x = 32768;
    short y = x;
    return !(x == y);
}

Wartość zmiennej x wynosi 2 ** 15. Ustawiam tę wartość na sztywno, ponieważ w C ani w C++ nie ma operatora potęgowania. Niektóre osoby mylą operator ^ (xor) z potęgowaniem.
Funkcja main() zwraca negację wyniku porównania. Jeśli wartości x i y są równe, to porównanie zwraca 1, a jeśli nie są równe, to wynik porównania równy jest 0. Negujemy wynik, ponieważ do systemu zwracamy 0 jeśli nie ma błędu, a w przypadku błędu zwracamy jego kod (liczbę).

Kompiluję, uruchamiam, patrzymy:

$ g++ main.cxx -o program -Wall -Wextra -Werror -Wpedantic
$ ./program && echo "OK" || echo "BROKEN"
BROKEN

Program jest zepsuty.

Przy okazji - w Python też mamy xor:

1
2
3
4
5
6
7
8
>>> def f(x): return x ^ 2
...
>>> import dis
>>> dis.dis(f)
  1           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (2)
              4 BINARY_XOR
              6 RETURN_VALUE

Wracamy jednak do C++. Dodajmy wypisywanie zmiennej y i rozmiaru typu short oraz dodajmy użycie argumentu przekazanego przy wywołaniu. Przy okazji zapiszemy tę instrukcję return w sposób przejrzysty, używając ternary operator (taki operator, który ma 3 argumenty, jak wyżej w shellu przy "BROKEN": (test ? ok : not_ok)). Występuje w większości języków programowania, ale np. język Go nie ma tego operatora. Argument argc jest zakomentowany, ponieważ zechcemy wyłapywać wszelkie błędy (flaga kompilatora -Werror). Argumentu argc nie używamy tutaj specjalnie, a zatem kompilator w tym przypadku potraktowałby to jako błąd. Funkcja atoi zmienia napis na liczbę (ascii to int). W shellu podajemy argumenty jako napisy, w programie chcemy tutaj liczbę.

#include <iostream>
#include <cstdlib>


int main(int /* argc */, char** argv)
{
    int x = atoi(argv[1]);
    short y = x;
    std::cout << "sizeof short: " << sizeof(short) << std::endl;
    std::cout << "y: " << y << std::endl;

    //      cond        if ok         if NOT ok
    return (x == y) ? EXIT_SUCCESS : EXIT_FAILURE;
}

Kompiluję:

$ g++ main.cxx -o program -Wall -Wextra -Werror -Wpedantic

Teraz możemy przekazywać argument (wartość dla x) przy wywołaniu.
Tak zadziała:

$ ./program 32767
sizeof short: 2
y: 32767

Tak nie zadziała:

$ ./program 32768
sizeof short: 2
y: -32768

32768 (czyli 2 ** 15) nie mieści się już w typie short. Kiedy zmieniamy typy (przypisanie, rzutowanie /type casting/), to musimy zważać na rozmiar typu wynikowego. Języki z wyższymi warstwami abstrakcji potrafią tego pilnować nawet bardzo głęboko w zawiłym i częściowo automatycznie generowanym kodzie. Nie będziemy tutaj rozważać jeszcze tych abstrakcji, wspomnijmy jedynie, że nie wszędzie języki mocno wyabstrahowane mają zastosowanie. Niektóre rodzaje sprzętu mają minimalną pojemność, czy architekturę, w niektórych nawet nie ma jak części rzeczy zrealizować (np. wyjątków).

Sprawy nieoczywiste

C++ już widzieliśmy powyżej - kontynuujemy.
Być może komuś, tak jak mnie, również zdarzyło się popełnić kiedyś podobną literówkę. Miało być 1, jest i. Zdarza się.

int main()
{
    int i = i;
    return i;
}

Kompiluję, uruchamiam, patrzymy:

$ g++ main.cxx -o program
$ ./program
$ echo $?
0

Zero. Program nie ma błędu...

U mnie wyszło 0, ale nie musi tak być. Dokument Standardu C++ (4.9M) opisuje to gdzieś na stronie 37 w punkcie "3.3.2 Point of declaration". Mówiąc w skrócie - jeśli wystąpiła deklaracja (int i), to za nią zmienna i już istnieje i można jej używać.

Aby się przed tym uchronić należy użyć -Winit-self, albo -Wall. Samo -Wuninitialized tego nie złapie. Jeśli nie wymusimy zmiany ostrzeżeń na błędy (-Werror), to kompilator wygeneruje jedynie ostrzeżenie.

$ g++ main.cxx -o program -Wall
main.cxx: In function ‘int main()’:
main.cxx:3:13: warning: ‘i’ is used uninitialized [-Wuninitialized]
    3 |         int i = i;
      |             ^

Zero

Kiedyś mądry człowiek, który dawno temu studiował kompilatory, opowiadał mi o przeróżnych rzeczach z czasów astronomicznych obliczeń w DOS. W tamtych czasach były inne standardy, inne kompilatory. Historia, która szczególnie utkwiła mi w pamięci, związana była z NULL.
NULL jest makrem, ale jego wartość była różna na różnych środowiskach.
Zdarzały się wtedy takie wypadki jak porównanie wskaźnika do NULL, który miał inny rozmiar.

Równo dwa lata temu pojawił się artykuł The Unix C library API can only be reliably used from C, który omawia wykorzystanie języka Go na OpenBSD. Go nie może w prosty sposób wykorzystywać standardowych mechanizmów, gdyż te częściowo są zaimplementowane w preprocesorze C. Artykuł pokazuje m.in. taką fajną rzecz:

/* OpenBSD */
int *__errno(void);
#define errno (*__errno())

Warningi, kryptografia, dystrybucja oprogramowania

Skoro widzieliśmy już wcześniej warning kompilatora (int i = i), to można tutaj przytoczyć Debian fiasco. Dawno temu maintainer pakietu OpenSSL zauważył warningi przy budowaniu i testowaniu kodu openssl. Ostrzeżenia były generowane, ponieważ kod openssl używał niezainicjalizowanej pamięci jako losowości (źródła entropii). Profiler kodu wyświetlał w związku z tym mnóstwo ostrzeżeń. Ostatecznie opiekun debianowego pakietu openssl postanowił zapobiec tym ostrzeżeniom. Profiler był "happy", ale świat już mniej. Świat przestał być bezpieczny. Brak tych wartości losowych sprawił, że generowane klucze/certyfikaty mogły zawierać identyczne dane. Losowość była "przewidywalna"/mierzalna, można było więc odszyfrować dane. Sprawie nie pomagało to, że większość rozwiązań natury SSL opiera się na kodzie openssl, a wiele systemów bazuje na Debianie. Sprawa zatem automatycznie dotyczyła systemów pochodnych (Ubuntu, itd).

Dzisiaj nadal używamy OpenSSL.
Istnieją również inne biblioteki SSL/TLS (gnutls, libressl, mbed TLS, ...).

Wiele osób w nowych językach programowania (tych memory safe) upatruje również szansy na polepszenie stanu rzeczy w tej wyjątkowo trudnej i wyjątkowo ważnej dziedzinie, jaką jest kryptografia. Nowsze języki dodają wiele abstrakcji zapewniających wyższy stopień gwarancji intergralności danych w programach (obsługa pamięci). Kto wie co przyszłość przyniesie.

Dzisiaj np. biblioteka cryptography w Python do zbudowania (sprawdzałem też przed chwilą w kontenerze docker na Alpine) wymaga oprócz kompilatora C i kodu openssl również kompilatora Rust. Sam kompilator Rust jednak już nie wystarcza, potrzebne jest jeszcze cargo. Programiści języków "rodziny C" i podobnych są coraz bardziej podzieleni. Programiści C nie chcą pisać w C++ i uważają jego abstrakcje za dystrakcje, programiści C++ często zmęczeni przeintelektualizowaniem odkrywają na nowo C, a programiści Rust uważają C i C++ za języki winne błędom w naszym kodzie. Dla tego ogólnego przeintelektualizowania ukuto nawet termin: Cargo cult programming.

Błędne są ostatecznie liczby/kalkulacje. Faktem niezaprzeczalnym jest też to, że w jednym z najpopularniejszych dzisiaj języków 1 + "11" wciąż daje 111 i to się raczej nie zmieni, choć wielu trzyma kciuki.

Faktem jest też to, że większość systemów jest w C. Ataki bywają różne, czasem można poczytać o różnych backdoorach (prawdziwych i domniemanych). Język jak język. IPhone ma sporo kodu Objective-C, Android dużo kodu Java, Windows sporo C++, a Pegasus działa na każdym z nich.

Precyzja liczb

W ramach powyższej sekcji odpoczęliśmy nieco od kodu, wracamy więc do liczb.
Jednym z najczęstszych źródeł błędów programów są błędne algorytmy, niepoprawne lub źle użyte typy danych, założenia odnośnie ich precyzji.

Sprawdźmy przybliżenie liczby π.

Python:

$ python -c "print(22 / 7)"
3.142857142857143

Perl:

$ perl -e "print(22 / 7)"
3.14285714285714

Nodejs:

$ node -pe "22 / 7"
3.142857142857143

bc (Program bc w Linux. /bc - An arbitrary precision calculator language/):

$ echo "22 / 7" | bc -l
3.14285714285714285714

Shell:

$ expr 22 / 7
3

Przykład dotyczący expr zamieściłem kiedyś tutaj.

Rust:

fn main()
{
    let x: f64 = 22.0 / 7.0;
    println!("{}", x);
    println!("{}", 22 / 7);
}
$ rustc int.rs
$ ./int
3.142857142857143
3

Domyślnie int. To by się zgadzało z B.
"underlying machine's natural memory word format, whatever that might B".

Podsumowując tę sekcję - rzec można - niby trywialne sprawy, a jednak z rozważań błędów zaokrągleń znamy dzisiaj terminy Butterly Effect oraz Chaos theory.

Pieniądze

Wiele systemów w świecie finansowym wciąż działa w oparciu o język COBOL. Kod napisano dawno, maszyny są stare... kosztowne jest utrzymanie, ale jakoś to działa.
Wielu ludzi zadawało i zadaje sobie pytanie dlaczego te systemy nie zostaną przepisane np. w języku Java. Programiści COBOL często odpowiadają jednak: "ponieważ nowe języki nie umieją liczyć". COBOL obliczenia prowadzi w trybie "arbitrary precision". Brak mi wiedzy aby w jakikolwiek sposób wypowiadać się tutaj o języku COBOL, ale zademonstruję co nieco posługując się interpreterem Common Lisp (clisp).

[1]> (/ 22 7)
22/7

Cóż innego ma pokazać? Dla niego to atom. Liczba podzielona przez 7 będzie w okresie (142857...).
Zatem:

[2]> (floor 22 7)
3 ;
1

Teraz mówi 3 z resztą. Do Lisp wrócimy jeszcze na końcu tego artykułu.

Przy obliczaniu wartości pieniężnych nie stosujemy typu float. "Wszyscy to wiedzą".
Jeśli każdemu klientowi "urwiemy" jedną setną grosza, to na milionie klientów różnica wyniesie 100zł.
Na plus lub na minus - zależy kto liczy.

>>> (1 / 100 * 0.01) * (10 ** 6)
100.0

Instytucje finansowe mają miliony klientów, liczba transakcji finansowych na świecie ma zatem jeszcze większy rząd wielkości. Dlatego - zależnie od wymaganej precyzji - stosujemy inne typy niż float. Przeważnie jakiś double, decimal, numeric, itd.

Python:

>>> import decimal
>>> from decimal import Decimal
>>> Decimal(22 / 7)
Decimal('3.142857142857142793701541449991054832935333251953125')
>>> import numpy as np
>>> np.float128(22 / 7)
3.1428571428571427937

>>> np.longdouble(np.float128(22 / 7)).as_integer_ratio()
(7077085128725065, 2251799813685248)
>>> 7077085128725065 / 2251799813685248
3.142857142857143

>>> np.float128(22 / 7).tobytes()
b'\x00H\x92$I\x92$\xc9\x00@\x00\x00\x00\x00\x00\x00'
>>> np.longdouble(np.float128(22 / 7)).tobytes()
b'\x00H\x92$I\x92$\xc9\x00@\x00\x00\x00\x00\x00\x00'

Zaokrąglenia, float, Decimal

O zaokrągleniach kwot w polskiej ordynacji podatkowej mówi Art. 63.

>>> def int_pln(kwota):
...     mantysa = kwota % 1
...     return int(kwota) + (0 if mantysa < 0.5 else 1)
...
>>> int_pln(0.5)
1
>>> int_pln(3.5)
4

Błędy zaokrągleń:

Python 3.10.4
>>> round(0.5)  # !
0
>>> round(1.5)  # int_pln(0.5) == 1
2
>>> round(2.5)  # int_pln(2.5) == 3
2
>>> round(3.5)
4

Porównanie:

>>> for x in [0.5, 1.5, 2.5, 3.5, 4.5, 5.5]:
...     x, round(x), int_pln(x), round(x) == int_pln(x)
... 
(0.5, 0, 1, False)
(1.5, 2, 2, True)
(2.5, 2, 3, False)
(3.5, 4, 4, True)
(4.5, 4, 5, False)
(5.5, 6, 6, True)

Python: round()

Note

The behavior of round() for floats can be surprising:
for example, round(2.675, 2) gives 2.67 instead of the expected 2.68.
This is not a bug: it’s a result of the fact that most decimal fractions
can’t be represented exactly as a float.
See Floating Point Arithmetic: Issues and Limitations for more information. 

Sprawdźmy dla Decimal:

>>> from decimal import Decimal
>>> for x in [0.5, 1.5, 2.5, 3.5, 4.5, 5.5]:
...     x = Decimal(x)
...     x, round(x, 1), int_pln(x), round(x, 1) == int_pln(x)
... 
(Decimal('0.5'), Decimal('0.5'), 1, False)
(Decimal('1.5'), Decimal('1.5'), 2, False)
(Decimal('2.5'), Decimal('2.5'), 3, False)
(Decimal('3.5'), Decimal('3.5'), 4, False)
(Decimal('4.5'), Decimal('4.5'), 5, False)
(Decimal('5.5'), Decimal('5.5'), 6, False)
>>> Decimal(0.5) == 0.5
True
>>> Decimal(0.5) % 1
Decimal('0.5')
>>> (Decimal(0.5) % 1) < 0.5
False
>>> 0 if (Decimal(0.5) % 1) < 0.5 else 1
1
>>> round(Decimal(0.5))
0
>>> round(Decimal(1.5))
2
>>> round(Decimal(2.5))
2
>>> round(Decimal(3.5))
4

To samo. Co na to dokumentacja round()?

Help on built-in function round in module builtins:

round(number, ndigits=None)
    Round a number to a given precision in decimal digits.

    The return value is an integer if ndigits is omitted or None.  Otherwise
    the return value has the same type as the number.  ndigits may be negative.

Dokumentacja float.__round__() mówi o zaokrągleniu do najbliższej parzystej:

Help on method_descriptor:

__round__(self, ndigits=None, /)
    Return the Integral closest to x, rounding half toward even.

    When an argument is passed, work like built-in round(x, ndigits).

Implementacja w floatobject.c.

Dokumentacja do Decimal.__round__() niczego nie mówi:

Help on method_descriptor:

__round__(...)

Sam decimal pozwala jednak na definicję sposobu zaokrąglenia:

>>> import decimal
>>> decimal.ROUND_
decimal.ROUND_05UP       decimal.ROUND_FLOOR      decimal.ROUND_HALF_UP
decimal.ROUND_CEILING    decimal.ROUND_HALF_DOWN  decimal.ROUND_UP
decimal.ROUND_DOWN       decimal.ROUND_HALF_EVEN 

pydecimal.

Typy danych, overflow, maximum

Przy omówieniu bibliotek dynamicznych przedstawiałem krótko ABI C++. Funkcje tam prezentowane były typu int: int my_function(...). Wychodąc od prostego przypadku dodawałem kolejne typy: unsigned long, następnie double. Nie chciałem umieścić tam typu void przy definicji funkcji, ani pominąć nazw argumentów. Wymagałoby to dygresji na temat argumentów bez nazw, czy unreachable code. Tak, kompilator ciało funkcji może też po prostu usunąć. Skoro kod nigdy niczego nie będzie robił, to najwidoczniej nie jest tam potrzebny. C Compilers Disprove Fermat’s Last Theorem.

Niemniej, zmieniłem w tamtym artykule typ funkcji z int na long double i teraz kod nadal jest niepoprawny, a dodatkowo nie jest już przejrzysty jak wcześniej.
W C nie ma "unsigned long long double", zatem ograniczyłem to do: "nie ma w C, więc się nie da".
Przeanalizujemy teraz takie różnice. Punktem wyjściowym niech będzie dokładnie ten przykład z funkcją typu int (czyli: int my_function()).

C++:

#include <iostream>
#include <limits>


int my_function(int x, unsigned long y, double z)
{
    return x + y + z;
}

int main()
{
    std::cout << my_function(std::numeric_limits<int>::max(),
                             std::numeric_limits<unsigned long>::max(),
                             std::numeric_limits<double>::max())
              << std::endl;

    return 0;
}

Kompiluję, uruchamiam, patrzymy:

$ g++ example.cxx -o example -Wall -Werror -Wpedantic
$ ./example
MAX INT    2147483647
MAX ULONG  18446744073709551615
MAX DOUBLE 1.79769e+308
-2147483648

Wynik jest ujemny, kompilator jest zadowolony z kodu, nie protestuje. Funkcja jest typu int, wynik jest zatem typu int. Przekręcił się.

Zmieniam typ funkcji na std::size_t:

//...
std::size_t my_function(int x, unsigned long y, double z) { return x + y + z; }

Kompiluję, uruchamiam, patrzymy:

$ g++ example.cxx -o example -Wall -Werror -Wpedantic
$ ./example
MAX INT    2147483647
MAX ULONG  18446744073709551615
MAX DOUBLE 1.79769e+308
0

Zero. To też ciekawe.
Zmieniam typ funkcji na long double:

//...
long double my_function(int x, unsigned long y, double z) { return x + y + z; }

Kompiluję, uruchamiam, patrzymy:

$ clang++-11 example.cxx -o example -Wall -Werror -Wpedantic
$ ./example
MAX INT    2147483647
MAX ULONG  18446744073709551615
MAX DOUBLE 1.79769e+308
1.79769e+308

Wynik jest taki sam jak maximum typu long double. Krótki reasearch mówi nam, że wsparcie dla typu long double pojawiło się w Standardzie C w roku 1989, jednak szczegóły implementacyjne zależne są od kompilatorów. Różne kompilatory implementują go po swojemu (rozmiary).

Sama funkcja jest typu long double, zatem wynik i tak nie może przekroczyć maximum dla tego typu.
Pozbywam się funkcji. W kodzie nie ma też teraz żadnej zmiennej:

#include <iostream>
#include <limits>


int main()
{
    using namespace std;

    cout << (
        numeric_limits<int>::max() +
        numeric_limits<unsigned long>::max() +
        numeric_limits<double>::max()
    ) << endl;

    return 0;
}

Kompiluję, uruchamiam, patrzymy:

$ clang++-11 example.cxx -o example -Wall -Werror -Wpedantic
$ ./example
1.79769e+308

Nic z tego raczej nie będzie, prawda? Możemy upewnić się jeszcze za pomocą asercji.

#include <limits>
#include <cassert>

int main()
{
    using NL_INT = std::numeric_limits<int>;
    using NL_ULONG = std::numeric_limits<unsigned long>;
    using NL_DOUBLE = std::numeric_limits<double>;

    assert((NL_INT::max() + NL_ULONG::max() + NL_DOUBLE::max()) > NL_DOUBLE::max());

    return 0;
}

Kompiluję, uruchamiam, patrzymy:

$ clang++-11 example.cxx -o example -Wall -Werror -Wpedantic
$ ./example
example: example.cxx:11: int main(): Assertion `(NL_INT::max() + NL_ULONG::max() + NL_DOUBLE::max()) > NL_DOUBLE::max()' failed.
Aborted

Miało być większe, a nie jest. Crash.
Co na to Lisp? Też niewiele...

$ clisp -q
[1]> (prin1 most-positive-double-float)
1.7976931348623157d308
1.7976931348623157d308
[2]> (+ 2147483647 18446744073709551615 most-positive-double-float)
1.7976931348623157d308

[3]> (> (+ 1 most-positive-double-float) most-positive-double-float)
NIL
[4]> (< (+ 1 most-positive-double-float) most-positive-double-float)
NIL
[5]> (= (+ 1 most-positive-double-float) most-positive-double-float)
T

Równe to równe. T.
Nie poddajemy się. "Jeśli czegoś nie ma w shellu, to jest w C, albo się nie da". Czas sięgnąć po armatę na muchę.
Mam przecież tyle pamięci w maszynie, żeby zmieścić jedną liczbę, bez przesady. Nawet jeśli miałbym operować na stringach reprezentujących te dodawane liczby.

#include <iostream>
#include <limits>
#include <string>
#include <gmp.h>

int main()
{
    const int max_int = std::numeric_limits<int>::max();
    const unsigned long max_ulong = std::numeric_limits<unsigned long>::max();
    const double max_double = std::numeric_limits<double>::max();

    const int precision = 1024;
    mpf_set_default_prec(precision);
    mpf_t mi, mul, md, result;

    mpf_init2(mi, precision);
    mpf_init2(mul, precision);
    mpf_init2(md, precision);
    mpf_init2(result, precision);

    mpf_set_d(mi, max_int);
    mpf_set_d(mul, max_ulong);
    mpf_set_d(md, max_double);

    mp_exp_t exp(1);

    /*  // sanity check (human intervention)
        std::cout << mpf_get_str(NULL, &exp, 10, precision, mi) << std::endl;
        std::cout << mpf_get_str(NULL, &exp, 10, precision, mul) << std::endl;
        std::cout << mpf_get_str(NULL, &exp, 10, precision, md) << std::endl;
    */

    mpf_add(result, result, mi);
    mpf_add(result, result, mul);
    mpf_add(result, result, md);

    std::string number_string(mpf_get_str(NULL, &exp, 10, precision, result));

    std::cout << "And the answer is... "  // hopefully
              << number_string
              << std::endl;
}

GMP posiada też interfejs C++, jednak jest mi tutaj wszystko jedno. Kompiluję jako C++, używam interfejsu C, a kod wygląda trochę jak assembler.

Precyzję ustawiam arbitralnie na 1K.

  • mpz_t i mpf_t to typ dla zmiennych (całkowite, rzeczywiste)
  • mpf_set_d() wpisuje wartości w zmienne rzeczywiste
  • mpf_add() umie dodawać liczby
  • mpf_get_str() zwraca tę liczbę jako napis. Dokumentacja twierdzi, że funkcja obcina zera.

Uwaga: Nie gwarantuję, że ten kod jest poprawny. Chwilę nad nim główkowałem, ale sprawdź samodzielnie. Let it B, show must go on.

Kompiluję, uruchamiam, patrzymy:

$ g++ gmptest.cxx -o gmptest -Wall -Wextra -Werror -Wpedantic -lgmp
./gmptest
And the answer is... 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881268850770259981893631

No proszę, udało się. A da się jakoś prościej?
Czy w Python dałoby się "normalnie", na liczbach?

>>> 2147483647 + 18446744073709551616 + 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368
179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881268850770259981893631

Hmmm, impressive.

>>> "{:.2e}".format(1.79769e+308)
'1.80e+308'
>>> import numpy as np
>>> result = np.float128(number)
>>> result
1.7976931348623157081e+308
>>> print(np.float128.__doc__)
Extended-precision floating-point number type, compatible with C
    'long double' but not necessarily with IEEE 754 quadruple-precision.
[...]

Do tego C powyżej przydałyby się testy... cóż... "kompiluje się, to pewnie i działa".

Na koniec przyjrzyjmy się samej liczbie, którą otrzymaliśmy z kodu C/C++ przy pomocy GMP. Podzielę ją dla przykładu przez liczbę wszystkich ludzi.

>>> people = 7.9 * (10 ** 9)
>>> number / people
2.2755609302054628e+298
>>> int(number / people)
22755609302054627631408578701146276926397258533012094002546064507305807052436943250084974266267307618457821235481403615364735301295708500652717973400425367585055978769134942547648183194357569192825500885460415386857197353108878084469047489517996067421402320818049511849335908637414868530268525821952
>>> len(str(int(number / people)))
299

Spora liczba, ale chyba już błądzimy i nikt nie wie co się dzieje.

Fun

Według Popular Mechanics w Universum jest 3.28 * 10 ** 80 cząstek (warto zadawać takie "głupie" pytania).

Tutaj nawet Lisp wysiada:

[1]> (* 3.28 (expt 10 80))

*** - *: floating point overflow

Ale Python czasem umie takie rzeczy, widzieliśmy już wcześniej:

>>> 3.28 * 10 ** 80
3.28e+80
>>> (3.28 * 10 ** 80) / number
1.8245605639759054e-228

Na koniec zatem posłuchajmy jedynie jak brzmi nazwa naszej liczby.
Archimedes z pewnością byłby ciekaw. Lisp tutaj też - niestety - wysiada. Zatem albo zmniejszę rząd wielkości w Python:

>>> number * (10 ** (-293))
1797693134862315.8

... albo Lisp zrobi ile umie:

(format t "~R" 179769313486231570814527423731704356798070567525844996598917476803)

Odczytanie tej krótszej liczby zajmuje prawie minutę.

Video: Vigintillion

Z dźwiękiem:

Video: MP4, 2.0M, 1920x1080. Duration: 00:01:00 Link

Podsumowanie

Już zsumowaliśmy.
NIL

Powyżej jednak nieco oszukaliśmy twierdząc, że "jeśli czegoś nie ma w shellu, to jest w C, albo się nie da". Zwracam tutaj uwagę na 'single quotes'. Teraz trzeba. Jest też nasz ulubiony xor, ale tutaj faktycznie w roli potęgowania.

$ echo '(3.28 * 10 ^ 80)' | bc -l
32800000000000000000000000000000000000000000000000000000000000000000\
0000000000000.00
$ echo '179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881268850770259981893631 / (3.28 * 10 ^ 80)' | bc -l
54807717526290113053209580406007425853070294977391767255767523415596\
72584756967645139925568072160919864038382729097026348607357527117810\
45025815691169321698128200946879729518525138689299967466292914233257\
485164458245680600314704.11026161146137842484

Ten to umie takie rzeczy. COBOL być może też to umie. Więc całę zabawę z C i GMP można tutaj odpuścić, sprawę można załatwić krótkim bc. Oczywiście nie od razu, gdyż bc nie obsługuje "scientific notation". Można podobno regexem.

Niemniej, przekroczyliśmy liczbę cząstek w Universum. Pewnie dałoby się to policzyć jeszcze raz w shellu.

To nie koniec liczb w całej serii (nie było przecież jeszcze nawet dat!) lecz na teraz wystarczy tego C.
Tym, którzy dotarli do podsumowania - gratuluję ponownie, a poniżej zamieszczam jeszcze ciekawostki.

  • Cling
    Cling is an interactive C++ interpreter, built on the top of LLVM and Clang libraries.
    [...] One of Cling’s main goals was [...] backward-compatibility with CINT.

Sam CINT umiał nawet trochę szablonów, można było też używać bibliotek. Skrypty w C++.

  • Ch
    Ch is a C/C++ interpreter and scripting language environment.
    It is used by teachers, students, engineers and scientists around the world to learn math.

A słonik? Słonik jest podstępny jak wąż:

postgres=> select ((pow(10, 80) * 3.28) / 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881268850770259981893631);
        ?column?
-------------------------
 1.8245605639759054e-228
(1 row)

postgres=>

Dość już tego! W następnej części "integrity series" będzie trochę więcej zagadnień integracyjnych.