Toán tử modulo (%) đưa ra một kết quả khác cho các phiên bản .NET khác nhau trong C #


89

Tôi đang mã hóa thông tin đầu vào của người dùng để tạo một chuỗi cho mật khẩu. Nhưng một dòng mã cho các kết quả khác nhau trong các phiên bản khác nhau của khuôn khổ. Mã một phần với giá trị của phím được người dùng nhấn:

Phím được nhấn: 1. Biến asciilà 49. Giá trị của 'e' và 'n' sau một số phép tính:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Kết quả của mã trên:

  • Trong .NET 3.5 (C #)

    Math.Pow(ascii, e) % n

    cho 9.0.

  • Trong .NET 4 (C #)

    Math.Pow(ascii, e) % n

    cho 77.0.

Math.Pow() cho kết quả đúng (giống nhau) trong cả hai phiên bản.

Nguyên nhân là gì, và có giải pháp không?


12
Tất nhiên, cả hai câu trả lời trong câu hỏi đều sai. Thực tế là bạn dường như không quan tâm đến điều đó, tốt, đáng lo ngại.
David Heffernan,

34
Bạn cần quay lại vài bước. "Tôi đang mã hóa thông tin đầu vào của người dùng để tạo chuỗi cho mật khẩu", phần này đã không rõ ràng. Bạn thực sự muốn làm gì? Bạn muốn lưu trữ mật khẩu ở dạng mã hóa hoặc băm? Bạn có muốn sử dụng nó làm entropy để tạo ra một giá trị ngẫu nhiên không? Mục tiêu bảo mật của bạn là gì?
CodesInChaos

49
Mặc dù câu hỏi này minh họa một vấn đề thú vị với số học dấu phẩy động, nếu mục tiêu của OP là "mã hóa đầu vào của người dùng để tạo chuỗi cho mật khẩu", tôi không nghĩ rằng việc sử dụng mã hóa của riêng bạn là một ý tưởng hay, vì vậy tôi không khuyên bạn nên thực sự triển khai bất kỳ câu trả lời nào.
Harrison Paine,

18
Minh chứng tuyệt vời tại sao các ngôn ngữ khác cấm sử dụng %với số dấu phẩy động.
Ben Voigt,

5
Mặc dù các câu trả lời đều tốt, nhưng không ai trong số họ trả lời câu hỏi về điều gì đã thay đổi giữa .NET 3.5 và 4 đang gây ra hành vi khác nhau.
msell

Câu trả lời:


160

Math.Powhoạt động trên các số dấu phẩy động có độ chính xác kép; do đó, bạn không nên mong đợi nhiều hơn 15–17 chữ số đầu tiên của kết quả là chính xác:

Tất cả các số dấu phẩy động cũng có một số giới hạn các chữ số có nghĩa, điều này cũng xác định mức độ chính xác của giá trị dấu phẩy động xấp xỉ một số thực. Một Doublegiá trị có độ chính xác lên đến 15 chữ số thập phân, mặc dù tối đa 17 chữ số được duy trì bên trong.

Tuy nhiên, số học modulo yêu cầu tất cả các chữ số phải chính xác. Trong trường hợp của bạn, bạn đang tính 49 103 , có kết quả bao gồm 175 chữ số, làm cho phép toán modulo trở nên vô nghĩa trong cả hai câu trả lời của bạn.

Để tính ra giá trị chính xác, bạn nên sử dụng số học có độ chính xác tùy ý, như được cung cấp bởi BigIntegerlớp (được giới thiệu trong .NET 4.0).

int val = (int)(BigInteger.Pow(49, 103) % 143);   // gives 114

Chỉnh sửa : Như đã chỉ ra bởi Mark Peters trong các nhận xét bên dưới, bạn nên sử dụng BigInteger.ModPowphương pháp được dành riêng cho loại hoạt động này:

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114

20
1 đã chỉ ra những vấn đề thực sự, cụ thể là các mã trong câu hỏi là sai đơn giản
David Heffernan

36
Cần lưu ý rằng BigInteger cung cấp một phương thức ModPow () thực hiện (trong thử nghiệm nhanh của tôi vừa rồi) nhanh hơn khoảng 5 lần cho thao tác này.
Mark Peters

8
+1 Với bản chỉnh sửa. ModPow không chỉ nhanh mà còn ổn định về mặt số học!
Ray

2
@maker Không, câu trả lời là vô nghĩa , không phải là không hợp lệ .
Cody Grey

3
@ makerofthings7: Tôi đồng ý với bạn về nguyên tắc. Tuy nhiên, tính không chính xác vốn có đối với số học dấu phẩy động và việc mong đợi các nhà phát triển nhận thức được rủi ro là thực tế hơn là áp đặt các hạn chế đối với hoạt động nói chung. Nếu một người muốn thực sự "an toàn", thì ngôn ngữ cũng cần phải cấm so sánh bình đẳng dấu phẩy động, để tránh các kết quả không mong muốn như 1.0 - 0.9 - 0.1 == 0.0đánh giá đối false.
Douglas

72

Ngoài thực tế là hàm băm của bạn không phải là một hàm quá tốt * , vấn đề lớn nhất với mã của bạn không phải là nó trả về một số khác tùy thuộc vào phiên bản .NET, mà là trong cả hai trường hợp, nó trả về một số hoàn toàn vô nghĩa: câu trả lời chính xác cho vấn đề là

49 103 mod 143 = là 114. ( liên kết tới Wolfram Alpha )

Bạn có thể sử dụng mã này để tính toán câu trả lời này:

private static int PowMod(int a, int b, int mod) {
    if (b == 0) {
        return 1;
    }
    var tmp = PowMod(a, b/2, mod);
    tmp *= tmp;
    if (b%2 != 0) {
        tmp *= a;
    }
    return tmp%mod;
}

Lý do tại sao phép tính của bạn tạo ra một kết quả khác là để tạo ra một câu trả lời, bạn sử dụng một giá trị trung gian làm giảm hầu hết các chữ số có nghĩa của số 49 103 : chỉ 16 chữ số đầu tiên trong số 175 chữ số của nó là đúng!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

159 chữ số còn lại đều sai. Tuy nhiên, hoạt động mod tìm kiếm một kết quả yêu cầu mọi chữ số đều phải đúng, kể cả những chữ số cuối cùng. Do đó, ngay cả những cải tiến nhỏ nhất về độ chính xác Math.Powcó thể đã được thực hiện trong .NET 4, sẽ dẫn đến sự khác biệt lớn trong phép tính của bạn, về cơ bản tạo ra một kết quả tùy ý.

* Vì câu hỏi này nói về việc nâng số nguyên lên lũy thừa cao trong bối cảnh băm mật khẩu, nên đọc liên kết câu trả lời này trước khi quyết định xem có nên thay đổi cách tiếp cận hiện tại của bạn để có khả năng tốt hơn hay không.


20
Câu trả lời tốt. Điểm thực sự là đây là một hàm băm khủng khiếp. OP cần suy nghĩ lại giải pháp và sử dụng một thuật toán thích hợp hơn.
david.pfx

1
Isaac Newton: Có thể nào mặt trăng bị hút vào trái đất giống như cách mà quả táo bị hút vào trái đất không? @ david.pfx: Điểm thực sự là đây là một cách hái táo khủng khiếp. Newton cần phải suy nghĩ lại về giải pháp và có lẽ thuê một người có thang.
jwg

2
@jwg Bình luận của David nhận được nhiều ủng hộ là có lý do. Câu hỏi ban đầu đã làm rõ rằng thuật toán đang được sử dụng để băm mật khẩu và nó thực sự là một thuật toán khủng khiếp cho mục đích đó - nó rất có khả năng bị phá vỡ giữa các phiên bản của .NET framework, như đã được chứng minh. Bất kỳ câu trả lời nào không đề cập đến việc OP cần phải thay thế thuật toán của mình hơn là "sửa chữa" nó sẽ khiến anh ta trở nên bất lợi.
Chris

@Chris Cảm ơn bạn đã nhận xét, tôi đã chỉnh sửa để đưa vào đề xuất của David. Tôi không nói mạnh như bạn, bởi vì hệ thống của OP có thể là một món đồ chơi hoặc một đoạn mã vứt đi mà anh ta xây dựng để giải trí cho riêng mình. Cảm ơn!
dasblinkenlight

27

Những gì bạn thấy là lỗi làm tròn thành đôi. Math.Powhoạt động với double và sự khác biệt như sau:

.NET 2.0 và 3.5 => var powerResult = Math.Pow(ascii, e);trả về:

1.2308248131348429E+174

.NET 4.0 và 4.5 => var powerResult = Math.Pow(ascii, e);trả về:

1.2308248131348427E+174

Chú ý đến chữ số cuối cùng trước đó Evà điều đó gây ra sự khác biệt trong kết quả. Nó không phải là toán tử mô-đun (%) .


3
thánh bò đây có phải là câu trả lời DUY NHẤT cho câu hỏi OP? Tôi đã đọc tất cả meta "blah blah câu hỏi bảo mật sai. Tôi biết nhiều hơn bạn n00b" và vẫn tự hỏi "tại sao sự khác biệt nhất quán giữa 3.5 và 4.0? Đã bao giờ bạn đặt ngón chân lên một tảng đá khi nhìn lên mặt trăng và hỏi" loại đá nào có phải đây không? "Chỉ được cho biết" Vấn đề thực sự của bạn không nằm ở việc nhìn vào đôi chân của bạn "hoặc" Bạn mong đợi điều gì khi mang dép tự làm vào buổi tối? !!! "CẢM ƠN!
Michael Paulukonis

1
@MichaelPaulukonis: Đó là một sự tương tự sai lầm. Nghiên cứu về đá là một theo đuổi hợp pháp; thực hiện số học có độ chính xác tùy ý bằng cách sử dụng các kiểu dữ liệu có độ chính xác cố định là sai hoàn toàn. Tôi muốn so sánh điều này với một nhà tuyển dụng phần mềm hỏi tại sao chó lại kém hơn mèo khi viết C #. Nếu bạn là một nhà động vật học, câu hỏi có thể có giá trị nhất định; đối với những người khác, nó vô nghĩa.
Douglas

24

Độ chính xác của dấu chấm động có thể khác nhau giữa các máy và thậm chí trên cùng một máy .

Tuy nhiên, .NET tạo một máy ảo cho các ứng dụng của bạn ... nhưng có những thay đổi giữa các phiên bản.

Vì vậy, bạn không nên dựa vào nó để tạo ra kết quả nhất quán. Để mã hóa, hãy sử dụng các lớp mà Framework cung cấp thay vì sử dụng các lớp của riêng bạn.


10

Có rất nhiều câu trả lời về cách code xấu. Tuy nhiên, tại sao kết quả lại khác…

FPU của Intel sử dụng định dạng 80-bit bên trong để có được độ chính xác hơn cho các kết quả trung gian. Vì vậy, nếu một giá trị nằm trong thanh ghi bộ xử lý, nó sẽ nhận được 80 bit, nhưng khi nó được ghi vào ngăn xếp, nó sẽ được lưu trữ ở 64 bit .

Tôi hy vọng rằng phiên bản mới hơn của .NET có trình tối ưu hóa tốt hơn trong quá trình biên dịch Just in Time (JIT), vì vậy nó đang giữ một giá trị trong một thanh ghi thay vì ghi nó vào ngăn xếp và sau đó đọc lại từ ngăn xếp.

Có thể JIT bây giờ có thể trả về một giá trị trong một thanh ghi thay vì trên ngăn xếp. Hoặc chuyển giá trị cho hàm MOD trong một thanh ghi.

Xem thêm Câu hỏi Tràn ngăn xếp Các ứng dụng / lợi ích của kiểu dữ liệu chính xác mở rộng 80-bit là gì?

Các bộ xử lý khác, ví dụ như ARM sẽ đưa ra các kết quả khác nhau cho mã này.


6

Có lẽ tốt nhất là bạn nên tự mình tính toán nó bằng cách chỉ sử dụng số học số nguyên. Cái gì đó như:

int n = 143;
int e = 103;
int result = 1;
int ascii = (int) 'a';

for (i = 0; i < e; ++i) 
    result = result * ascii % n;

Bạn có thể so sánh hiệu suất với hiệu suất của giải pháp BigInteger được đăng trong các câu trả lời khác.


7
Điều đó sẽ yêu cầu 103 phép nhân và giảm môđun. Người ta có thể làm tốt hơn bằng cách tính toán e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n, v.v. và sau đó kết quả = e * e2% n * e4% n * e32% n * e64% n. Tổng cộng có 11 phép nhân và giảm môđun. Với kích thước của số liên quan, người ta có thể loại bỏ một vài mô đun giảm hơn, nhưng đó sẽ là nhỏ so với giảm 103 hoạt động để 11.
supercat

2
@supercat Toán học tuyệt vời, nhưng trong thực tế chỉ phù hợp nếu bạn đang chạy điều này trên máy nướng bánh mì.
alextgordon

7
@alextgordon: Hoặc nếu người ta định sử dụng các giá trị số mũ lớn hơn. Mở rộng giá trị số mũ thành ví dụ: 65521 sẽ mất khoảng 28 phép nhân và giảm mô-đun nếu một người sử dụng giảm sức mạnh, so với 65,520 nếu không.
supercat

+1 để đưa ra giải pháp có thể truy cập trong đó rõ ràng chính xác cách tính toán được thực hiện.
jwg

2
@Supercat: bạn hoàn toàn đúng. Thật dễ dàng để cải thiện thuật toán, điều này có liên quan nếu nó được tính toán rất thường xuyên hoặc số mũ lớn. Nhưng thông điệp chính là nó có thể và nên được tính bằng số học số nguyên.
Ronald
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.