Tại sao việc thay đổi thứ tự tổng trả về một kết quả khác nhau?


294

Tại sao việc thay đổi thứ tự tổng trả về một kết quả khác nhau?

23.53 + 5.88 + 17.64 = = 47.05

23.53 + 17.64 + 5.88 = = 47.050000000000004

Cả JavaJavaScript trả về cùng một kết quả.

Tôi hiểu rằng, do cách các số dấu phẩy động được biểu diễn dưới dạng nhị phân, một số số hữu tỷ ( như 1/3 - 0.333333 ... ) không thể được biểu diễn chính xác.

Tại sao chỉ đơn giản là thay đổi thứ tự của các yếu tố ảnh hưởng đến kết quả?


28
Tổng các số thực là liên kết và giao hoán. Điểm nổi không phải là số thực. Trong thực tế, bạn chỉ chứng minh rằng hoạt động của họ không giao hoán. Thật dễ dàng để chứng minh rằng họ cũng không liên kết (ví dụ (2.0^53 + 1) - 1 == 2.0^53 - 1 != 2^53 == 2^53 + (1 - 1)). Do đó, có: hãy cảnh giác khi chọn thứ tự tổng và các hoạt động khác. Một số ngôn ngữ cung cấp tích hợp để thực hiện các khoản tiền "có độ chính xác cao" (ví dụ: python math.fsum), vì vậy bạn có thể cân nhắc sử dụng các hàm này thay vì thuật toán tổng ngây thơ.
Bakuriu

1
@RBerteig Điều đó có thể được xác định bằng cách kiểm tra thứ tự các thao tác của ngôn ngữ cho các biểu thức số học và, trừ khi biểu diễn số dấu phẩy động của chúng trong bộ nhớ là khác nhau, kết quả sẽ giống nhau nếu quy tắc ưu tiên toán tử của chúng giống nhau. Một điểm lưu ý khác: Tôi tự hỏi phải mất bao lâu để các nhà phát triển ứng dụng ngân hàng phát hiện ra điều này? Những 0000000000004 xu thêm đó thực sự cộng lại!
Chris Cirefice

3
@ChrisCirefice: nếu bạn có 0,00000004 xu , bạn đã làm sai. Bạn không bao giờ nên sử dụng loại dấu phẩy động nhị phân để tính toán tài chính.
Daniel Pryden

2
@DanielPryden À, thật là một trò đùa ... chỉ loanh quanh ý tưởng rằng những người thực sự cần giải quyết loại vấn đề này có một trong những công việc quan trọng nhất mà bạn biết, nắm giữ tình trạng tiền tệ của mọi người và tất cả những điều đó . Tôi đã rất mỉa mai ...
Chris Cirefice

Câu trả lời:


276

Có thể câu hỏi này là ngu ngốc, nhưng tại sao chỉ đơn giản là thay đổi thứ tự của các yếu tố ảnh hưởng đến kết quả?

Nó sẽ thay đổi các điểm tại đó các giá trị được làm tròn, dựa trên cường độ của chúng. Ví dụ về loại điều chúng ta đang thấy, hãy giả vờ rằng thay vì dấu phẩy động nhị phân, chúng ta đã sử dụng loại dấu phẩy động thập phân có 4 chữ số có nghĩa, trong đó mỗi phép cộng được thực hiện với độ chính xác "vô hạn" và sau đó làm tròn thành số đại diện gần nhất. Đây là hai khoản tiền:

1/3 + 2/3 + 2/3 = (0.3333 + 0.6667) + 0.6667
                = 1.000 + 0.6667 (no rounding needed!)
                = 1.667 (where 1.6667 is rounded to 1.667)

2/3 + 2/3 + 1/3 = (0.6667 + 0.6667) + 0.3333
                = 1.333 + 0.3333 (where 1.3334 is rounded to 1.333)
                = 1.666 (where 1.6663 is rounded to 1.666)

Chúng tôi thậm chí không cần số nguyên cho vấn đề này:

10000 + 1 - 10000 = (10000 + 1) - 10000
                  = 10000 - 10000 (where 10001 is rounded to 10000)
                  = 0

10000 - 10000 + 1 = (10000 - 10000) + 1
                  = 0 + 1
                  = 1

