Modulo operatörü (%), C # 'da farklı .NET sürümleri için farklı bir sonuç verir.


89

Parola dizesi oluşturmak için kullanıcının girişini şifreliyorum. Ancak bir kod satırı, çerçevenin farklı sürümlerinde farklı sonuçlar verir. Kullanıcı tarafından basılan tuş değerine sahip kısmi kod:

Tuşa basıldı: 1. Değişken ascii49'dur. Bazı hesaplamalardan sonra 'e' ve 'n' değeri:

e = 103, 
n = 143,

Math.Pow(ascii, e) % n

Yukarıdaki kodun sonucu:

  • .NET 3.5'te (C #)

    Math.Pow(ascii, e) % n
    

    verir 9.0.

  • .NET 4'te (C #)

    Math.Pow(ascii, e) % n
    

    verir 77.0.

Math.Pow() her iki versiyonda da doğru (aynı) sonucu verir.

Sebep nedir ve bir çözüm var mı?


12
Elbette sorudaki her iki cevap da yanlış. Bunu umursamıyor gibi görünmen aslında endişe verici.
David Heffernan

34
Birkaç adım geri gitmeniz gerekiyor. "Parola dizesi oluşturmak için kullanıcının girişini şifreliyorum" bu kısım zaten şüpheli. Gerçekte ne yapmak istiyorsun? Bir parolayı şifrelenmiş veya karma biçimde saklamak istiyor musunuz? Rastgele bir değer üretmek için bunu entropi olarak kullanmak ister misiniz? Güvenlik hedefleriniz neler?
CodesInChaos

49
Bu soru kayan nokta aritmetiğiyle ilgili ilginç bir sorunu ortaya koysa da, OP'nin amacı "kullanıcının girişini parola için bir dize oluşturmak için şifrelemek" ise, kendi şifrelemenizi atmanın iyi bir fikir olduğunu düşünmüyorum, bu yüzden tavsiye etmem aslında yanıtlardan herhangi birini uygulamak.
Harrison Paine

18
Diğer dillerin %kayan noktalı sayılarla kullanımını neden yasakladığını güzel bir gösteri .
Ben Voigt

5
Cevaplar iyi olsa da, hiçbiri .NET 3.5 ile 4 arasında farklı davranışa neden olan neyin değiştiği sorusuna cevap vermiyor.
msell

Yanıtlar:


160

Math.Powçift ​​duyarlıklı kayan nokta sayıları üzerinde çalışır; bu nedenle, sonucun ilk 15-17 hanesinden fazlasının doğru olmasını beklememelisiniz :

Tüm kayan noktalı sayıların sınırlı sayıda anlamlı basamağı da vardır, bu aynı zamanda bir kayan nokta değerinin gerçek bir sayıya ne kadar doğru yaklaştığını da belirler. Bir Doubledeğerin 15 ondalık basamağa kadar kesinlik vardır, ancak dahili olarak maksimum 17 basamak korunur.

Bununla birlikte, modulo aritmetiği tüm rakamların doğru olmasını gerektirir. Senin durumda, 49 hesaplıyoruz 103 , sonucun, 175 haneden oluşur, hem yanıtlarınıza modülo operasyon anlamsız hale.

