Sự khác biệt giữa NaN yên tĩnh và NaN báo hiệu là gì?


96

Tôi đã đọc về dấu phẩy động và tôi hiểu rằng NaN có thể là kết quả của các phép toán. Nhưng tôi không thể hiểu chính xác đây là những khái niệm gì. sự khác biệt giữa chúng là gì?

Cái nào có thể được tạo ra trong quá trình lập trình C ++? Là một lập trình viên, tôi có thể viết một chương trình gây ra sNaN không?

Câu trả lời:


68

Khi một hoạt động dẫn đến NaN yên tĩnh, không có dấu hiệu nào cho thấy bất kỳ điều gì bất thường cho đến khi chương trình kiểm tra kết quả và thấy NaN. Tức là, quá trình tính toán tiếp tục mà không có bất kỳ tín hiệu nào từ đơn vị dấu chấm động (FPU) hoặc thư viện nếu dấu phẩy động được triển khai trong phần mềm. Một NaN báo hiệu sẽ tạo ra một tín hiệu, thường ở dạng ngoại lệ từ FPU. Liệu ngoại lệ có được ném ra hay không phụ thuộc vào trạng thái của FPU.

C ++ 11 bổ sung một số điều khiển ngôn ngữ đối với môi trường dấu phẩy động và cung cấp các cách chuẩn hóa để tạo và kiểm tra NaN . Tuy nhiên, liệu các điều khiển có được thực hiện không được chuẩn hóa tốt và các ngoại lệ dấu phẩy động thường không được bắt giống như các ngoại lệ C ++ tiêu chuẩn.

Trong các hệ thống POSIX / Unix, các ngoại lệ dấu chấm động thường được bắt bằng cách sử dụng một trình xử lý cho SIGFPE .


34
Thêm vào điều này: Nói chung, mục đích của NaN báo hiệu (sNaN) là để gỡ lỗi. Ví dụ, các đối tượng dấu phẩy động có thể được khởi tạo thành sNaN. Sau đó, nếu chương trình không thành một trong số chúng một giá trị trước khi sử dụng nó, thì một ngoại lệ sẽ xảy ra khi chương trình sử dụng sNaN trong một phép toán số học. Một chương trình sẽ không vô tình tạo ra một sNaN; không có hoạt động bình thường nào tạo ra sNaN. Chúng chỉ được tạo ra đặc biệt cho mục đích có một NaN báo hiệu, không phải là kết quả của bất kỳ phép số học nào.
Eric Postpischil

18
Ngược lại, NaN dành cho lập trình bình thường hơn. Chúng có thể được tạo ra bằng các phép toán thông thường khi không có kết quả số (ví dụ: lấy căn bậc hai của một số âm khi kết quả phải là số thực). Mục đích của chúng nói chung là để cho phép số học diễn ra bình thường. Ví dụ: bạn có thể có một mảng lớn các số, một số đại diện cho các trường hợp đặc biệt mà không thể xử lý bình thường. Bạn có thể gọi một hàm phức tạp để xử lý mảng này và nó có thể hoạt động trên mảng với số học thông thường, bỏ qua các NaN. Sau khi nó kết thúc, bạn sẽ tách các trường hợp đặc biệt để làm việc nhiều hơn.
Eric Postpischil

@wrdieter Cảm ơn bạn, sau đó chỉ có sự khác biệt najor là tạo ra tiếng kêu hay không.
JalalJaberi

@EricPostpischil Cảm ơn bạn đã chú ý đến câu hỏi thứ hai.
JalalJaberi

@JalalJaberi có, ngoại trừ là sự khác biệt chính
wrdieter

34

Trong thực nghiệm, qNaN và sNaN trông như thế nào?

Đầu tiên chúng ta hãy học cách xác định xem chúng ta có sNaN hay qNaN.

Tôi sẽ sử dụng C ++ trong câu trả lời này thay vì C vì nó mang lại sự thuận tiện std::numeric_limits::quiet_NaNstd::numeric_limits::signaling_NaNđiều mà tôi không thể tìm thấy trong C một cách thuận tiện.

Tuy nhiên, tôi không thể tìm thấy một hàm để phân loại nếu NaN là sNaN hay qNaN, vì vậy chúng ta chỉ cần in ra các byte thô NaN:

main.cpp

#include <cassert>
#include <cstring>
#include <cmath> // nanf, isnan
#include <iostream>
#include <limits> // std::numeric_limits

#pragma STDC FENV_ACCESS ON

void print_float(float f) {
    std::uint32_t i;
    std::memcpy(&i, &f, sizeof f);
    std::cout << std::hex << i << std::endl;
}

int main() {
    static_assert(std::numeric_limits<float>::has_quiet_NaN, "");
    static_assert(std::numeric_limits<float>::has_signaling_NaN, "");
    static_assert(std::numeric_limits<float>::has_infinity, "");

    // Generate them.
    float qnan = std::numeric_limits<float>::quiet_NaN();
    float snan = std::numeric_limits<float>::signaling_NaN();
    float inf = std::numeric_limits<float>::infinity();
    float nan0 = std::nanf("0");
    float nan1 = std::nanf("1");
    float nan2 = std::nanf("2");
    float div_0_0 = 0.0f / 0.0f;
    float sqrt_negative = std::sqrt(-1.0f);

    // Print their bytes.
    std::cout << "qnan "; print_float(qnan);
    std::cout << "snan "; print_float(snan);
    std::cout << " inf "; print_float(inf);
    std::cout << "-inf "; print_float(-inf);
    std::cout << "nan0 "; print_float(nan0);
    std::cout << "nan1 "; print_float(nan1);
    std::cout << "nan2 "; print_float(nan2);
    std::cout << " 0/0 "; print_float(div_0_0);
    std::cout << "sqrt "; print_float(sqrt_negative);

    // Assert if they are NaN or not.
    assert(std::isnan(qnan));
    assert(std::isnan(snan));
    assert(!std::isnan(inf));
    assert(!std::isnan(-inf));
    assert(std::isnan(nan0));
    assert(std::isnan(nan1));
    assert(std::isnan(nan2));
    assert(std::isnan(div_0_0));
    assert(std::isnan(sqrt_negative));
}

Biên dịch và chạy:

g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

đầu ra trên máy x86_64 của tôi:

qnan 7fc00000
snan 7fa00000
 inf 7f800000
-inf ff800000
nan0 7fc00000
nan1 7fc00001
nan2 7fc00002
 0/0 ffc00000
sqrt ffc00000

Chúng tôi cũng có thể thực thi chương trình trên aarch64 với chế độ người dùng QEMU:

aarch64-linux-gnu-g++ -ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
qemu-aarch64 -L /usr/aarch64-linux-gnu/ main.out

và điều đó tạo ra cùng một đầu ra, cho thấy rằng nhiều vòm thực hiện chặt chẽ IEEE 754.

Tại thời điểm này, nếu bạn chưa quen với cấu trúc của số dấu phẩy động IEEE 754, hãy xem: Số dấu phẩy động siêu thường là gì?

Trong hệ nhị phân, một số giá trị ở trên là:

     31
     |
     | 30    23 22                    0
     | |      | |                     |
-----+-+------+-+---------------------+
qnan 0 11111111 10000000000000000000000
snan 0 11111111 01000000000000000000000
 inf 0 11111111 00000000000000000000000
-inf 1 11111111 00000000000000000000000
-----+-+------+-+---------------------+
     | |      | |                     |
     | +------+ +---------------------+
     |    |               |
     |    v               v
     | exponent        fraction
     |
     v
     sign

Từ thí nghiệm này, chúng tôi nhận thấy rằng:

  • qNaN và sNaN dường như chỉ được phân biệt bởi bit 22: 1 nghĩa là yên tĩnh và 0 có nghĩa là báo hiệu

  • số nguyên cũng khá tương tự với số mũ == 0xFF, nhưng chúng có phân số == 0.

    Vì lý do này, các NaN phải đặt bit 21 thành 1, nếu không sẽ không thể phân biệt sNaN với dương vô cực!

  • nanf() tạo ra một số NaN khác nhau, vì vậy phải có nhiều mã hóa có thể có:

    7fc00000
    7fc00001
    7fc00002
    

    nan0giống nhau std::numeric_limits<float>::quiet_NaN()nên chúng tôi suy ra rằng chúng đều là những NaN yên tĩnh khác nhau.

    Bản nháp tiêu chuẩn C11 N1570 xác nhận rằng nanf()tạo ra các NaN yên tĩnh, vì nanfchuyển tiếp đến strtodvà 7.22.1.3 "Các hàm strtod, strtof và strtold" cho biết:

    Chuỗi ký tự NAN hoặc NAN (n-char-serial opt) được hiểu là NaN yên tĩnh, nếu được hỗ trợ trong kiểu trả về, khác giống như phần chuỗi chủ đề không có dạng mong đợi; ý nghĩa của dãy n-char được xác định bằng cách triển khai. 293)

