Lý do nào được sử dụng khi các nhà thiết kế ngôn ngữ lập trình quyết định những gì ký kết quả của hoạt động modulo mất?


9

Trải qua hoạt động Modulo (đại lộ tôi đã nhập trong khi khám phá sự khác biệt giữa remmod ) tôi đã đi qua:

Trong toán học, kết quả của phép toán modulo là phần còn lại của phép chia Euclide. Tuy nhiên, các quy ước khác là có thể. Máy tính và máy tính có nhiều cách lưu trữ và biểu diễn số khác nhau; do đó định nghĩa của họ về hoạt động modulo phụ thuộc vào ngôn ngữ lập trình và / hoặc phần cứng cơ bản.

Câu hỏi:

  • Đi qua bộ phận Euclide tôi thấy rằng phần còn lại của hoạt động này luôn dương (hoặc 0). Giới hạn nào của phần cứng máy tính cơ bản buộc các nhà thiết kế ngôn ngữ lập trình khác với toán học?
  • Mọi ngôn ngữ lập trình đều có quy tắc được xác định trước hoặc không xác định theo quy tắc mà kết quả của hoạt động modulo có được. Lý do nào được thông qua trong khi đưa ra các quy tắc này? Và nếu phần cứng cơ bản là mối quan tâm thì quy tắc không nên thay đổi theo đó, độc lập với ngôn ngữ lập trình?

1
Trong mã của tôi, tôi hầu như luôn cần modulo chứ không phải phần còn lại. Không có lý do tại sao phần còn lại là rất phổ biến.
CodeInChaos