Điều này chứng tỏ có thể rõ ràng hơn rằng phần quan trọng là chúng ta có một số lượng hạn chế các chữ số có nghĩa - không phải là một số thập phân giới hạn . Nếu chúng ta luôn có thể giữ cùng một số vị trí thập phân, thì với phép cộng và trừ ít nhất, chúng ta sẽ ổn (miễn là các giá trị không bị tràn). Vấn đề là khi bạn nhận được số lớn hơn, thông tin nhỏ hơn sẽ bị mất - 10001 được làm tròn thành 10000 trong trường hợp này. (Đây là một ví dụ về vấn đề mà Eric Lippert lưu ý trong câu trả lời của anh ấy .)

Điều quan trọng cần lưu ý là các giá trị ở dòng đầu tiên của phía bên phải giống nhau trong mọi trường hợp - vì vậy mặc dù điều quan trọng là phải hiểu rằng các số thập phân của bạn (23,53, 5,88, 17,64) sẽ không được biểu thị chính xác như doublecác giá trị, đó là chỉ có một vấn đề vì những vấn đề được hiển thị ở trên.


10
May extend this later - out of time right now!háo hức chờ đợi nó @Jon
Prateek

3
khi tôi nói rằng tôi sẽ quay lại câu trả lời sau đó, cộng đồng sẽ hơi tử tế với tôi
Người chơi Grady

2
@ZongZhengLi: Mặc dù điều đó chắc chắn quan trọng để hiểu điều đó, nhưng đó không phải là nguyên nhân sâu xa trong trường hợp này. Bạn có thể viết một ví dụ tương tự với các giá trị được biểu diễn chính xác dưới dạng nhị phân và thấy hiệu ứng tương tự. Vấn đề ở đây là duy trì thông tin quy mô lớn và thông tin quy mô nhỏ cùng một lúc.
Jon Skeet

1
@Buksy: Làm tròn đến 10000 - vì chúng tôi đang xử lý một kiểu dữ liệu chỉ có thể lưu trữ 4 chữ số có nghĩa. (vì vậy x.xxx * 10 ^ n)
Jon Skeet

3
@meteors: Không, nó không gây ra lỗi tràn - và bạn đang sử dụng sai số. Đó là 10001 được làm tròn thành 10000, không phải 1001 được làm tròn thành 1000. Để làm cho rõ ràng hơn, 54321 sẽ được làm tròn thành 54320 - bởi vì chỉ có bốn chữ số có nghĩa. Có một sự khác biệt lớn giữa "bốn chữ số có nghĩa" và "giá trị tối đa là 9999". Như tôi đã nói trước đây, về cơ bản, bạn đại diện cho x.xxx * 10 ^ n, trong đó với 10000, x.xxx sẽ là 1.000 và n sẽ là 4. Điều này giống như doublefloat, trong đó đối với các số rất lớn, các số có thể biểu diễn liên tiếp cách nhau hơn 1.
Jon Skeet

52

Đây là những gì đang diễn ra trong nhị phân. Như chúng ta biết, một số giá trị dấu phẩy động không thể được biểu diễn chính xác dưới dạng nhị phân, ngay cả khi chúng có thể được biểu diễn chính xác bằng số thập phân. 3 con số này chỉ là ví dụ về thực tế đó.

Với chương trình này, tôi đưa ra các biểu diễn thập lục phân của mỗi số và kết quả của mỗi phép cộng.

public class Main{
   public static void main(String args[]) {
      double x = 23.53;   // Inexact representation
      double y = 5.88;    // Inexact representation
      double z = 17.64;   // Inexact representation
      double s = 47.05;   // What math tells us the sum should be; still inexact

      printValueAndInHex(x);
      printValueAndInHex(y);
      printValueAndInHex(z);
      printValueAndInHex(s);

      System.out.println("--------");

      double t1 = x + y;
      printValueAndInHex(t1);
      t1 = t1 + z;
      printValueAndInHex(t1);

      System.out.println("--------");

      double t2 = x + z;
      printValueAndInHex(t2);
      t2 = t2 + y;
      printValueAndInHex(t2);
   }

   private static void printValueAndInHex(double d)
   {
      System.out.println(Long.toHexString(Double.doubleToLongBits(d)) + ": " + d);
   }
}

Các printValueAndInHexphương pháp chỉ là một helper hex-in.

Đầu ra như sau:

403787ae147ae148: 23.53
4017851eb851eb85: 5.88
4031a3d70a3d70a4: 17.64
4047866666666666: 47.05
--------
403d68f5c28f5c29: 29.41
4047866666666666: 47.05
--------
404495c28f5c28f6: 41.17
4047866666666667: 47.050000000000004