Xem thêm:

QNaNs và sNaNs trông như thế nào trong sách hướng dẫn?

IEEE 754 2008 khuyến nghị rằng (CẦN LÀM bắt buộc hay tùy chọn?):

  • bất kỳ thứ gì có số mũ == 0xFF và phân số! = 0 là một NaN
  • và bit phân số cao nhất phân biệt qNaN với sNaN

nhưng nó dường như không nói rằng bit nào được ưu tiên để phân biệt vô cực với NaN.

6.2.1 "Mã hóa NaN ở định dạng nhị phân" cho biết:

Điều khoản phụ này chỉ rõ thêm các mã hóa của NaN dưới dạng chuỗi bit khi chúng là kết quả của các phép toán. Khi được mã hóa, tất cả các NaN đều có một bit dấu và một mẫu bit cần thiết để xác định mã hóa là NaN và xác định loại của nó (sNaN so với qNaN). Các bit còn lại, nằm trong trường ý nghĩa và dấu sau, mã hóa tải trọng, có thể là thông tin chẩn đoán (xem ở trên). 34

Tất cả các chuỗi bit NaN nhị phân có tất cả các bit của trường số mũ thiên vị E được đặt thành 1 (xem 3.4). Một chuỗi bit NaN yên lặng nên được mã hóa với bit đầu tiên (d1) của trường dấu và T là 1. Một chuỗi bit NaN báo hiệu phải được mã hóa với bit đầu tiên của trường dấu và bằng 0. Nếu bit đầu tiên của trường có dấu và bằng 0, một số bit khác của trường dấu và phải khác 0 để phân biệt NaN với vô cùng. Trong cách mã hóa ưu tiên vừa mô tả, NaN báo hiệu sẽ được làm yên bằng cách đặt d1 thành 1, giữ nguyên các bit còn lại của T không đổi. Đối với các định dạng nhị phân, trọng tải được mã hóa trong p-2 bit có ý nghĩa nhỏ nhất của trường dấu và

Các tay Intel 64 và IA-32 Kiến trúc phát triển phần mềm của - Tập 1 Kiến trúc cơ bản - 253665-056US tháng 9 năm 2015 4.8.3.4 "Nans" xác nhận rằng x86 sau IEEE 754 bằng cách phân biệt NaN và SNaN bởi các bit phần cao nhất:

Kiến trúc IA-32 xác định hai lớp NaN: NaN yên tĩnh (QNaN) và NaN báo hiệu (SNaN). QNaN là NaN với bit phần quan trọng nhất được thiết lập. SNaN là NaN với bit phần quan trọng nhất rõ ràng.

Hướng dẫn Tham khảo Kiến trúc ARM - ARMv8, dành cho cấu hình kiến ​​trúc ARMv8-A - DDI 0487C.a A1.4.3 "Định dạng dấu chấm động chính xác đơn":

fraction != 0: Giá trị là NaN và là NaN yên tĩnh hoặc NaN báo hiệu. Hai loại NaN được phân biệt bằng bit phần quan trọng nhất của chúng, bit [22]:

  • bit[22] == 0: NaN là một NaN báo hiệu. Bit dấu có thể nhận bất kỳ giá trị nào và các bit phân số còn lại có thể nhận bất kỳ giá trị nào ngoại trừ tất cả các số không.
  • bit[22] == 1: NaN là một NaN trầm lặng. Bit dấu và các bit phân số còn lại có thể nhận bất kỳ giá trị nào.

QNanS và sNaNs được tạo ra như thế nào?