8
Liên quan gì là sự khác biệt? Remainder vs Modulus - Blog của Eric Lippert (bởi một trong những nhà thiết kế C #, nhưng tôi tin rằng anh ấy đã tham gia nhóm sau khi quyết định này được đưa ra)
CodeInChaos

1
Nếu bạn tiếp tục đọc bài viết Wikipedia (ngoài phần bạn trích dẫn), nó sẽ giải thích những gì bạn trích dẫn khá tốt. Những gì về lời giải thích mà bạn bối rối về?
Robert Harvey

1
Một câu hỏi liên quan là hoạt động nào trong số các thao tác này ánh xạ trực tiếp tới các hướng dẫn CPU. Trong c, việc triển khai được xác định, phù hợp với triết lý c là ánh xạ trực tiếp vào phần cứng trên càng nhiều nền tảng càng tốt. Vì vậy, nó không chỉ định những thứ có thể khác nhau giữa các CPU.
CodeInChaos

5
@BleinatingFingers Lập trình thường sử dụng phép chia số nguyên đi về 0, ví dụ (-3)/2 == -1. Định nghĩa này có thể hữu ích. Khi bạn muốn %thống nhất với bộ phận này, x == (x/y)*y + x % ybạn sẽ kết thúc với định nghĩa %được sử dụng trong C #.
CodeInChaos

Câu trả lời:


6

Phần cứng của tất cả các máy tính hiện đại đủ mạnh để thực hiện các hoạt động mod của một trong hai dấu hiệu mà không có tác động hiệu năng (hoặc tầm thường). Đây không phải là lý do.

Kỳ vọng chung của hầu hết các ngôn ngữ máy tính là (a div b) * b + (a mod b) = a. Nói cách khác, div và mod được xem xét cùng nhau chia một số thành các phần có thể được đặt lại một lần nữa. Yêu cầu này là rõ ràng trong tiêu chuẩn C ++. Khái niệm này liên quan chặt chẽ đến việc lập chỉ mục các mảng đa chiều. Tôi đã sử dụng nó thường xuyên.

Từ đó có thể thấy rằng div và mod sẽ giữ nguyên dấu của a nếu b là dương (như thường lệ).

Một số ngôn ngữ cung cấp hàm 'rem ()' có liên quan đến mod và có một số biện minh toán học khác. Tôi chưa bao giờ cần phải sử dụng này. Xem ví dụ frem () trong Gnu C. [đã chỉnh sửa]


Tôi nghĩ rằng đó rem(a,b)là nhiều khả năng mod(a,b)nếu nó là tích cực hoặc mod(a,b) + bnếu nó không.
dùng40989

3
(a div b) * b + (a mod b) = a- cái này, rất nhiều Trên thực tế, trái với cách Wikipedia mô tả việc mở rộng nó thành các số âm trong phân chia Euclide (đặc biệt là "Phần còn lại là một trong bốn số không bao giờ có thể âm.") Làm tôi bối rối vì tôi luôn được dạy rằng phần còn lại thể âm trong mỗi lớp toán ở cấp độ đó.
Izkata

@ user40989: Tôi đã nói rằng tôi chưa bao giờ sử dụng nó. Xem chỉnh sửa!
david.pfx

4

Đối với lập trình thông thường bạn muốn X == (X/n)*n + X%n; do đó, modulo được định nghĩa như thế nào tùy thuộc vào cách xác định số nguyên.

Với suy nghĩ này, bạn thực sự đang hỏi " Lý do nào được sử dụng khi các nhà thiết kế ngôn ngữ lập trình quyết định cách phân chia số nguyên hoạt động? "

Thực tế có khoảng 7 sự lựa chọn:

  • tròn đến vô cực
  • tròn đến vô cực tích cực
  • làm tròn đến không
  • một số phiên bản của "làm tròn đến gần nhất" (với sự khác biệt về cách làm tròn 0,5)

Bây giờ hãy xem xét -( (-X) / n) == X/n. Tôi muốn điều này là đúng, vì mọi thứ khác có vẻ không nhất quán (nó đúng với dấu phẩy động) và phi logic (một nguyên nhân có thể gây ra lỗi và cũng có khả năng tối ưu hóa bị bỏ lỡ). Điều này làm cho 2 lựa chọn đầu tiên cho phép chia số nguyên (làm tròn thành vô cực) không mong muốn.

Tất cả các lựa chọn "vòng đến gần nhất" là một vấn đề khó khăn đối với lập trình, đặc biệt là khi bạn đang làm một cái gì đó như bitmap (ví dụ offset = index / 8; bitNumber = index%8;).

Điều đó làm tròn số về phía 0 là lựa chọn "có khả năng lành mạnh nhất", ngụ ý rằng modulo trả về một giá trị có cùng dấu với tử số (hoặc số không).

Lưu ý: Bạn cũng sẽ lưu ý rằng hầu hết các CPU (tất cả các CPU mà tôi biết) đều thực hiện phân chia số nguyên theo cùng một cách "làm tròn thành không". Điều này có thể là vì những lý do tương tự.


Nhưng phân chia cắt ngắn cũng có những mâu thuẫn riêng: Nó phá vỡ (a+b*c)/b == a % ba >> n == a / 2 ** n, trong đó phân chia nổi có hành vi lành mạnh.
dan04

Ví dụ đầu tiên của bạn không có ý nghĩa. Ví dụ thứ hai của bạn là một mớ hỗn độn cho các lập trình viên: đối với dương a và dương n thì nó nhất quán, đối với âm a và dương n, nó phụ thuộc vào cách xác định quyền dịch chuyển (số học so với logic) và đối với âm n bị phá vỡ (ví dụ 1 >> -2 == a / 2 ** (-2)).
Brendan

Ví dụ đầu tiên là một lỗi đánh máy: ý tôi là (a + b * c) % b == a % b, %toán tử được chia theo định kỳ trong cổ tức, điều này thường rất quan trọng. Ví dụ, với phân chia nổi, day_count % 7cung cấp cho bạn ngày trong tuần, nhưng với phân chia cắt ngắn, điều này sẽ phá vỡ các ngày trước thời đại.
dan04

0

Đầu tiên, tôi sẽ nhắc lại rằng một modulo b phải bằng a - b * (div b) và nếu một ngôn ngữ không cung cấp điều đó, thì bạn đang ở trong một mớ hỗn độn toán học khủng khiếp. Biểu thức đó a - b * (a div b) thực sự có bao nhiêu cách thực hiện tính toán một modulo b.

Có một số lý do có thể. Đầu tiên là bạn muốn tốc độ tối đa, do đó, div b được định nghĩa là bất cứ thứ gì bộ xử lý sử dụng sẽ cung cấp. Nếu bộ xử lý của bạn có lệnh "div" thì div b là bất cứ thứ gì mà lệnh div thực hiện (miễn là nó không hoàn toàn điên rồ).

Thứ hai là bạn muốn một số hành vi toán học cụ thể. Trước tiên hãy giả sử b> 0. Khá hợp lý khi bạn muốn kết quả của div b được làm tròn về 0. Vậy 4 div 5 = 0, 9 div 5 = 1, -4 div 5 = -0 = 0, -9 div 5 = -1. Điều này mang lại cho bạn (-a) div b = - (a div b) và (-a) modulo b = - (a modulo b).

Điều này khá hợp lý nhưng không hoàn hảo; ví dụ: (a + b) div b = (a div b) + 1 không giữ, giả sử nếu a = -1. Với giá trị b> 0 cố định, thường có (b) các giá trị có thể có của a sao cho div b cho cùng kết quả, ngoại trừ có 2b - 1 giá trị a từ -b + 1 đến b - 1 trong đó div b bằng 0 Nó cũng có nghĩa là một modulo b sẽ âm nếu a âm. Chúng tôi muốn một modulo b luôn luôn là một số trong phạm vi từ 0 đến b-1.

Mặt khác, cũng khá hợp lý khi yêu cầu khi bạn trải qua các giá trị liên tiếp của a, một modulo b phải đi qua các giá trị từ 0 đến b-1 sau đó bắt đầu lại với 0. Và để yêu cầu (a + b) div b phải là (a div b) + 1. Để đạt được điều đó, bạn muốn kết quả của một div b được làm tròn theo hướng vô cực, vì vậy -1 div b = -1. Một lần nữa, có những bất lợi. (-a) div b = - (a div b) không giữ. Chia nhiều lần cho hai hoặc bất kỳ số nào b> 1 cuối cùng sẽ không cho bạn kết quả bằng 0.

Vì có xung đột, các ngôn ngữ sẽ phải quyết định tập hợp lợi thế nào quan trọng hơn với chúng và quyết định theo đó.

Đối với b âm, hầu hết mọi người không thể hiểu được div b và modulo b ở vị trí đầu tiên, vì vậy cách đơn giản là xác định rằng div b = (-a) div (-b) và một modulo b = (-a) modulo (-b) nếu b <0, hoặc bất cứ điều gì là kết quả tự nhiên của việc sử dụng mã cho dương b.

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.