Wykład 8:
Obliczenia całkowite i zmiennoprzecinkowe

Mariusz Chilmon
m.chilmon@amw.gdynia.pl
uc.vmario.org

Operacje całkowite

Zakresy rejestrów

2
1 bit

Zakresy rejestrów

2
1 bit
256
8 bitów

Zakresy rejestrów

2
1 bit
256
8 bitów
65 536
16 bitów

Zakresy rejestrów

2
1 bit
256
8 bitów
65 536
16 bitów
4 294 967 296
32 bitów

Zakresy rejestrów

2
1 bit
256
8 bitów
65 536
16 bitów
4 294 967 296
32 bitów
18 446 744 073 709 551 616
64 bity

Typy całkowite w C i C++

Typy całkowite w Rust

Typy o ustalonej szerokości w C i C++

Typy o ustalonej szerokości w C i C++

Kod U2 (two's complement)

  • aby zamienić liczbę na przeciwną, należy zanegować bity i dodać 1
  • jest tylko jedno zero
  • operacje na liczbach ujemnych nie wymagają specjalnego traktowania
  • wynik operacji automatycznie jest poprawną wartością U2

Operacje zmiennoprzecinkowe

Standard IEEE 754

  • format zapisu liczb i wartości specjalnych (±0, ±∞, NaN)
  • arytmetyka i inne operacje (np. trygonometryczne)
  • sposób zaokrąglania
  • obsługa błędów (dzielenie przez zero, przepełnienie)

Podstawowe typy IEEE 754

  • 64 bity — podwójna precyzja (double)
    • min subnormal: \(\pm 4{,}94 \cdot 10^{−324}\)
    • min normal: \(\pm 2{,}23 \cdot 10^{−308}\)
    • max: \(\pm 1{,}80 \cdot 10^{308}\)
  • 32 bity — pojedyncza precyzja (float)
    • min subnormal: \(\pm 1{,}40 \cdot 10^{−45}\)
    • min normal: \(\pm 1{,}18 \cdot 10^{−38}\)
    • max: \(\pm 3{,}40 \cdot 10^{38}\)

Przykłady typów zmiennoprzecinkowych

Błąd zaokrąglania


		#include <cinttypes>
		#include <iostream>
		using namespace std;

		int main() {
			double foo = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 
					+ 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
			uint64_t foo_int = foo;
			cout << foo_int << endl;
			// 🠞 0
		}
	

Błąd zaokrąglania


		#include <cinttypes>
		#include <iostream>
		#include <iomanip>
		using namespace std;

		int main() {
			cout << 0.1 << endl;
			// 🠞 0.1

			cout << setprecision(40);

			cout << 0.1 << endl;
			// 🠞 0.1000000000000000055511151231257827021182

			cout << 0.1 + 0.1 + 0.1 + 0.1 + 0.1
					+ 0.1 + 0.1 + 0.1 + 0.1 + 0.1 << endl;
			// 🠞 0.9999999999999998889776975374843459576368
			
			cout << 0.1f << endl;
			// 🠞 0.100000001490116119384765625
			cout << 0.1f + 0.1f + 0.1f + 0.1f + 0.1f
					+ 0.1f + 0.1f + 0.1f + 0.1f + 0.1f << endl;
			// 🠞 1.00000011920928955078125

			cout << 0.5 << endl;
			// 🠞 0.5
			cout << 0.5 + 0.5 + 0.5 + 0.5 + 0.5
					+ 0.5 + 0.5 + 0.5 + 0.5 + 0.5 << endl;
			// 🠞 5
		}
	

Cyfry znaczące


		#include <cinttypes>
		#include <iostream>
		#include <iomanip>
		using namespace std;

		int main() {
			cout << setprecision(40);

			uint64_t foo = UINT64_C(12345678901234567890);
			cout << foo << endl;
			// 🠞 12345678901234567890

			double foo_double = foo;
			cout << foo_double << endl;
			// 🠞 12345678901234567168

			float foo_float{foo};
			cout << foo_float << endl;
			// 🠞 12345679395506094080
			
			double bar_huge = 1234567890e100;
			cout << bar_huge << endl;
			// 🠞 1.234567890000000078944836269502040274001e+109

			double bar_tiny = 1234567890e-100;
			cout << bar_tiny << endl;
			// 🠞 1.2345678900000001211954157757914595974e-91

			double bar_inf = 1234567890e300;
			cout << bar_inf << endl;
			// 🠞 inf
		}
	

Porównywanie wartości zmiennoprzecinkowych


		#include <cinttypes>
		#include <iostream>
		#include <iomanip>
		using namespace std;

		int main() {
			cout << setprecision(40);

			double sum = 0.1 + 0.1 + 0.1 + 0.1 + 0.1
					+ 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
			cout << sum << endl;
			// 🠞 0.9999999999999998889776975374843459576368

			double multiplication = 0.1 * 10;
			cout << multiplication << endl;
			// 🠞 1
		 
			if (sum == multiplication) {
				cout << "OK" << endl;
			} else {
				cout << "Error" << endl;
			}
			// 🠞 Error
		 
			constexpr double EPSILON = 1.0 / 1000;
			if (abs(sum - multiplication) < EPSILON) {
				cout << "OK" << endl;
			} else {
				cout << "Error" << endl;
			}
			// 🠞 OK
		}
	

Domyślne typy podczas obliczeń

Domyślny typ całkowity


		#include <cinttypes>
		#include <iostream>
		using namespace std;

		int main() {
			cout << 123'456'789 * 10 << endl;
			// 🠞 1234567890

			cout << 1'123'456'789 * 10 << endl;
			// 🠞 -1650333998 🤨

			cout << 11'123'456'789 * 10 << endl;
			// 🠞 111234567890
		}
	

		foo.cpp: In function ‘int main()’:
		foo.cpp:9:31: warning: integer overflow in expression
		of type ‘int’ results in ‘-1650333998’ [-Woverflow]
			9 |         cout << 1'123'456'789 * 10 << endl;
			  |                 ~~~~~~~~~~~~~~^~~~
	

Domyślny typ całkowity


		#include <cinttypes>
		#include <iostream>
		using namespace std;

		int main() {
			cout << 1'123'456'789ll * 10 << endl;
			// 🠞 11234567890

			cout << 1'123'456'789 * 10ll << endl;
			// 🠞 11234567890

			cout << INT64_C(1'123'456'789) * 10 << endl;
			// 🠞 11234567890
		}
	

Domyślny typ zmiennoprzecinkowy


		#include <cinttypes>
		#include <iostream>
		#include <iomanip>
		using namespace std;

		int main() {
			cout << 1 / 10 << endl;
			// 🠞 0

			double foo = 1 / 10;
			cout << foo << endl;
			// 🠞 0

			cout << 1.0 / 10 << endl;
			// 🠞 0.1

			cout << setprecision(40);

			cout << 1.0 / 10 << endl;
			// 🠞 0.1000000000000000055511151231257827021182

			cout << 1.0f / 10 << endl;
			// 🠞 0.100000001490116119384765625
		}
	

Sposób przeprowadzania obliczeń

FPU (Floating-Point Unit)

  • emulacja programowa
  • zewnętrzny koprocesor
  • wewnętrzny koprocesor
  • integralna część ALU

FPU w procesorach ARM

  • FPU (Floating-Point Unit)
  • VFP (Vector Floating Point)
  • Neon (aka Advanced SIMD Extension)

Szybka odwrotność pierwiastka


		float Q_rsqrt( float number )
		{
			long i;
			float x2, y;
			const float threehalfs = 1.5F;

			x2 = number * 0.5F;
			y  = number;
			i  = * ( long * ) &y; // evil floating point bit level hacking
			i  = 0x5f3759df - ( i >> 1 ); // what the fuck?
			y  = * ( float * ) &i;
			y  = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
		//y  = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed

			return y;
		}