Một điểm khác biệt chính giữa qNaNs và sNaNs là:

  • qNaN được tạo ra bởi các phép toán số học (phần mềm hoặc phần cứng) tích hợp sẵn thông thường với các giá trị kỳ lạ
  • sNaN không bao giờ được tạo bởi các hoạt động tích hợp, nó chỉ có thể được thêm vào bởi các lập trình viên một cách rõ ràng, ví dụ: với std::numeric_limits::signaling_NaN

Tôi không thể tìm thấy dấu ngoặc kép IEEE 754 hoặc C11 rõ ràng cho điều đó, nhưng tôi cũng không thể tìm thấy bất kỳ hoạt động tích hợp nào tạo sNaNs ;-)

Tuy nhiên, hướng dẫn sử dụng Intel nêu rõ nguyên tắc này tại 4.8.3.4 "NaNs":

SNaN thường được sử dụng để bẫy hoặc gọi một trình xử lý ngoại lệ. Chúng phải được chèn bằng phần mềm; nghĩa là bộ xử lý không bao giờ tạo SNaN do hoạt động dấu phẩy động.

Điều này có thể được nhìn thấy từ ví dụ của chúng tôi, nơi cả hai:

float div_0_0 = 0.0f / 0.0f;
float sqrt_negative = std::sqrt(-1.0f);

tạo ra chính xác các bit giống như std::numeric_limits<float>::quiet_NaN().

Cả hai hoạt động đó đều biên dịch thành một lệnh hợp ngữ x86 duy nhất tạo qNaN trực tiếp trong phần cứng (TODO xác nhận với GDB).

QNaNs và sNaNs làm gì khác nhau?

Bây giờ chúng ta đã biết qNaN và sNaN trông như thế nào và cách sử dụng chúng, chúng tôi cuối cùng đã sẵn sàng để thử và làm cho sNaN thực hiện công việc của họ và làm nổ tung một số chương trình!

Vì vậy, không cần quảng cáo thêm:

blow_up.cpp

#include <cassert>
#include <cfenv>
#include <cmath> // isnan
#include <iostream>
#include <limits> // std::numeric_limits
#include <unistd.h>

#pragma STDC FENV_ACCESS ON

int main() {
    float snan = std::numeric_limits<float>::signaling_NaN();
    float qnan = std::numeric_limits<float>::quiet_NaN();
    float f;

    // No exceptions.
    assert(std::fetestexcept(FE_ALL_EXCEPT) == 0);

    // Still no exceptions because qNaN.
    f = qnan + 1.0f;
    assert(std::isnan(f));
    if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
        std::cout << "FE_ALL_EXCEPT qnan + 1.0f" << std::endl;

    // Now we can get an exception because sNaN, but signals are disabled.
    f = snan + 1.0f;
    assert(std::isnan(f));
    if (std::fetestexcept(FE_ALL_EXCEPT) == FE_INVALID)
        std::cout << "FE_ALL_EXCEPT snan + 1.0f" << std::endl;
    feclearexcept(FE_ALL_EXCEPT);

    // And now we enable signals and blow up with SIGFPE! >:-)
    feenableexcept(FE_INVALID);
    f = qnan + 1.0f;
    std::cout << "feenableexcept qnan + 1.0f" << std::endl;
    f = snan + 1.0f;
    std::cout << "feenableexcept snan + 1.0f" << std::endl;
}

Biên dịch, chạy và nhận trạng thái thoát:

g++ -ggdb3 -O0 -Wall -Wextra -pthread -std=c++11 -pedantic-errors -o blow_up.out blow_up.cpp -lm -lrt
./blow_up.out
echo $?

Đầu ra:

FE_ALL_EXCEPT snan + 1.0f
feenableexcept qnan + 1.0f
Floating point exception (core dumped)
136

Lưu ý rằng hành vi này chỉ xảy ra với -O0GCC 8.2: với -O3, GCC tính toán trước và tối ưu hóa tất cả các hoạt động sNaN của chúng tôi! Tôi không chắc liệu có cách tuân thủ tiêu chuẩn nào để ngăn chặn điều đó hay không.