Doğru değeri bulmak için, BigIntegersınıf tarafından sağlanan (.NET 4.0'da sunulan) rasgele duyarlıklı aritmetik kullanmanız gerekir .

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

Düzenleme : Aşağıdaki yorumlarda Mark Peters'ın belirttiği gibi, BigInteger.ModPowözellikle bu tür operasyonlar için tasarlanan yöntemi kullanmalısınız :

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

20
Gerçek soruna işaret etmek için +1, yani sorudaki kod tamamen yanlış
David Heffernan

36
BigInteger'ın bu işlem için yaklaşık 5 kat daha hızlı (şu anda yaptığım hızlı testte) bir ModPow () yöntemi sağladığını belirtmek gerekir.
Mark Peters

8
+1 düzenleme ile. ModPow sadece hızlı değil, sayısal olarak kararlı!
Ray

2
@maker Hayır, cevap anlamsız , geçersiz değil .
Cody Grey

3
@ makerofthings7: Prensip olarak sana katılıyorum. Bununla birlikte, belirsizlik kayan nokta aritmetiğinin doğasında vardır ve geliştiricilerin risklerin farkında olmasını beklemek, genel olarak operasyonlara kısıtlamalar getirmekten daha pratik kabul edilir. Eğer biri gerçekten "güvenli" olmak istiyorsa, o zaman dilin, 1.0 - 0.9 - 0.1 == 0.0değerlendirmek gibi beklenmedik sonuçlardan kaçınmak için kayan nokta eşitliği karşılaştırmalarını da yasaklaması gerekirdi false.
Douglas

72

Karma işlevinizin çok iyi bir işlev olmadığı gerçeğinin dışında * , kodunuzla ilgili en büyük sorun, .NET sürümüne bağlı olarak farklı bir sayı döndürmesi değil, her iki durumda da tamamen anlamsız bir sayı döndürmesidir: sorunun doğru cevabı

49 103 = 114 (143 mod Wolfram Alpha link )

Bu cevabı hesaplamak için bu kodu kullanabilirsiniz:

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

Hesaplamanızın farklı bir sonuç üretmesinin nedeni, bir cevap üretmek için 49103 sayısının önemli basamaklarının çoğunu düşüren bir ara değer kullanmanızdır : 175 basamağının yalnızca ilk 16'sı doğrudur!

1230824813134842807283798520430636310264067713738977819859474030746648511411697029659004340261471771152928833391663821316264359104254030819694748088798262075483562075061997649

Kalan 159 hanenin hepsi yanlış. Bununla birlikte mod işlemi, sonuncular da dahil olmak üzere her bir rakamın doğru olmasını gerektiren bir sonuç arar. Bu nedenle, Math.Pow.NET 4'te uygulanmış olabilecek kesinlikteki en ufak bir iyileştirme bile, hesaplamanızda esasen keyfi bir sonuç üreten büyük bir farkla sonuçlanacaktır.

* Bu soru, parola karması bağlamında tam sayıları yüksek güçlere yükseltmekten bahsettiğinden , mevcut yaklaşımınızın potansiyel olarak daha iyi bir yaklaşım için değiştirilip değiştirilmeyeceğine karar vermeden önce bu yanıt bağlantısını okumak çok iyi bir fikir olabilir .


20
İyi cevap. Gerçek nokta, bunun korkunç bir hash fonksiyonu olmasıdır. OP'nin çözümü yeniden düşünmesi ve daha uygun bir algoritma kullanması gerekir.
david.pfx

1
Isaac Newton: Elmanın dünyaya çekilmesi gibi ayın da dünyaya çekilmesi mümkün müdür? @ david.pfx: Asıl nokta, bunun elmaları seçmenin korkunç bir yolu olması. Newton'un çözümü yeniden düşünmesi ve belki de merdiveni olan bir adamı işe alması gerekiyor.
jwg

2
@jwg David'in yorumunun bir nedenden ötürü bu kadar çok oy aldı. Asıl soru, algoritmanın şifreleri hash etmek için kullanıldığını açıkça ortaya koydu ve bu gerçekten de bu amaç için korkunç bir algoritmadır - daha önce de gösterildiği gibi, .NET çerçevesinin sürümleri arasında büyük olasılıkla kırılma olasılığı yüksektir. OP'nin algoritmasını "düzeltmek" yerine değiştirmesi gerektiğinden bahsetmeyen herhangi bir cevap, ona bir kötülük yapıyor.
Chris

@Chris Yorumunuz için teşekkürler, David'in önerisini içerecek şekilde düzenledim. Bunu sizin kadar güçlü bir şekilde söylemedim, çünkü OP'nin sistemi bir oyuncak ya da kendi eğlencesi için oluşturduğu bir kod parçası olabilir. Teşekkürler!
Sergey Kalinichenko

27

Gördüğünüz şey ikiye yuvarlama hatasıdır. Math.Powdouble ile çalışır ve fark aşağıdaki gibidir:

.NET 2.0 ve 3.5 => şunu var powerResult = Math.Pow(ascii, e);döndürür:

1.2308248131348429E+174

.NET 4.0 ve 4.5 => şunu var powerResult = Math.Pow(ascii, e);döndürür:

1.2308248131348427E+174

Son basamağa dikkat edin ve Esonuçta farklılığa neden oluyor. Modül operatörü değil (%) .


3
kutsal inek bu OPs sorusunun TEK cevabı mı? Tüm meta "blah blah security yanlış sorusunu sizden daha fazla biliyorum n00b" yi okudum ve hala "3.5 ile 4.0 arasında neden tutarlı bir tutarsızlık var? Ay'a bakarken ayak parmağınızı bir kayaya çarptı ve" ne tür bir kaya Bu mu? "Sadece" Asıl sorunun ayaklarına
bakmamak

1
@MichaelPaulukonis: Bu yanlış bir benzetme. Kayaların incelenmesi meşru bir arayıştır; Sabit duyarlıklı veri türlerini kullanarak keyfi kesinlikte aritmetik yapmak tamamen yanlıştır. Bunu, C # yazmada köpeklerin neden kedilerden daha kötü olduğunu araştıran bir yazılım uzmanıyla karşılaştırırım. Eğer bir zoologsanız, soru biraz değer taşıyabilir; diğer herkes için anlamsız.
Douglas

24

Kayan nokta hassasiyeti makineden makineye ve hatta aynı makinede değişebilir .

Ancak .NET, uygulamalarınız için sanal bir makine oluşturur ... ancak sürümden sürüme değişiklikler vardır.

Bu nedenle, tutarlı sonuçlar üretmek için ona güvenmemelisiniz. Şifreleme için, kendi sınıfınızı döndürmek yerine Framework'ün sağladığı sınıfları kullanın.


10

Kodun kötü olma şekliyle ilgili pek çok yanıt var. Ancak sonucun neden farklı olduğuna gelince ...

Intel'in FPU'ları , ara sonuçlar için daha fazla kesinlik elde etmek için dahili olarak 80 bit formatı kullanır . Yani işlemci kaydında bir değer varsa 80 bit alır, ancak yığına yazıldığında 64 bitte depolanır .

NET'in daha yeni sürümünün Tam Zamanında (JIT) derlemesinde daha iyi bir iyileştirici olmasını bekliyorum, bu nedenle bir değeri yığına yazmak ve ardından yığından geri okumak yerine bir kayıt defterinde tutuyor.

JIT artık yığın yerine bir kayıtta bir değer döndürebilir. Veya değeri bir kayıt defterindeki MOD işlevine iletin.

Ayrıca bkz. Yığın Taşması sorusu 80 bitlik genişletilmiş hassas veri türünün uygulamaları / avantajları nelerdir?

Diğer işlemciler, örneğin ARM bu kod için farklı sonuçlar verecektir.


6

Belki de bunu yalnızca tam sayı aritmetiği kullanarak kendiniz hesaplamak en iyisidir. Gibi bir şey:

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

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

Performansı, diğer yanıtlarda yayınlanan BigInteger çözümünün performansıyla karşılaştırabilirsiniz.


7
Bu, 103 çarpma ve modül azaltma gerektirir. E2 = e * e% n, e4 = e2 * e2% n, e8 = e4 * e4% n, vb. Hesaplanarak daha iyi yapılabilir ve sonra sonuç = e * e2% n * e4% n * e32% n * e64% n. Toplam 11 çarpma ve modül azaltma. İlgili sayıların boyutu göz önüne alındığında, birkaç modül azaltımı daha ortadan kaldırılabilir, ancak bu 103 işlemi 11'e düşürmekle karşılaştırıldığında küçük olacaktır.
supercat

2
@supercat Güzel matematik, ancak pratikte yalnızca bunu bir ekmek kızartma makinesinde çalıştırıyorsanız geçerlidir.
alextgordon

7
@alextgordon: Veya biri daha büyük üs değerleri kullanmayı planlıyorsa. Üs değerinin örneğin 65521'e genişletilmesi, güç azaltma kullanılıyorsa yaklaşık 28 çarpma ve modül azalması alırken, kullanılmıyorsa 65.520 gerekir.
supercat

Hesaplamanın tam olarak nasıl yapıldığının açık olduğu erişilebilir bir çözüm sağlamak için +1.
jwg

2
@Supercat: Kesinlikle haklısın. Algoritmayı geliştirmek kolaydır; bu, ya çok sık hesaplanırsa ya da üsler büyükse önemlidir. Ancak ana mesaj, tam sayı aritmetiği kullanılarak hesaplanabileceği ve hesaplanması gerektiğidir.
Ronald
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.