Operator modulo (%) memberikan hasil yang berbeda untuk versi .NET berbeda di C #

89

Saya mengenkripsi masukan pengguna untuk menghasilkan string untuk kata sandi. Tetapi sebaris kode memberikan hasil yang berbeda dalam versi kerangka kerja yang berbeda. Kode parsial dengan nilai kunci yang ditekan oleh pengguna:

Ditekan tombol: 1. Variabel asciiadalah 49. Nilai 'e' dan 'n' setelah dilakukan perhitungan:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Hasil kode di atas:

  • Di .NET 3.5 (C #)

    Math.Pow(ascii, e) % n
    

    memberi 9.0.

  • Di .NET 4 (C #)

    Math.Pow(ascii, e) % n
    

    memberi 77.0.

Math.Pow() memberikan hasil yang benar (sama) di kedua versi.

Apa penyebabnya, dan adakah solusinya?

Rajiv
sumber
12
Tentu saja, kedua jawaban tersebut salah. Fakta bahwa Anda tampaknya tidak peduli tentang itu, yah, mengkhawatirkan.
David Heffernan
34
Anda harus mundur beberapa langkah. "Saya mengenkripsi masukan pengguna untuk menghasilkan string untuk kata sandi" bagian ini sudah meragukan. Apa yang sebenarnya ingin Anda lakukan? Apakah Anda ingin menyimpan kata sandi dalam bentuk terenkripsi atau berciri? Apakah Anda ingin menggunakan ini sebagai entropi untuk menghasilkan nilai acak? Apa tujuan keamanan Anda?
CodesInChaos
49
Sementara pertanyaan ini menggambarkan masalah menarik dengan aritmatika floating point, jika tujuan OP adalah "mengenkripsi input pengguna untuk menghasilkan string untuk kata sandi", menurut saya menggulung enkripsi Anda sendiri adalah ide yang bagus, jadi saya tidak akan merekomendasikan benar-benar menerapkan salah satu jawaban.
Harrison Paine
18
Demonstrasi yang bagus mengapa bahasa lain melarang penggunaan %dengan bilangan floating-point.
Ben Voigt
5
Meskipun jawabannya bagus, tidak satupun dari mereka menjawab pertanyaan tentang apa yang telah berubah antara .NET 3.5 dan 4 yang menyebabkan perilaku berbeda.
msell

Jawaban:

160

Math.Powbekerja pada bilangan floating-point presisi ganda; dengan demikian, Anda tidak boleh mengharapkan lebih dari 15–17 digit pertama dari hasil agar akurat:

Semua bilangan floating-point juga memiliki jumlah digit signifikan yang terbatas, yang juga menentukan seberapa akurat nilai floating-point mendekati bilangan real. Sebuah Doublenilai memiliki presisi hingga 15 digit desimal, meskipun secara internal maksimal 17 digit dipertahankan.

Namun, aritmatika modulo membutuhkan semua digit untuk akurat. Dalam kasus Anda, Anda menghitung 49103 , yang hasilnya terdiri dari 175 digit, membuat operasi modulo menjadi tidak berarti di kedua jawaban Anda.

Untuk menghitung nilai yang benar, Anda harus menggunakan aritmatika presisi-arbitrer, seperti yang disediakan oleh BigIntegerkelas (diperkenalkan di .NET 4.0).

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

Sunting : Seperti yang ditunjukkan oleh Mark Peters dalam komentar di bawah, Anda harus menggunakan BigInteger.ModPowmetode, yang ditujukan khusus untuk jenis operasi ini:

int val = (int)BigInteger.ModPow(49, 103, 143);   // gives 114
Douglas
sumber
20
+1 untuk menunjukkan masalah sebenarnya, yaitu bahwa kode dalam pertanyaan itu jelas salah
David Heffernan
36
Perlu dicatat bahwa BigInteger menyediakan metode ModPow () yang melakukan (dalam pengujian cepat saya sekarang) sekitar 5 kali lebih cepat untuk operasi ini.
Mark Peters
8
+1 Dengan edit. ModPow tidak hanya cepat, tetapi juga stabil secara numerik!
Ray
2
@ pembuat Tidak, jawabannya tidak berarti , tidak valid .
Cody Grey
3
@ makerofthings7: Saya setuju dengan Anda pada prinsipnya. Namun, ketidaktepatan melekat pada aritmatika floating-point, dan dianggap lebih praktis untuk mengharapkan pengembang menyadari risikonya, daripada memaksakan pembatasan pada operasi secara umum. Jika seseorang ingin benar-benar "aman", maka bahasa tersebut juga harus melarang perbandingan kesetaraan floating-point, untuk menghindari hasil yang tidak diharapkan seperti 1.0 - 0.9 - 0.1 == 0.0mengevaluasi ke false.
Douglas
72

Terlepas dari fakta bahwa fungsi hashing Anda tidak terlalu bagus * , masalah terbesar dengan kode Anda bukanlah bahwa ia mengembalikan angka yang berbeda tergantung pada versi .NET, tetapi dalam kedua kasus itu mengembalikan angka yang sama sekali tidak berarti: jawaban yang benar untuk masalah ini adalah

49 103 mod 143 = adalah 114. ( link Wolfram Alpha )

Anda dapat menggunakan kode ini untuk menghitung jawaban ini:

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;
}

Alasan mengapa perhitungan Anda menghasilkan hasil yang berbeda adalah karena untuk menghasilkan jawaban, Anda menggunakan nilai antara yang menurunkan sebagian besar digit signifikan dari bilangan 49103 : hanya 16 pertama dari 175 digitnya yang benar!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

159 digit sisanya semuanya salah. Operasi mod, bagaimanapun, mencari hasil yang membutuhkan setiap digit untuk benar, termasuk yang terakhir. Oleh karena itu, bahkan peningkatan terkecil pada ketepatan Math.Powyang mungkin telah diterapkan di .NET 4, akan menghasilkan perbedaan drastis pada penghitungan Anda, yang pada dasarnya menghasilkan hasil yang berubah-ubah.

* Karena pertanyaan ini berbicara tentang menaikkan bilangan bulat menjadi kekuatan tinggi dalam konteks hashing kata sandi, mungkin ide yang sangat baik untuk membaca tautan jawaban ini sebelum memutuskan apakah pendekatan Anda saat ini harus diubah untuk pendekatan yang berpotensi lebih baik.

Sergey Kalinichenko
sumber
20
Jawaban yang bagus. Poin sebenarnya adalah bahwa ini adalah fungsi hash yang buruk. OP perlu memikirkan kembali solusinya dan menggunakan algoritma yang lebih tepat.
david.pfx
1
Isaac Newton: Mungkinkah bulan tertarik ke bumi dengan cara yang sama seperti apel tertarik ke bumi? @ david.pfx: Poin sebenarnya adalah bahwa ini adalah cara yang buruk untuk memetik apel. Newton perlu memikirkan kembali solusinya dan mungkin mempekerjakan seorang pria dengan tangga.
jwg
2
Komentar @jwg David mendapat banyak suara positif karena suatu alasan. Pertanyaan asli memperjelas bahwa algoritme tersebut digunakan untuk mencirikan kata sandi, dan itu memang algoritme yang buruk untuk tujuan itu - sangat mungkin untuk memutuskan versi kerangka .NET, seperti yang telah ditunjukkan. Setiap jawaban yang tidak menyebutkan bahwa OP perlu mengganti algoritmanya daripada "memperbaiki", itu akan merugikannya.
Chris
@Chris Terima kasih atas komentarnya, saya mengedit untuk menyertakan saran David. Saya tidak mengatakannya sekuat Anda, karena sistem OP mungkin mainan atau potongan kode yang dia buat untuk kesenangannya sendiri. Terima kasih!
Sergey Kalinichenko
27

Apa yang Anda lihat adalah kesalahan pembulatan dua kali lipat. Math.Powbekerja dengan double dan perbedaannya adalah sebagai berikut:

.NET 2.0 dan 3.5 => var powerResult = Math.Pow(ascii, e);mengembalikan:

1.2308248131348429E+174

.NET 4.0 dan 4.5 => var powerResult = Math.Pow(ascii, e);mengembalikan:

1.2308248131348427E+174

Perhatikan digit terakhir sebelumnya Edan itulah yang menyebabkan perbedaan hasil. Ini bukan operator modulus (%) .

Habib
sumber
3
sapi suci apakah ini SATU-SATUNYA jawaban untuk pertanyaan OP? Saya membaca semua meta "blah blah security wrong question I know more than you n00b" dan masih bertanya-tanya "mengapa ada perbedaan yang konsisten antara 3.5 dan 4.0? Pernah menginjak jari kaki Anda di atas batu saat melihat bulan dan bertanya" jenis batu apa apakah ini? "Hanya untuk diberi tahu" Masalah sebenarnya Anda tidak melihat kaki Anda "atau" Apa yang Anda harapkan saat mengenakan sandal buatan sendiri di malam hari? !!! "TERIMA KASIH!
Michael Paulukonis
1
@MichaelPaulukonis: Itu adalah analogi yang salah. Studi tentang batuan adalah pengejaran yang sah; melakukan aritmatika presisi sewenang-wenang menggunakan tipe data presisi tetap benar-benar salah. Saya akan membandingkan ini dengan perekrut perangkat lunak yang menanyakan mengapa anjing lebih buruk daripada kucing dalam menulis C #. Jika Anda seorang ahli zoologi, pertanyaan itu mungkin bermanfaat; untuk orang lain, itu tidak ada gunanya.
Douglas
24

Presisi floating-point dapat bervariasi dari mesin ke mesin, dan bahkan pada mesin yang sama .

Namun, .NET membuat mesin virtual untuk aplikasi Anda ... tetapi ada perubahan dari versi ke versi.

Oleh karena itu, Anda tidak boleh mengandalkannya untuk menghasilkan hasil yang konsisten. Untuk enkripsi, gunakan kelas yang disediakan Kerangka daripada menggulirkan kelas Anda sendiri.

Joe
sumber
10

Ada banyak jawaban tentang cara kode itu buruk. Namun, mengapa hasilnya berbeda…

FPU Intel menggunakan format 80-bit secara internal untuk mendapatkan lebih banyak presisi untuk hasil menengah. Jadi jika sebuah nilai ada di register prosesor, ia mendapat 80 bit, tetapi ketika ditulis ke tumpukan itu disimpan di 64 bit .

Saya berharap bahwa versi .NET yang lebih baru memiliki pengoptimal yang lebih baik dalam kompilasi Just in Time (JIT), jadi ia menyimpan nilai dalam register daripada menuliskannya ke stack dan kemudian membacanya kembali dari stack.

Mungkin JIT sekarang dapat mengembalikan nilai dalam register daripada di stack. Atau berikan nilai ke fungsi MOD dalam register.

Lihat juga pertanyaan Stack Overflow Apa aplikasi / manfaat dari tipe data presisi diperpanjang 80-bit?

Prosesor lain, misalnya ARM akan memberikan hasil yang berbeda untuk kode ini.

Ian Ringrose
sumber
6

Mungkin yang terbaik adalah menghitungnya sendiri dengan hanya menggunakan aritmatika integer. Sesuatu seperti:

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

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

Anda dapat membandingkan kinerja dengan kinerja solusi BigInteger yang diposting di jawaban lain.

Ronald
sumber
7
Itu akan membutuhkan 103 perkalian dan pengurangan modulus. Seseorang dapat melakukan lebih baik dengan menghitung e2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n, dll. Dan kemudian menghasilkan = e * e2% n * e4% n * e32% n * e64% n. Sebanyak 11 perkalian dan reduksi modulus. Mengingat besarnya jumlah yang terlibat, seseorang dapat menghilangkan beberapa pengurangan modulus lagi, tetapi itu akan kecil dibandingkan dengan mengurangi 103 operasi menjadi 11.
supercat
2
@supercat Matematika yang bagus, tetapi dalam praktiknya hanya relevan jika Anda menjalankannya di pemanggang roti.
alextgordon
7
@alextgordon: Atau jika seseorang berencana menggunakan nilai eksponen yang lebih besar. Memperluas nilai eksponen ke misalnya 65521 akan membutuhkan sekitar 28 kali pengali dan pengurangan modulus jika seseorang menggunakan pengurangan kekuatan, dibandingkan 65.520 jika tidak.
supercat
+1 untuk memberikan solusi yang dapat diakses di mana jelas persis bagaimana penghitungan dilakukan.
jwg
2
@Supercat: Anda benar sekali. Sangat mudah untuk meningkatkan algoritme, yang relevan jika algoritme tersebut sangat sering dihitung atau eksponennya besar. Tetapi pesan utamanya adalah bahwa itu dapat dan harus dihitung menggunakan aritmatika integer.
Ronald