Vì vậy, chúng tôi suy ra từ ví dụ này rằng:

  • snan + 1.0nguyên nhân FE_INVALID, nhưng qnan + 1.0không

  • Linux chỉ tạo ra một tín hiệu nếu nó được bật với feenableexept.

    Đây là một phần mở rộng glibc, tôi không thể tìm thấy bất kỳ cách nào để làm điều đó trong bất kỳ tiêu chuẩn nào.

Khi tín hiệu xảy ra, đó là do bản thân phần cứng CPU tạo ra một ngoại lệ, mà nhân Linux xử lý và thông báo cho ứng dụng thông qua tín hiệu.

Kết quả là bản in bash Floating point exception (core dumped)và trạng thái thoát là 136, tương ứng với tín hiệu 136 - 128 == 8, theo:

man 7 signal

SIGFPE.

Lưu ý rằng đó SIGFPElà tín hiệu tương tự mà chúng ta nhận được nếu chúng ta cố gắng chia một số nguyên cho 0:

int main() {
    int i = 1 / 0;
}

mặc dù đối với số nguyên:

  • chia bất kỳ thứ gì cho không sẽ làm tăng tín hiệu, vì không có biểu diễn vô cực trong số nguyên
  • tín hiệu mà nó xảy ra theo mặc định, không cần feenableexcept

Làm thế nào để xử lý SIGFPE?

Nếu bạn chỉ tạo một trình xử lý trả về bình thường, nó sẽ dẫn đến một vòng lặp vô hạn, vì sau khi trình xử lý trả về, phép chia lại xảy ra! Điều này có thể được xác minh với GDB.

Cách duy nhất là sử dụng setjmplongjmpchuyển sang một nơi khác như được hiển thị tại: C xử lý tín hiệu SIGFPE và tiếp tục thực hiện

Một số ứng dụng trong thế giới thực của sNaN là gì?

Thành thật mà nói, tôi vẫn chưa hiểu một trường hợp sử dụng siêu hữu ích cho sNaN, điều này đã được hỏi tại: Tính hữu ích của việc báo hiệu NaN?

sNaNs cảm thấy đặc biệt vô dụng vì chúng tôi có thể phát hiện các hoạt động không hợp lệ ban đầu ( 0.0f/0.0f) tạo ra qNaN với feenableexcept: nó dường như snanchỉ làm tăng lỗi cho nhiều hoạt động hơn mà qnankhông phát sinh, ví dụ: ( qnan + 1.0f).

Ví dụ:

C chính

#define _GNU_SOURCE
#include <fenv.h>
#include <stdio.h>

int main(int argc, char **argv) {
    (void)argv;
    float f0 = 0.0;

    if (argc == 1) {
        feenableexcept(FE_INVALID);
    }
    float f1 = 0.0 / f0;
    printf("f1 %f\n", f1);

    feenableexcept(FE_INVALID);
    float f2 = f1 + 1.0;
    printf("f2 %f\n", f2);
}

biên dịch:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -lm

sau đó:

./main.out

cho:

Floating point exception (core dumped)

và:

./main.out  1

cho:

f1 -nan
f2 -nan

Xem thêm: Cách theo dõi NaN trong C ++

Cờ tín hiệu là gì và chúng được thao tác như thế nào?

Mọi thứ đều được thực hiện trong phần cứng CPU.

Các cờ nằm ​​trong một số thanh ghi, và bit cho biết liệu một ngoại lệ / tín hiệu có nên được nâng lên hay không.

Những đăng ký đó có thể truy cập từ userland từ hầu hết các cổng.

Phần này của mã glibc 2.29 thực sự rất dễ hiểu!

Ví dụ: fetestexceptđược triển khai cho x86_86 tại sysdeps / x86_64 / fpu / ftestexcept.c :

#include <fenv.h>

int
fetestexcept (int excepts)
{
  int temp;
  unsigned int mxscr;

  /* Get current exceptions.  */
  __asm__ ("fnstsw %0\n"
       "stmxcsr %1" : "=m" (*&temp), "=m" (*&mxscr));

  return (temp | mxscr) & excepts & FE_ALL_EXCEPT;
}
libm_hidden_def (fetestexcept)

vì vậy chúng tôi ngay lập tức thấy rằng hướng dẫn sử dụng là stmxcsrviết tắt của "Lưu trữ trạng thái đăng ký MXCSR".