4 số đầu tiên là x, y, z, và s's đại diện thập lục phân. Trong biểu diễn dấu phẩy động của IEEE, các bit 2-12 biểu thị số mũ nhị phân , nghĩa là tỷ lệ của số. (Các bit đầu tiên là bit dấu, và các bit còn lại cho mantissa .) Số mũ đại diện thực sự là số nhị phân trừ đi 1023.

Số mũ của 4 số đầu tiên được trích xuất:

    sign|exponent
403 => 0|100 0000 0011| => 1027 - 1023 = 4
401 => 0|100 0000 0001| => 1025 - 1023 = 2
403 => 0|100 0000 0011| => 1027 - 1023 = 4
404 => 0|100 0000 0100| => 1028 - 1023 = 5

Bộ bổ sung đầu tiên

Số thứ hai ( y) có độ lớn nhỏ hơn. Khi thêm hai số này để có được x + y, 2 bit cuối của số thứ hai ( 01) được dịch chuyển ra khỏi phạm vi và không tính vào phép tính.

Bổ sung thứ hai thêm x + yzthêm hai số có cùng tỷ lệ.

Bộ bổ sung thứ hai

Ở đây, x + zxảy ra đầu tiên. Chúng có cùng tỷ lệ, nhưng chúng mang lại một con số cao hơn về tỷ lệ:

404 => 0|100 0000 0100| => 1028 - 1023 = 5

Bổ sung thứ hai thêm x + zy, và bây giờ 3 bit được loại bỏ yđể thêm các số ( 101). Ở đây, phải có một vòng lên trên, vì kết quả là số dấu phẩy động tiếp theo tăng lên: 4047866666666666cho tập hợp bổ sung đầu tiên so với4047866666666667 tập hợp bổ sung thứ hai. Lỗi đó là đủ đáng kể để hiển thị trong bản in của tổng số.

Tóm lại, hãy cẩn thận khi thực hiện các phép toán trên các số của IEEE. Một số đại diện là không chính xác, và chúng thậm chí còn trở nên không chính xác hơn khi quy mô khác nhau. Thêm và trừ các số có tỷ lệ tương tự nếu bạn có thể.


Các quy mô là khác nhau là phần quan trọng. Bạn có thể viết (bằng số thập phân) các giá trị chính xác đang được biểu diễn dưới dạng nhị phân làm đầu vào và vẫn có cùng một vấn đề.
Jon Skeet

@rgettman Là một lập trình viên, tôi thích câu trả lời của bạn tốt hơn =)+1 cho người trợ giúp máy in hex của bạn ... điều đó thực sự gọn gàng!
ADTC

44

Câu trả lời của Jon tất nhiên là đúng. Trong trường hợp của bạn, lỗi không lớn hơn lỗi bạn sẽ tích lũy khi thực hiện bất kỳ thao tác dấu phẩy động đơn giản nào. Bạn đã có một kịch bản trong đó trong một trường hợp bạn không có lỗi và trong trường hợp khác bạn gặp một lỗi nhỏ; Đó thực sự không phải là một kịch bản thú vị. Một câu hỏi hay là: có kịch bản nào trong đó việc thay đổi thứ tự tính toán chuyển từ một lỗi nhỏ thành một lỗi rất lớn (tương đối) không? Câu trả lời rõ ràng là có.

Xem xét ví dụ:

x1 = (a - b) + (c - d) + (e - f) + (g - h);

đấu với

x2 = (a + c + e + g) - (b + d + f + h);

đấu với

x3 = a - b + c - d + e - f + g - h;

Rõ ràng trong số học chính xác, chúng sẽ giống nhau. Thật thú vị khi cố gắng tìm các giá trị cho a, b, c, d, e, f, g, h sao cho các giá trị của x1 và x2 và x3 khác nhau bởi một số lượng lớn. Xem nếu bạn có thể làm như vậy!


Làm thế nào để bạn xác định một số lượng lớn? Có phải chúng ta đang nói về thứ tự 1000? 100 giây? 1 của ???
Cruncher

3
@Cruncher: Tính kết quả toán học chính xác và các giá trị x1 và x2. Gọi sự khác biệt toán học chính xác giữa kết quả đúng và tính toán e1 và e2. Bây giờ có một số cách để suy nghĩ về kích thước lỗi. Đầu tiên là: bạn có thể tìm thấy một kịch bản trong đó | e1 / e2 | hoặc | e2 / e1 | lớn? Giống như, bạn có thể làm cho lỗi này gấp mười lần lỗi kia không? Tuy nhiên, điều thú vị hơn là nếu bạn có thể làm cho lỗi của một phần nhỏ đáng kể về kích thước của câu trả lời đúng.
Eric Lippert

