Floating Point Rounding Error và câu chuyện của một game thủ Dota

Mấy ngày vừa qua, cộng đồng chơi Dota2 báo một bug khá thú vị như trong hình.

APTPBxn.png

Có điều gì không ổn ở đây ? Nếu bạn là một người chơi Dota2, hẳn bạn sẽ biết, công thức tính chỉ số (attribute )của một hero là

Current Attr = Base Attr + Grownth * (level -1)

trong đó Base là chỉ số của hero tại level 1, Growth là lượng chỉ số gia tăng mỗi level. Thêm nữa là công thức tính lượng mana

Current Mana Pool  = Base Mana + Int * 12

trong đó, base mana là lượng mana cơ bản của mỗi hero, và Int là chỉ số Intelligent, mỗi điểm tăng 12 mana.

Theo như công thức , tại level 6, hero Juggernault trong hình, có base int = 14, grownth 1,4 int / level , base mana = 50 sẽ phải có lượng mana là

50 + (14 + 1.4 * (6-1)) * 12 = 302

Thực tế trong hình lại là 290. Chuyện gì đã xảy ra ??? Nếu bạn chỉ là một người chơi bình thường, đây có lẽ cũng chỉ đơn giản là một bug, game quái nào chả có cả tỉ bug.

Nhưng nếu bạn có một chút kiến thức cơ bản về lập trình, hẳn bạn sẽ phải mỉm cười khi nhận ra, ở đây, các lập trình viên của Valve đã gặp phải một lỗi khá phổ biến, thường bị bỏ sót. Và nếu như người của Valve, một trong những công ty bậc nhất, nổi tiếng về khâu tuyển chọn gắt gao vẫn có thể gặp phải sai lầm này, thì có lẽ, nó cũng đáng để ta làm một bài viết nhỏ đó chứ ?

Vâng, đến đây là kết thúc phần giới thiệu dông dài nhất từ trước đến nay. Xin chào mừng các bạn đến với phần tiếp theo ( quên số bao nhiêu rồi ) của series P.E.N.I.S ( Programmer's Extremely Neglected but Interesting Subjects ), chủ đề của phần này, và cũng là nguyên nhân của bug vừa mô tả phía trên , chính làaaaaaaaaaaaaaaaaaaaa ............. FLOATING POINT ROUNDING ERROR

Em chưa hiểu, số float được tạo ra như thế nào ?

Nào, hãy cùng nhau quay về thời năm thứ nhất, hay nếu theo như kết quả khảo sát năm 2015 của Stack Overflow , bất cứ khi nào mà 41.8% trong số chúng ta bắt đầu học lập trình, hẳn bạn còn nhớ, một trong những khái niệm đầu tiên mà chúng ta được học, đó là hệ nhị phân. Vì sao ? Vì cho dù bạn có lập trình gì đi chăng nữa, cho đến tận cùng, xử lí của máy tính vẫn chỉ là những phép tính nhị phân với những số 0 và 1. Điều đó có nghĩa là gì ? Mỗi con số của ta, khi máy tính xử lí sẽ đều được quy về hệ nhị phân tất. Thế nên, như trong ví dụ trên, con số 1.4 sẽ không phải được ghép bởi 2 chữ số 14 cùng dấu . ngăn cách như cách ta hiểu thông thường, mà nó sẽ là ( Viblo có vẻ không cho viết dấu mũ, nên xin tạm dùng Javascript để viết ví dụ )

Math.pow(2,1) + Math.pow(2,-2) + Math.pow(2,-3) + Math.pow(2,-7) + ....

túm lại là, con số 1.4 của hệ thập phân không thể biểu diễn một cách chính xác trong hệ nhị phân, cũng giống như kết quả của phép chia 1/3 không thể biểu diễn một cách chính xác trong hệ thập phân. Giá trị 1.4 mà ta hiểu, máy tính sẽ chỉ có thể hiểu là 1.3999999999, rất rất rất gần với 1.4 , nhưng không bao giờ bằng chính xác như vậy. Không tin à ? Bạn có thể làm một phép kiểm tra rất đơn giản. Bật F12 lên, vào màn hình console, gõ thử 1 phép tính rất đơn giản thôi

2.01 - 1

Kết quả là bao nhiều nào, 1.01, phải không ? Cái này đến trẻ con nó cũng làm được 14586868e5ee58dbd0c5f5ab67ee4572.png

Tất nhiên, nói không bao giờ có lẽ là hơi quá. Thật ra có rất nhiều cách để thể hiện chính xác con số 1.4 . Ví dụ, theo như cách ta hiểu, ta có thể dùng thư viện, để biểu diễn mọi số thập phân thành 3 phần, phần tự nhiên trước dấu ngăn cách, dấu ngăn cách , và thập phân sau dấu ngăn cách ( mọi số tự nhiên đều có thể biểu diễn dưới dạng nhị phân một cách ngon lành, không vấn đề gì ) . Tuy nhiên, làm theo cách nào thì cũng sẽ phải trả một cái giá khá đắt về mặt tốc độ xử lí.

Làm sao sống chung với lũ ?

Vâng, lối đi cho người nông dân ở đây không phải là dùng sức trâu chó làm sao ngăn cho sạch lũ mà phải tìm cách sống chung với nó. Về cơ bản thì lỗi này không khó sửa, nhưng vì lại dễ dính. Theo ngu ý của bản thân tác giả, thì để sống chung với nó, ta nên :

  • Đừng đọc xong rồi máy móc, chỗ nào động đến float cũng lăm lăm làm tròn, vì như đã nói, giải pháp là có nhưng sẽ phải trả giá bằng hiệu năng.

  • Tuy nhiên, cần luôn luôn ghi nhớ rằng, làm việc với số float là rất xương, rất "tricky", rất khốn nạn. Những đoạn nào liên quan đến kết quả của phép tính với số float, nên lưu ý để kiểm tra.

  • Kiểm tra thì thể nào cũng phát hiện ra chỗ bị ảnh hưởng. Những khi đó thì áp dụng giải pháp khắc phục. Một phương án đơn giản và thường dùng nhất là xác định xem mình cần độ chính xác đến đâu, rồi sau đó làm tròn tương ứng. Ngôn ngữ nào cũng có cách làm tròn cả, ở trên dùng JS làm ví dụ, thì ở đây cũng xin viết ví dụ một cách trong JS vậy ( ở đây là làm tròn đến 12 chữ số

function strip(number) {
    return (parseFloat(number).toPrecision(12));
}
  • Sẽ có những trường hợp hiếm hoi mà bạn cần phải xử lí các con số một cách hoàn toàn chính xác. Như đã nói, ngôn ngữ nào cũng có thư viện để xử lí bài toán này cả. Lại lôi JavaScript ra làm ví dụ, nhìn quanh một lúc ta có thể tìm ra khá nhiều, ví dụ như mathjs, bigjs ... Như đã nói, những trường hợp như thế này là khá hiếm, tùy bạn quyết định xem, liệu chương trình của mình có cần chính xác đến mức đó hay không.

Vâng, bài viết lần này đến đây là hết. Chúc các bạn chơi game vui vẻ hơn, tích cực hơn, để biết đâu vô tình làm tìm ra đề tài report như tác giả. Chào thân ái và quyết thắng.


All Rights Reserved