feenableexceptđược triển khai tại sysdeps / x86_64 / fpu / feenablxcpt.c :

#include <fenv.h>

int
feenableexcept (int excepts)
{
  unsigned short int new_exc, old_exc;
  unsigned int new;

  excepts &= FE_ALL_EXCEPT;

  /* Get the current control word of the x87 FPU.  */
  __asm__ ("fstcw %0" : "=m" (*&new_exc));

  old_exc = (~new_exc) & FE_ALL_EXCEPT;

  new_exc &= ~excepts;
  __asm__ ("fldcw %0" : : "m" (*&new_exc));

  /* And now the same for the SSE MXCSR register.  */
  __asm__ ("stmxcsr %0" : "=m" (*&new));

  /* The SSE exception masks are shifted by 7 bits.  */
  new &= ~(excepts << 7);
  __asm__ ("ldmxcsr %0" : : "m" (*&new));

  return old_exc;
}

Tiêu chuẩn C nói gì về qNaN và sNaN?

Các C11 N1570 tiêu chuẩn dự thảo rõ ràng nói rằng tiêu chuẩn không phân biệt giữa họ tại F.2.1 "infinities, số không có chữ ký, và Nans":

1 Đặc điểm kỹ thuật này không xác định hành vi báo hiệu các NaN. Nó thường sử dụng thuật ngữ NaN để biểu thị các NaN yên tĩnh. Các macro NAN và INFINITY và các hàm nan trong <math.h>cung cấp các ký hiệu cho IEC 60559 NaN và các vô hạn.

Đã thử nghiệm trong Ubuntu 18.10, GCC 8.2. GitHub ngược dòng:


vi.wikipedia.org/wiki/IEEE_754#Interchange_formats chỉ ra rằng IEEE-754 chỉ đơn thuần gợi ý rằng 0 để báo hiệu NaN là một lựa chọn triển khai tốt, để cho phép làm yên NaN mà không có nguy cơ biến nó thành vô cực (ý nghĩa và = 0). Rõ ràng điều đó không được tiêu chuẩn hóa, mặc dù nó là những gì x86 làm. (Và thực tế là MSB của ý nghĩa và xác định qNaN so với sNaN được chuẩn hóa). vi.wikipedia.org/wiki/Single-pre khít_floating-point_format cho biết x86 và ARM giống nhau, nhưng PA-RISC lại đưa ra lựa chọn ngược lại.
Peter Cordes

@PeterCordes vâng, tôi không chắc cái gì "nên" == "phải" hoặc "được ưu tiên" trong IEEE 754 20at "Một chuỗi bit NaN báo hiệu phải được mã hóa với bit đầu tiên của trường ý nghĩa và ở sau là 0".
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

re: nhưng nó dường như không chỉ định bit nào nên được sử dụng để phân biệt vô cực với NaN. Bạn đã viết rằng giống như bạn mong đợi sẽ có một số bit cụ thể mà tiêu chuẩn khuyến nghị đặt để phân biệt sNaN với vô cực. IDK tại sao bạn mong đợi có bất kỳ bit nào như vậy; bất kỳ lựa chọn nào khác không đều tốt. Chỉ cần chọn một cái gì đó mà sau này xác định được sNaN đến từ đâu. IDK, nghe có vẻ giống cụm từ kỳ lạ, và ấn tượng đầu tiên của tôi khi đọc nó là bạn nói rằng trang web đó không mô tả những gì phân biệt inf với NaN trong mã hóa (một ý nghĩa hoàn toàn không).
Peter Cordes

Trước năm 2008, IEEE 754 cho biết đó là bit báo hiệu / yên tĩnh (bit 22) nhưng không xác định giá trị nào. Hầu hết các bộ vi xử lý đều hội tụ trên 1 = yên tĩnh, vì vậy điều đó đã trở thành một phần của tiêu chuẩn trong phiên bản năm 2008. Nó nói "nên" thay vì "phải" để tránh thực hiện các triển khai cũ hơn làm cho cùng một lựa chọn không phù hợp. Nói chung, "nên" trong tiêu chuẩn có nghĩa là "phải, trừ khi bạn có lý do rất thuyết phục (và tốt hơn là được ghi chép đầy đủ) để không tuân thủ".
John Cowan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.