1
Tôi nhận ra anh ấy đang nói về thời gian chạy, nhưng tôi tự hỏi: Nếu biểu thức là biểu thức thời gian biên dịch (giả sử là constexpr), thì trình biên dịch có đủ thông minh để giảm thiểu lỗi không?
Kevin Hsu

@kevinhsu nói chung là không, trình biên dịch không thông minh. Tất nhiên trình biên dịch có thể chọn thực hiện thao tác theo số học chính xác nếu nó được chọn, nhưng nó thường không.
Eric Lippert

8
@f Frozenkoi: Có, lỗi có thể vô hạn rất dễ dàng. Ví dụ: hãy xem xét C #: double d = double.MaxValue; Console.WriteLine(d + d - d - d); Console.WriteLine(d - d + d - d);- đầu ra là Infinity sau đó 0.
Jon Skeet

10

Điều này thực sự bao gồm nhiều thứ hơn là chỉ Java và Javascript và có thể sẽ ảnh hưởng đến bất kỳ ngôn ngữ lập trình nào bằng cách sử dụng số float hoặc double.

Trong bộ nhớ, các điểm nổi sử dụng một định dạng đặc biệt dọc theo các dòng của IEEE 754 (bộ chuyển đổi cung cấp giải thích tốt hơn nhiều so với tôi có thể).

Dù sao, đây là bộ chuyển đổi float.

http://www.h-schmidt.net/FloatConverter/

Điều về thứ tự của các hoạt động là "độ mịn" của hoạt động.

Dòng đầu tiên của bạn mang lại 29,41 từ hai giá trị đầu tiên, cung cấp cho chúng tôi 2 ^ 4 dưới dạng số mũ.

Dòng thứ hai của bạn mang lại 41,17, cung cấp cho chúng tôi 2 ^ 5 là số mũ.

Chúng ta đang mất một con số đáng kể bằng cách tăng số mũ, có khả năng thay đổi kết quả.

Hãy thử đánh dấu vào bit cuối cùng ở phía bên phải và tắt cho 41,17 và bạn có thể thấy rằng một cái gì đó "không đáng kể" là 1/2 ^ 23 của số mũ sẽ đủ để gây ra sự khác biệt điểm nổi này.

Chỉnh sửa: Đối với những người nhớ các số liệu quan trọng, điều này sẽ thuộc danh mục đó. 10 ^ 4 + 4999 với con số đáng kể là 1 sẽ là 10 ^ 4. Trong trường hợp này, con số đáng kể nhỏ hơn nhiều, nhưng chúng ta có thể thấy kết quả với .00000000004 được đính kèm.


9

Các số dấu phẩy động được biểu diễn bằng định dạng IEEE 754, cung cấp một kích thước bit cụ thể cho lớp phủ (ý nghĩa). Thật không may, điều này cung cấp cho bạn một số 'khối xây dựng phân đoạn' cụ thể để chơi và các giá trị phân số nhất định không thể được trình bày chính xác.

Điều đang xảy ra trong trường hợp của bạn là trong trường hợp thứ hai, phần bổ sung có thể gặp phải một số vấn đề chính xác vì thứ tự các phần bổ sung được ước tính. Tôi đã không tính toán các giá trị, nhưng có thể ví dụ rằng 23,53 + 17,64 không thể được biểu diễn chính xác, trong khi 23,53 + 5,88 thì có thể.

Thật không may, nó là một vấn đề được biết đến mà bạn phải giải quyết.


6

Tôi tin rằng nó phải làm theo thứ tự của sự trốn tránh. Mặc dù tổng là tự nhiên giống nhau trong một thế giới toán học, trong thế giới nhị phân thay vì A + B + C = D, nó là

A + B = E
E + C = D(1)

Vì vậy, có bước thứ cấp mà số dấu phẩy động có thể tắt.

Khi bạn thay đổi thứ tự,

A + C = F
F + B = D(2)

4
Tôi nghĩ rằng câu trả lời này tránh được lý do thực sự. "Có bước thứ cấp mà số dấu phẩy động có thể tắt". Rõ ràng, điều này là đúng, nhưng những gì chúng tôi muốn giải thích là tại sao .
Zong
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.