Việc so sánh sự bình đẳng của số float có đánh lừa các nhà phát triển cơ sở ngay cả khi không có lỗi làm tròn xảy ra trong trường hợp của tôi không?


31

Ví dụ: tôi muốn hiển thị danh sách các nút từ 0,0,5, ... 5, nhảy cho mỗi 0,5. Tôi sử dụng vòng lặp for để làm điều đó và có màu khác nhau tại nút STANDARD_LINE:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0;i<=MAX;i=i+DIFF){
    button.text=i+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Trong trường hợp này, không nên có lỗi làm tròn vì mỗi giá trị là chính xác trong IEEE 754. Nhưng tôi đang vật lộn nếu tôi nên thay đổi nó để tránh so sánh bằng dấu phẩy động:

var MAX=10;
var STANDARD_LINE=3;

for(var i=0;i<=MAX;i++){
    button.text=i/2.0+'';
    if(i==STANDARD_LINE/2.0){
      button.color='red';
    }
}

Một mặt, mã gốc đơn giản hơn và chuyển tiếp với tôi. Nhưng có một điều tôi đang xem xét: liệu tôi == STANDARD_LINE có đánh lừa đồng đội đàn em không? Liệu nó có che giấu thực tế là số dấu phẩy động có thể có lỗi làm tròn không? Sau khi đọc bình luận từ bài viết này:

https://stackoverflow.com/questions/33646148/is-hardcode-float-precise-if-it-can-be-interesents-by-binary-format-in-ieee-754

có vẻ như có nhiều nhà phát triển không biết một số số float là chính xác. Tôi có nên tránh so sánh bình đẳng số float ngay cả khi nó hợp lệ trong trường hợp của tôi không? Hay tôi đang suy nghĩ quá nhiều về điều này?


23
Hành vi của hai danh sách mã này là không tương đương. 3 / 2.0 là 1.5 nhưng isẽ chỉ là toàn bộ số trong danh sách thứ hai. Hãy thử loại bỏ cái thứ hai /2.0.
candied_orange

27
Nếu bạn thực sự cần phải so sánh hai FP cho sự bằng nhau (điều này không bắt buộc như những người khác đã chỉ ra trong câu trả lời tốt của họ vì bạn chỉ có thể sử dụng so sánh vòng lặp với số nguyên), nhưng nếu bạn đã làm như vậy, thì một nhận xét phải đủ. Cá nhân tôi đã làm việc với IEEE FP trong một thời gian dài và tôi vẫn bối rối nếu tôi thấy, giả sử, so sánh SPFP trực tiếp mà không có bất kỳ loại bình luận hay bất cứ điều gì. Đó chỉ là mã rất tinh vi - đáng để nhận xét ít nhất mỗi lần IMHO.

14
Bất kể bạn chọn loại nào, đây là một trong những trường hợp nhận xét giải thích cách thức và lý do hoàn toàn cần thiết. Một nhà phát triển sau này thậm chí có thể không xem xét sự tinh tế mà không có nhận xét để khiến họ chú ý. Ngoài ra, tôi bị phân tâm rất nhiều bởi thực tế là buttonkhông thay đổi bất cứ nơi nào trong vòng lặp của bạn. Làm thế nào là danh sách các nút truy cập? Thông qua chỉ số vào mảng hoặc một số cơ chế khác? Nếu đó là bằng cách truy cập chỉ mục vào một mảng, thì đây là một đối số khác có lợi cho việc chuyển sang số nguyên.
jpmc26

9
Viết mã đó. Cho đến khi ai đó nghĩ rằng 0,6 sẽ là kích thước bước tốt hơn và chỉ cần thay đổi hằng số đó.
tofro

11
"... Đánh lừa nhà phát triển cơ sở" Bạn cũng sẽ đánh lừa nhà phát triển cấp cao. Mặc dù số lượng suy nghĩ bạn đã đặt vào đây, họ sẽ cho rằng bạn không biết bạn đang làm gì và có thể sẽ thay đổi nó thành phiên bản số nguyên.
GrandOpener

Câu trả lời:


116

Tôi sẽ luôn tránh các phép toán dấu phẩy động liên tiếp trừ khi mô hình mà tôi tính toán yêu cầu chúng. Số học dấu phẩy động là không trực quan đối với hầu hết và là một nguồn chính của lỗi. Và nói với các trường hợp trong đó nó gây ra lỗi từ những người mà nó không phải là một sự khác biệt thậm chí còn tinh tế hơn!

Do đó, sử dụng float làm bộ đếm vòng lặp là một khiếm khuyết đang chờ xảy ra và sẽ yêu cầu ít nhất một nhận xét nền tảng giải thích lý do tại sao sử dụng 0,5 ở đây và điều này phụ thuộc vào giá trị số cụ thể. Tại thời điểm đó, viết lại mã để tránh bộ đếm float có thể sẽ là tùy chọn dễ đọc hơn. Và khả năng đọc là bên cạnh tính chính xác trong hệ thống phân cấp của các yêu cầu chuyên nghiệp.


48
Tôi thích "một khiếm khuyết đang chờ xảy ra". Chắc chắn, nó có thể hoạt động ngay bây giờ , nhưng một làn gió nhẹ từ ai đó đi ngang qua sẽ phá vỡ nó.
AakashM

10
Ví dụ: giả sử các yêu cầu thay đổi sao cho thay vì 11 nút cách đều nhau từ 0 đến 5 bằng "đường chuẩn" trên nút thứ 4, bạn có 16 nút cách đều nhau từ 0 đến 5 với "đường chuẩn" vào ngày 6 nút. Vì vậy, bất cứ ai thừa hưởng mã này từ bạn đều thay đổi 0,5 thành 1.0 / 3.0 và thay đổi 1.5 thành 5.0 / 3.0. Chuyện gì xảy ra sau đó?
David K

8
Vâng, tôi không thoải mái với ý tưởng rằng việc thay đổi số dường như là một số tùy ý (như "bình thường" như một số có thể) thành một số tùy ý khác ( có vẻ như "bình thường") thực sự đưa ra một khiếm khuyết.
Alexander - Tái lập Monica

7
@Alexander: đúng, bạn cần một bình luận cho biết DIFF must be an exactly-representable double that evenly divides STANDARD_LINE. Nếu bạn không muốn viết nhận xét đó (và dựa vào tất cả các nhà phát triển trong tương lai để biết đủ về điểm nổi nhị phân của IEEE754 để hiểu nó), thì đừng viết mã theo cách này. tức là không viết mã theo cách này. Đặc biệt bởi vì nó có thể thậm chí không hiệu quả hơn: bổ sung FP có độ trễ cao hơn bổ sung số nguyên và đó là một phụ thuộc mang theo vòng lặp. Ngoài ra, trình biên dịch (thậm chí trình biên dịch JIT?) Có thể làm tốt hơn trong việc tạo các vòng lặp với bộ đếm số nguyên.
Peter Cordes

39

Theo nguyên tắc chung, các vòng lặp nên được viết theo cách để nghĩ về việc làm một cái gì đó n lần. Nếu bạn đang sử dụng chỉ số dấu chấm động, nó không còn là một vấn đề làm điều gì đó n lần nhưng thay vì chạy cho đến khi một điều kiện được đáp ứng. Nếu điều kiện này xảy ra rất giống với i<nđiều mà rất nhiều lập trình viên mong đợi, thì mã dường như đang làm một việc khi nó thực sự đang làm một điều khác có thể dễ dàng bị hiểu sai bởi các lập trình viên lướt qua mã.

Điều này hơi chủ quan, nhưng theo ý kiến ​​khiêm tốn của tôi, nếu bạn có thể viết lại một vòng lặp để sử dụng một chỉ số nguyên để lặp một số lần cố định, bạn nên làm như vậy. Vì vậy, hãy xem xét các phương án sau:

var DIFF=0.5;                           // pixel increment
var MAX=Math.floor(5.0/DIFF);           // 5.0 is max pixel width
var STANDARD_LINE=Math.floor(1.5/DIFF); // 1.5 is pixel width

for(var i=0;i<=MAX;i++){
    button.text=(i*DIFF)+'';
    if(i==STANDARD_LINE){
      button.color='red';
    }
}

Các vòng lặp hoạt động trong điều khoản của toàn bộ số. Trong trường hợp inày là một số nguyên và STANDARD_LINEcũng bị ép buộc thành một số nguyên. Điều này tất nhiên sẽ thay đổi vị trí của dòng tiêu chuẩn của bạn nếu có tình cờ là vòng và tương tự như vậy MAX, vì vậy bạn vẫn nên cố gắng ngăn chặn vòng để hiển thị chính xác. Tuy nhiên, bạn vẫn có lợi thế là thay đổi các tham số về pixel và không phải là toàn bộ số mà không phải lo lắng về việc so sánh các điểm nổi.


3
Bạn cũng có thể muốn xem xét làm tròn thay vì sàn trong các bài tập, tùy thuộc vào những gì bạn muốn. Nếu phép chia được cho là có kết quả nguyên, sàn có thể gây bất ngờ nếu bạn vấp ngã khi các số xảy ra sự phân chia.
ilkkachu

1
@ilkkachu Đúng. Tôi nghĩ rằng nếu bạn đặt 5.0 là lượng pixel tối đa, thì thông qua làm tròn, tốt nhất là bạn nên ở phía dưới của 5.0 đó hơn là một chút. 5.0 có hiệu quả sẽ là tối đa. Mặc dù làm tròn có thể được ưa thích hơn theo những gì bạn cần làm. Trong cả hai trường hợp, nó sẽ tạo ra một chút khác biệt nếu cách chia này tạo ra một số nguyên.
Neil

4
Tôi rất không đồng ý. Cách tốt nhất để ngăn chặn một vòng lặp là với điều kiện thể hiện logic kinh doanh một cách tự nhiên nhất. Nếu logic nghiệp vụ là bạn cần 11 nút, vòng lặp sẽ dừng ở lần lặp 11. Nếu logic nghiệp vụ là các nút cách nhau 0,5 cho đến khi dòng đầy, vòng lặp sẽ dừng khi dòng đầy. Có những cân nhắc khác có thể đẩy sự lựa chọn về phía cơ chế này hay cơ chế kia nhưng không có những cân nhắc đó, hãy chọn cơ chế phù hợp nhất với yêu cầu kinh doanh.
Phục hồi

Giải thích của bạn sẽ hoàn toàn chính xác cho Java / C ++ / Ruby / Python / ... Nhưng Javascript không có số nguyên, iSTANDARD_LINEchỉ trông giống như số nguyên. Không có sự ép buộc nào cả, và DIFF, MAXSTANDARD_LINEtất cả đều chỉ Numberlà. NumberMặc dù được sử dụng làm số nguyên nên an toàn bên dưới 2**53, tuy nhiên chúng vẫn là số dấu phẩy động.
Eric Duminil

@EricDuminil Có, nhưng đây là một nửa của nó. Nửa còn lại là dễ đọc. Tôi đề cập đến nó như là lý do chính để làm theo cách này, không phải để tối ưu hóa.
Neil

20

Tôi đồng ý với tất cả các câu trả lời khác rằng sử dụng biến vòng lặp không nguyên thường là kiểu xấu ngay cả trong trường hợp như thế này khi nó sẽ hoạt động chính xác. Nhưng dường như có một lý do khác khiến nó trở nên tồi tệ ở đây.

Mã của bạn "biết" rằng độ rộng dòng có sẵn chính xác là bội số của 0,5 từ 0 đến 5.0. Có nên không? Có vẻ như đó là một quyết định giao diện người dùng có thể dễ dàng thay đổi (ví dụ: có thể bạn muốn khoảng cách giữa các chiều rộng có sẵn trở nên lớn hơn khi các chiều rộng thực hiện. 0,25, 0,5, 0,75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0 hoặc một cái gì đó).

Mã của bạn "biết" rằng tất cả các độ rộng dòng có sẵn đều có các biểu diễn "đẹp" cả dưới dạng số dấu phẩy động và dưới dạng số thập phân. Điều đó cũng có vẻ như một cái gì đó có thể thay đổi. (Bạn có thể muốn 0,1, 0,2, 0,3, ... tại một số điểm.)

Mã của bạn "biết" rằng văn bản để đặt trên các nút chỉ đơn giản là những gì Javascript biến các giá trị dấu phẩy động đó thành. Điều đó cũng có vẻ như một cái gì đó có thể thay đổi. (Ví dụ: có thể một ngày nào đó bạn sẽ muốn chiều rộng như 1/3, mà bạn có thể không muốn hiển thị là 0.33333333333333 hoặc bất cứ điều gì. Hoặc có lẽ bạn muốn xem "1.0" thay vì "1" để thống nhất với "1.5" .)

Tất cả những điều này đối với tôi giống như những biểu hiện của một điểm yếu duy nhất, đó là một loại pha trộn của các lớp. Những số dấu phẩy động này là một phần của logic bên trong của phần mềm. Văn bản hiển thị trên các nút là một phần của giao diện người dùng. Chúng nên tách biệt hơn so với mã trong đây. Các khái niệm như "cái nào trong số này là mặc định cần được làm nổi bật?" là các vấn đề giao diện người dùng và có lẽ chúng không nên được gắn với các giá trị dấu phẩy động đó. Và vòng lặp của bạn ở đây thực sự (hoặc ít nhất nên là) một vòng lặp trên các nút , không vượt quá độ rộng dòng . Được viết theo cách đó, sự cám dỗ sử dụng biến vòng lặp lấy các giá trị không nguyên sẽ biến mất: bạn chỉ cần sử dụng các số nguyên liên tiếp hoặc một ... trong / cho ... của vòng lặp.

Cảm giác của tôi là hầu hết các trường hợp người ta có thể muốn lặp lại các số không nguyên như thế này: có những lý do khác, hoàn toàn không liên quan đến các vấn đề về số, tại sao mã phải được tổ chức khác nhau. (Không phải tất cả các trường hợp; tôi có thể tưởng tượng một số thuật toán toán học có thể được thể hiện gọn gàng nhất theo vòng lặp trên các giá trị không nguyên.)


8

Một mùi mã đang sử dụng phao trong vòng lặp như thế.

Vòng lặp có thể được thực hiện theo nhiều cách, nhưng trong 99,9% trường hợp bạn nên giữ mức tăng 1 hoặc chắc chắn sẽ có sự nhầm lẫn, không chỉ bởi các nhà phát triển cơ sở.


Tôi không đồng ý, tôi nghĩ bội số nguyên của 1 không gây nhầm lẫn trong một vòng lặp for. Tôi sẽ không coi đó là mùi mã. Chỉ phân số.
CodeMonkey

3

Vâng, bạn muốn tránh điều này.

Số dấu phẩy động là một trong những cái bẫy lớn nhất đối với lập trình viên không nghi ngờ (có nghĩa là, theo kinh nghiệm của tôi, hầu hết mọi người). Từ việc phụ thuộc vào các bài kiểm tra đẳng thức điểm nổi, đến việc thể hiện tiền là điểm nổi, tất cả đều là một vũng lầy lớn. Thêm một cái nổi trên cái kia là một trong những tội lớn nhất. Có toàn bộ tập tài liệu khoa học về những thứ như thế này.

Sử dụng số dấu phẩy động chính xác ở những nơi thích hợp, ví dụ như khi thực hiện các phép tính toán học thực tế nơi bạn cần chúng (như lượng giác, vẽ đồ thị hàm, v.v.) và cực kỳ cẩn thận khi thực hiện các phép toán nối tiếp. Bình đẳng là đúng. Kiến thức về bộ số cụ thể nào chính xác theo tiêu chuẩn của IEEE là rất phức tạp và tôi sẽ không bao giờ phụ thuộc vào nó.

Trong trường hợp của bạn, có sẽ , bởi Định luật Murphy, đến điểm mà quản lý muốn bạn không có 0.0, 0.5, 1.0 ... nhưng 0.0, 0.4, 0.8 ... hoặc bất cứ điều gì; bạn sẽ ngay lập tức bị làm phiền, và lập trình viên cơ sở (hoặc chính bạn) sẽ gỡ lỗi lâu và khó cho đến khi bạn tìm ra vấn đề.

Trong mã cụ thể của bạn, tôi thực sự sẽ có một biến vòng lặp số nguyên. Nó đại diện cho inút th, không phải số đang chạy.

Và tôi có lẽ, vì lợi ích của sự rõ ràng hơn, không viết i/2nhưng i*0.5điều đó làm cho nó rõ ràng rất nhiều những gì đang xảy ra.

var BUTTONS=11;
var STANDARD_LINE=3;

for(var i=0; i<BUTTONS; i++) {
    button.text = (i*0.5)+'';
    if (i==STANDARD_LINE) {
      button.color='red';
    }
}

Lưu ý: như được chỉ ra trong các nhận xét, JavaScript thực sự không có một loại riêng cho số nguyên. Nhưng số nguyên có tối đa 15 chữ số được đảm bảo là chính xác / an toàn (xem https: //www.ecma-i Intl.org / ecma-626/6 / # ecs-số khó hiểu hơn / dễ bị lỗi khi làm việc với số nguyên hoặc không số nguyên ") điều này gần đúng với việc có một loại" tinh thần "riêng biệt; trong sử dụng hàng ngày (vòng lặp, tọa độ màn hình, chỉ số mảng, v.v.) sẽ không có gì ngạc nhiên với số nguyên được biểu diễn dưới Numberdạng JavaScript.


Tôi sẽ đổi tên BUTTONS thành một tên khác - có tất cả 11 nút chứ không phải 10. Có thể FIRST_BUTTON = 0, LAST_BUTTON = 10, STANDARD_LINE_BUTTON = 3. Ngoài ra, đó là cách bạn nên làm.
gnasher729

Đó là sự thật, @EricDuminil, và tôi đã thêm một chút về điều này vào câu trả lời. Cảm ơn bạn!
AnoE

1

Tôi không nghĩ rằng một trong những gợi ý của bạn là tốt. Thay vào đó, tôi sẽ giới thiệu một biến cho số lượng nút dựa trên giá trị tối đa và khoảng cách. Sau đó, nó đủ đơn giản để lặp qua các chỉ số của nút.

function precisionRound(number, precision) {
  let factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

var maxButtonValue = 5.0;
var buttonSpacing = 0.5;

let countEstimate = precisionRound(maxButtonValue / buttonSpacing, 5);
var buttonCount = Math.floor(countEstimate) + 1;

var highlightPosition = 3;
var highlightColor = 'red';

for (let i=0; i < buttonCount; i++) {
    let buttonValue = i / buttonSpacing;
    button.text = buttonValue.toString();
    if (i == highlightPosition) {
        button.color = highlightColor;
    }
}

Nó có thể là nhiều mã hơn, nhưng nó cũng dễ đọc hơn và mạnh mẽ hơn.


0

Bạn có thể tránh toàn bộ bằng cách tính toán giá trị bạn đang hiển thị thay vì sử dụng bộ đếm vòng lặp làm giá trị:

var MAX=5.0;
var DIFF=0.5
var STANDARD_LINE=1.5;

for(var i=0; (i*DIFF) < MAX ; i=i+1){
    var val = i * DIFF

    button.text=val+'';

    if(val==STANDARD_LINE){
      button.color='red';
    }
}

-1

Số học dấu phẩy động chậm và số học số nguyên nhanh, vì vậy khi tôi sử dụng dấu phẩy động, tôi sẽ không sử dụng nó một cách không cần thiết nơi các số nguyên có thể được sử dụng. Sẽ rất hữu ích khi luôn nghĩ về các số dấu phẩy động, thậm chí là hằng số, là gần đúng, với một số lỗi nhỏ. Nó rất hữu ích trong quá trình gỡ lỗi để thay thế các số dấu phẩy động gốc bằng các đối tượng dấu phẩy động cộng / trừ trong đó bạn coi mỗi số là một phạm vi thay vì một điểm. Bằng cách đó, bạn phát hiện ra sự thiếu chính xác ngày càng tăng sau mỗi hoạt động số học. Vì vậy, "1,5" nên được coi là "một số từ 1,45 đến 1,55" và "1,50" nên được coi là "một số giữa 1,495 và 1,505".


5
Sự khác biệt về hiệu năng giữa số nguyên và số float là rất quan trọng khi viết mã C cho bộ vi xử lý nhỏ, nhưng CPU có nguồn gốc x86 hiện đại làm nổi điểm nhanh đến mức mọi hình phạt đều dễ bị lu mờ khi sử dụng ngôn ngữ động. Cụ thể, không phải Javascript thực sự đại diện cho mọi số dưới dạng dấu phẩy động, sử dụng tải trọng NaN khi cần thiết?
rẽ trái

1
"Số học dấu phẩy động là chậm và số học số nguyên là nhanh" là một sự thật trong lịch sử mà bạn không nên giữ lại khi phúc âm tiến lên. Để thêm vào những gì @leftaroundabout đã nói, không chỉ đúng là hình phạt sẽ gần như không liên quan, bạn cũng có thể thấy các phép toán dấu phẩy động nhanh hơn các phép toán số nguyên tương đương của chúng, nhờ vào phép thuật của trình biên dịch tự động và bộ lệnh có thể giòn số lượng lớn phao trong một chu kỳ. Đối với câu hỏi này không liên quan, nhưng giả định "số nguyên nhanh hơn float" cơ bản đã không đúng trong một thời gian dài.
Jeroen Mostert

1
@JeroenMostert SSE / AVX có các hoạt động được véc tơ cho cả số nguyên và số float và bạn có thể sử dụng các số nguyên nhỏ hơn (vì không có bit nào bị lãng phí theo số mũ), vì vậy về nguyên tắc, người ta vẫn có thể giảm hiệu suất nhiều hơn từ mã số được tối ưu hóa cao hơn với phao. Nhưng một lần nữa, điều này không phù hợp với hầu hết các ứng dụng và chắc chắn không phải cho câu hỏi này.
leftaroundabout

1
@leftaroundabout: Chắc chắn rồi. Quan điểm của tôi không phải là cái nào chắc chắn nhanh hơn trong bất kỳ tình huống cụ thể nào, chỉ là "Tôi biết rằng FP chậm và số nguyên rất nhanh nên tôi sẽ sử dụng số nguyên nếu có thể" không phải là một động lực tốt ngay cả trước khi bạn giải quyết câu hỏi liệu điều bạn đang làm có cần tối ưu hóa không.
Jeroen Mostert
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.