# Kết quả trải nghiệm, đánh giá, kết luận ## Lỗ hổng nguyên với số `float` Từ những phân tích về lý thuyết ở bên trên, ta thấy số float sẽ bị nhảy cóc khi số nguyên với sô mũ là 24, nhóm tiến hành cho chạy đoạn code số nguyên từ $2^{24}$ đến $2^{25}$, ghi kết quả liên tục vào file `output-float.txt` để xem kết quả. Với số nhỏ chúng ta có thể dùng số nguyên 32 hoặc 64 bit để biều diễn, để kiểm tra với số nguyên lớn hơn ta có thể sử dụng các thư viện số nguyên lớn để kiểm tra ```java public static void main(String[] args) throws Exception { int a = 0b0000000_10000000_00000000_00000000; // 2^24 int b = 0b00000001_00000000_00000000_00000000; // 2^25 System.out.printf("a: %d, b: %d\n", a, b); WorkerThreadTick workerThreadTick = new WorkerThreadTick(); BufferedWriter bufferedWriter = getBufferWriter("output-float.txt"); workerThreadTick.start(); long startTime = System.currentTimeMillis(); for (int c = a; c <= b; c++) { bufferedWriter.write(String.format("d:%08d, float:%8.1f\n", d, (float)d)); bufferedWriter.flush(); } long endTime = System.currentTimeMillis(); workerThreadTick.stopTick(); bufferedWriter.write(String.format("done in: %d seconds", (endTime - startTime) / 1000)); bufferedWriter.flush(); bufferedWriter.close(); } ``` Mã nguồn: [FloatTest.java](https://github.com/ngthotuan/floatting-point-testing/blob/master/FloatTest.java) Kết quả in ra console: ```sh a: 16777216, b: 33554432 exec_time: 0 seconds exec_time: 1 seconds exec_time: 2 seconds ... exec_time: 51 seconds ``` Kết quả file output-float.txt: ```file= d:16777216, float:16777216.0 d:16777217, float:16777216.0 d:16777218, float:16777218.0 d:16777219, float:16777220.0 d:16777220, float:16777220.0 d:16777221, float:16777220.0 d:16777222, float:16777222.0 d:16777223, float:16777224.0 d:16777224, float:16777224.0 d:16777225, float:16777224.0 d:16777226, float:16777226.0 ... d:33554422, float:33554422.0 d:33554423, float:33554424.0 d:33554424, float:33554424.0 d:33554425, float:33554424.0 d:33554426, float:33554426.0 d:33554427, float:33554428.0 d:33554428, float:33554428.0 d:33554429, float:33554428.0 d:33554430, float:33554430.0 d:33554431, float:33554432.0 d:33554432, float:33554432.0 done in: 51 seconds ``` > Ta có thể thấy số float từ $2^{24}$ đến $2^{25}$ bị nhảy 1 khoảng là $2^{x - 23}$ = $2^{24 - 23}$ = $2^1$ = 2, đúng với công thức đã được trình bày ở trên phần lý thuyết Chúng ta có thể thay 2 số a,b ở trên để kiểm thử với các số nguyên lớn hơn, kết quả thu được có thể biểu diễn ở bảng sau: | a |b | Khoảng cách nhảy cóc | | -------- | -------- | -------- | | $2^{24}$ | $2^{25}$ | $2^{1}$ | | $2^{25}$ | $2^{26}$ | $2^{2}$ | | ... | ... | ... | | $2^{63}$ | $2^{64}$ | $2^{39}$ | Với các số nguyên lớn hơn 64 bit kêt quả cũng tương tự, nhóm sẽ sử dụng thư viện số nguyên lớn để kiểm tra, lý do độ lớn số floating point theo `lý thuyết` biếu diễn số nguyên lớn tới khoảng ${3.4}*{10}^{38}$. Suy ra ta cần số nguyên lớn khoảng $log_2({3.4}*{10}^{38})$ ~ 128 bit, nhóm sử dụng thư viện số nguyên lớn để demo trực quan có thể xem chi tiết tại [đây](https://ft.ftool.me/float) > Vậy liệu số nguyên từ $2^0$ đến $2^{23}$ thật sự là không bị nhảy cóc khi biếu diễn bắng sô `float`, chúng ta sẽ tiến hành sửa lại đoạn code ở trên lại như bên dưới và xem kết quả: ```java int a = 0b0000000_10000000_00000000_00000000; // 2^23 int b = 0b0000001_00000000_00000000_00000000; // 2^24 ``` Kết quả in ra console: ```sh a: 8388608, b: 16777216 exec_time: 0 seconds exec_time: 1 seconds exec_time: 2 seconds ... exec_time: 26 seconds ``` Kết quả file output-float.txt: ```file= d:08388608, float:8388608.0 d:08388609, float:8388609.0 d:08388610, float:8388610.0 d:08388611, float:8388611.0 ... d:16777215, float:16777215.0 d:16777216, float:16777216.0 done in: 26 seconds ``` ## Lỗ hổng nguyên với số `double` Tương tự số float thì số double sẽ bị nhảy cóc khi biểu diễn số nguyên với số mũ là từ 53 trở đi, nhóm tiến hành cho chạy đoạn code số nguyên từ $2^{53}$ đến $2^{54}$, ghi kết quả liên tục vào file `output-double.txt` để xem kết quả. Đối với số nguyên lớn hơn $2^{54}$ chũng ta chỉ có thể dùng số nguyên 64 bit để biều diễn (lý do tại sao ở đây chúng ta dùng dữ liệu để biểu diễn 2 số a, b là kiểu `long`, để kiểm tra với số nguyên lớn hơn ta có thể sử dụng các thư viện số nguyên lớn để kiểm tra ```java public static void main(String[] args) throws Exception { long a = 0b0100000_00000000_00000000_00000000_00000000_00000000_00000000L; // 2^52 long b = 0b1000000_00000000_00000000_00000000_00000000_00000000_00000000L; // 2^53 System.out.printf("a: %d, b: %d\n", a, b); WorkerThreadTick workerThreadTick = new WorkerThreadTick(); BufferedWriter bufferedWriter = getBufferWriter("output-double.txt"); workerThreadTick.start(); long startTime = System.currentTimeMillis(); for (long d = a; d <= b; d++) { bufferedWriter.write(String.format("d:%08d, double:%8.1f\n", d, (double)d)); bufferedWriter.flush(); } long endTime = System.currentTimeMillis(); workerThreadTick.stopTick(); bufferedWriter.write(String.format("done in: %d seconds", (endTime - startTime) / 1000)); bufferedWriter.flush(); bufferedWriter.close(); } ``` Mã nguồn: [DoubleTest.java](https://github.com/ngthotuan/floatting-point-testing/blob/master/DoubleTest.java) Kết quả in ra console: ```sh a: 9007199254740992, b: 18014398509481984 exec_time: 0 seconds exec_time: 1 seconds exec_time: 2 seconds ... exec_time: 1334 seconds ... ``` Kết quả file output-double.txt: ```file= d:9007199254740992, double:9007199254740992.0 d:9007199254740993, double:9007199254740992.0 d:9007199254740994, double:9007199254740994.0 d:9007199254740995, double:9007199254740996.0 d:9007199254740996, double:9007199254740996.0 d:9007199254740997, double:9007199254740996.0 d:9007199254740998, double:9007199254740998.0 d:9007199254740999, double:9007199254741000.0 d:9007199254741000, double:9007199254741000.0 d:9007199254741001, double:9007199254741000.0 d:9007199254741002, double:9007199254741002.0 ... ``` > Do quá trình chạy rất lâu nên chũng ta sẽ dừng chương trình và xem kết quả thì thấy sô nguyên từ $2^{53}$ đến $2^{54}$ khi biểu diễn bằng số double sẽ bị nhảy số bước là 2 đúng với công thức $2^{x - 52}$ = $2^{53 - 52}$ = $2^1$ = 2 đã được trình bày ở trên phần lý thuyết Chúng ta có thể thay 2 số a,b ở trên để kiểm thử với các số nguyên lớn hơn, kết quả thu được có thể biểu diễn ở bảng sau: | a |b | Khoảng cách nhảy cóc | | -------- | -------- | -------- | | $2^{53}$ | $2^{54}$ | $2^{1}$ | | $2^{54}$ | $2^{55}$ | $2^{2}$ | | ... | ... | ... | | $2^{63}$ | $2^{64}$ | $2^{10}$ | Với các số nguyên lớn hơn 64 bit kêt quả cũng tương tự, nhóm sẽ sử dụng thư viện số nguyên lớn để kiểm tra, bản demo trực quan có thể xem chi tiết tại [đây](https://ft.ftool.me/double) ## Chương trình kiểm tra số nguyên nhập vào khi biểu diễn bằng số float/double có còn đúng không? - Ý tưởng chính là sẽ ép kiểu số nguyên d về dạng sồ float/double để xem số này có bằng với số nguyên nhập vào hay không? - Nếu kết quả bằng nhau thì sô được biểu diễn đúng - Nếu kết quả không bằng nhau thì số không được biểu diễn đúng - Chỉ thử được với số nguyên 64 bit - Đoạn [mã nguồn](https://github.com/ngthotuan/floatting-point-testing/blob/master/TestFloatingPoint.java) thể hiện việc này ```java public static void main(String[] args) { System.out.print("Enter a number: "); long number = new Scanner(System.in).nextLong(); System.out.printf("d: %d\n" , number); System.out.printf("float: %.1f\n" , (float)number); System.out.printf("double: %.1f\n" , (double)number); System.out.println("d == float: " + (number == (long)(float)number)); System.out.println("d == double: " + (number == (long)(double)number)); } ``` - Trường hợp số được biểu diễn đúng: ```sh Enter a number: 16777216 d: 16777216 float: 16777216.0 double: 16777216.0 d == float: true d == double: true ``` - Trường hợp số được biểu diễn đúng bởi số double nhưng sai số khi đối với float: ```java Enter a number: 16777217 d: 16777217 float: 16777216.0 double: 16777217.0 d == float: false d == double: true ``` - Trường hợp số được biểu diễn sai cả số double, float (số bị nhảy cóc lớn hơn $2^{53}$): ```java Enter a number: 9007199254740993 d: 9007199254740993 float: 9007199254740992.0 double: 9007199254740992.0 d == float: false d == double: false ``` - Để trực quan thì nhóm cũng có viết tool để kiểm tra (phù hợp cả với số nguyên lớn hơn 64 bit) tại [đây](https://ft.ftool.me/test) ## Thử nghiệm số sự nhảy cóc số thực với một vài ngôn ngữ lập trình khác - Các ví dụ về mã nguồn để demo nhóm chủ yếu sử dụng ngôn ngữ lập trình là Java, tuy nhiên để trải nghiệm nhiều trường hợp hơn nữa thì nhóm quyết định thử với một số ngôn ngữ lập trinh khác ### C/C++ - Số float/double của ngôn ngữ C/C++ cũng bị nhảy cóc tương tự như float/double trong ngôn ngữ Java - Đoạn [mã nguồn](https://github.com/ngthotuan/floatting-point-testing/blob/master/FloatTest.cpp) kiểm tra sô float từ $2^{24}$ đến $2^{25}$ với bước nhảy là 2: ```c/c++ #include <stdio.h> int main() { int limit = 10; int a = 0b00000001'00000000'00000000'00000000; // 2^24 int b = 0b00000010'00000000'00000000'00000000; // 2^25 printf("a: %d, b: %d\n", a, b); for (int c = a; c <= b && limit-- >= 0; c++) { printf("c=%08d,(float)c=%8.1f\n", c, (float)c); } return 0; } ``` > Kết quả trên console: ```sh= a: 16777216, b: 33554432 d:16777216, float:16777216.0 d:16777217, float:16777216.0 d:16777218, float:16777218.0 d:16777219, float:16777220.0 d:16777220, float:16777220.0 d:16777221, float:16777220.0 d:16777222, float:16777222.0 d:16777223, float:16777224.0 d:16777224, float:16777224.0 d:16777225, float:16777224.0 d:16777226, float:16777226.0 ``` - Đoạn [mã nguồn](https://github.com/ngthotuan/floatting-point-testing/blob/master/DoubleTest.cpp) kiểm tra sô double từ $2^{53}$ đến $2^{54}$ với bước nhảy là 2: ```c/c++ #include <stdio.h> int main() { int limit = 10; long a = 0b0100000'00000000'00000000'00000000'00000000'00000000'00000000L; // 2^53 long b = 0b1000000'00000000'00000000'00000000'00000000'00000000'00000000L; // 2^54 printf("a: %ld, b: %ld\n", a, b); for (long d = a; d <= b && limit-- >= 0; d++) { printf("d:%08ld, float:%8.1lf\n", d, (double)d); } return 0; } ``` > Kết quả trên console: ```sh= a: 9007199254740992, b: 18014398509481984 d:9007199254740992, float:9007199254740992.0 d:9007199254740993, float:9007199254740992.0 d:9007199254740994, float:9007199254740994.0 d:9007199254740995, float:9007199254740996.0 d:9007199254740996, float:9007199254740996.0 d:9007199254740997, float:9007199254740996.0 d:9007199254740998, float:9007199254740998.0 d:9007199254740999, float:9007199254741000.0 d:9007199254741000, float:9007199254741000.0 d:9007199254741001, float:9007199254741000.0 d:9007199254741002, float:9007199254741002.0 ``` ### Javascript - Số trong Javascript không phân theo kiểu dữ liệu là gì, tuy nhiên số nguyên trong JS vẫn dùng chuẩn IEEE-754 với độ chính xác kép nên lỗ hổng nguyên vẫn xảy ra, ta có thể hiểu số trong JS khi biểu diễn bị nhảy cóc giống như số `double` trong C/C++ hay Java: - Đoạn [mã nguồn](https://github.com/ngthotuan/floatting-point-testing/blob/master/NumberTest.js) sau minh họa cho việc đó: ```javascript const a = 9007199254740992; // 2^53 let limit = 16; for (let i = 1; i <= limit; i++) { console.log({a, i, 'a+i': a+i}); } ``` > Kết quả trên console: ```sh { a: 9007199254740992, i: 1, 'a+i': 9007199254740992 } { a: 9007199254740992, i: 2, 'a+i': 9007199254740994 } { a: 9007199254740992, i: 3, 'a+i': 9007199254740996 } { a: 9007199254740992, i: 4, 'a+i': 9007199254740996 } { a: 9007199254740992, i: 5, 'a+i': 9007199254740996 } { a: 9007199254740992, i: 6, 'a+i': 9007199254740998 } { a: 9007199254740992, i: 7, 'a+i': 9007199254741000 } { a: 9007199254740992, i: 8, 'a+i': 9007199254741000 } { a: 9007199254740992, i: 9, 'a+i': 9007199254741000 } { a: 9007199254740992, i: 10, 'a+i': 9007199254741002 } { a: 9007199254740992, i: 11, 'a+i': 9007199254741004 } { a: 9007199254740992, i: 12, 'a+i': 9007199254741004 } { a: 9007199254740992, i: 13, 'a+i': 9007199254741004 } { a: 9007199254740992, i: 14, 'a+i': 9007199254741006 } { a: 9007199254740992, i: 15, 'a+i': 9007199254741008 } { a: 9007199254740992, i: 16, 'a+i': 9007199254741008 } ``` > Ta có thể thấy số trong Javascript bị nhảy bước là từ $2^{53}$ tương tự như số double đã trình bày ở trên ## Tool trải nghiệm sự nhảy cóc của số floating point > Trong quá trình tìm hiểu và tinh toán nhóm có tạo ra 1 tool nhỏ để có thể trải nghiệm dễ dàng phần sự nhảy cóc của số nguyên khi biểu diễn bằng số floating-point. Link tool tại [đây](https://ft.ftool.me/) ### Kiểm tra sự nhảy cóc của số `float` trong đoạn từ $2^x$ đến $2^y$ - Bước 1: Chọn tính năng [Floating Point - Float](https://ft.ftool.me/float) ![](https://i.imgur.com/shND1d6.png) - Bước 2: Chọn số mũ x và y (chọn x > y nếu muốn xem giá trị giảm dần). Lưu ý nên chọn giới hạn số lượng số muốn xem, nếu không nhập thì sẽ hiện tất cả, cẩn thận sẽ bị treo trình duyệt nếu số quá nhiều ![](https://i.imgur.com/fTByRHG.png) - Bước 3: Nhấn `Xem kết quả` để xem, trong ví dụ này đang xem số trong đoạn [$2^{23}$,$2^{24}$] lấy giới hạn là 1024 ![](https://i.imgur.com/wRNMzY0.png) ### Kiểm tra sự nhảy cóc của số `double` trong đoạn từ $2^x$ đến $2^y$ (tương tự số float) - Bước 1: Chọn tính năng [Floating Point - Double](https://ft.ftool.me/double) ![](https://i.imgur.com/xkL5vcy.png) - Bước 2: Chọn số mũ x và y (chọn x > y nếu muốn xem giá trị giảm dần). Ở đây ví dụ chọn x = 54 > y = 53, limit = 100 ![](https://i.imgur.com/tZXjBhZ.png) - Bước 3: Nhấn `Xem kết quả` để xem, trong ví dụ này đang xem số giảm dần từ $2^{54}$ về $2^{53}$ ![](https://i.imgur.com/LieKT19.png) ### Kiểm tra 1 số nguyên d có biểu diễn đúng khi chuyển thành số float/double hay không. - Bước 1: Chọn tính năng [Floating Point - Test Number](https://ft.ftool.me/test) ![](https://i.imgur.com/CBNaoi0.png) - Bước 2: Nhập vào số cần kiểm tra ![](https://i.imgur.com/mpBGuxS.png) - Bước 3: Nhấn `Kiểm tra` để xem kết quả ![](https://i.imgur.com/oFuzCK3.png)