Mümkün olduğunda bölmenin yerini çarpma ile değiştirmek iyi bir uygulamadır mı?


73

Ne zaman bölünmeye, örneğin durum kontrolüne ihtiyacım olursa, bölmenin ifadesini çarpma olarak yeniden yansıtmak istiyorum, örneğin:

Orijinal versiyon:

if(newValue / oldValue >= SOME_CONSTANT)

Yeni sürüm:

if(newValue >= oldValue * SOME_CONSTANT)

Çünkü önleyebileceğini düşünüyorum:

  1. Sıfıra bölüm

  2. oldValueÇok küçük olduğunda taşma

Bu doğru mu? Bu alışkanlık için bir sorun mu var?


41
Negatif sayılarla iki versiyonun tamamen farklı şeyleri kontrol etmesine dikkat edin. Bundan emin oldValue >= 0misin?
user2313067

37
Dile bağlı olarak (ama en önemlisi C ile), ne düşünürseniz düşünün, derleyici genellikle daha iyi yapabilir, -VEYA- hiç yapmamak için yeterli hissi vardır.
Mark Benningfield

63
X ve Y semantik olarak eşdeğer olmadığında, X kodunu daima Y koduyla değiştirmek asla “iyi bir uygulama” değildir. Ancak , X ve Y'ye bakmak, beyni açmak , gereksinimlerin ne olduğunu düşünmek ve sonra iki alternatifin hangisinin daha doğru olduğuna karar vermek her zaman iyi bir fikirdir . Ve bundan sonra, anlamsal farklılıkları doğru yaptığınızı doğrulamak için hangi testlerin yapılması gerektiğini düşünmelisiniz.
Doktor Brown

12
@MarkBenningfield: Ne olursa olsun, derleyici bölmeyi sıfıra göre en iyi duruma getiremez. Düşündüğünüz "optimizasyon", "hız optimizasyonu" dır. OP, başka bir optimizasyon türü düşünüyor - böceklerden kaçınma.
Slebetman

25
2. nokta sahtedir. Orijinal sürüm küçük değerler için taşabilir, ancak yeni sürüm büyük değerler için taşabilir, bu nedenle de her ikisi de daha güvenli değildir.
JacquesB

Yanıtlar:


74

Dikkate alınması gereken iki genel durum:

Tamsayılı aritmetik

Açıkçası tamsayı aritmetiği kullanıyorsanız (ki bu kısalır) farklı bir sonuç alırsınız. İşte C # küçük bir örnek:

public static void TestIntegerArithmetic()
{
    int newValue = 101;
    int oldValue = 10;
    int SOME_CONSTANT = 10;

    if(newValue / oldValue > SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue > oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Çıktı:

First comparison says it's not bigger.
Second comparison says it's bigger.

Kayan nokta aritmetiği

Bölünmenin sıfıra böldüğü zaman farklı bir sonuç verebilmesi (bir istisna oluşturur, oysa çarpma yapmaz) dışında, aynı zamanda biraz farklı yuvarlama hataları ve farklı bir sonuçla sonuçlanabilir. C # 'daki basit örnek:

public static void TestFloatingPoint()
{
    double newValue = 1;
    double oldValue = 3;
    double SOME_CONSTANT = 0.33333333333333335;

    if(newValue / oldValue >= SOME_CONSTANT)
    {
        Console.WriteLine("First comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("First comparison says it's not bigger.");
    }

    if(newValue >= oldValue * SOME_CONSTANT)
    {
        Console.WriteLine("Second comparison says it's bigger.");
    }
    else
    {
        Console.WriteLine("Second comparison says it's not bigger.");
    }
}

Çıktı:

First comparison says it's not bigger.
Second comparison says it's bigger.

Bana inanmamanız durumunda işte kendiniz uygulayabileceğiniz ve görebileceğiniz bir Keman .

Diğer diller farklı olabilir; Bununla birlikte, birçok dilde olduğu gibi C # 'nın da bir IEEE standardı (IEEE 754) kayan nokta kitaplığı uyguladığını , bu nedenle diğer standartlaştırılmış çalışma zamanlarında aynı sonuçları almanız gerektiğini unutmayın.

Sonuç

Yeşil tarlada çalışıyorsanız , muhtemelen sorun değil.

Eski kod üzerinde çalışıyorsanız ve uygulama aritmetik işlem yapan ve tutarlı sonuçlar sağlamak için gereken finansal veya diğer hassas bir uygulama ise, işlemleri değiştirirken çok dikkatli olun. Gerekirse, aritmetikteki ince değişiklikleri tespit edecek birim testler yaptığınızdan emin olun.

Yalnızca bir dizideki öğeleri sayma veya diğer genel işlemsel işlevler gibi şeyler yapıyorsanız, muhtemelen iyi olacaksınız. Çarpma yönteminin kodunuzu daha netleştirdiğinden emin değilim.

Bir şartnameye bir algoritma uyguluyorsanız, yalnızca yuvarlama hataları nedeniyle değil, hiçbir şeyi değiştirmeyeceğim, ancak geliştiricilerin kodu gözden geçirip herhangi bir ifadenin uygulanmamasını sağlamak için her bir ifadeyi tekrar belirtime eşleyebilirler. kusurları.


41
İkincisi finansal bit. Bu tür bir geçiş muhasebecilerin dirgenlerle sizi kovalamalarını istiyor. Dirgenleri uzak tutmak için "doğru" cevabı bulmaktan daha fazla çaba sarf etmem gereken 5000 çizgiyi hatırlıyorum - ki bu genellikle biraz yanlıştı. % 0,01 oranında kapalı olması önemli değildi, kesinlikle tutarlı cevaplar zorunluydu. Böylece hesaplamayı sistematik bir genel hataya neden olacak şekilde yapmak zorunda kaldım.
Loren Pechtel

8
5 kuruş şeker almayı düşünün (artık mevcut değil.) 20 adet satın alın, "doğru" cevap vergi değildi, çünkü 20 adet tek parça alımında vergi yoktu.
Loren Pechtel

24
@LorenPechtel, çünkü çoğu vergi sistemi, işlem başına verginin toplanmasına ilişkin bir kural (açık nedenlerden dolayı) içerir ve verginin, alemin en küçük madeni parasından daha küçük olmayan artışlarla ödenmesi ve fraksiyonel miktarların, vergi mükellefinin lehine yuvarlanması gerekir. Bu kurallar “doğru” çünkü yasal ve tutarlılar. Dirgenlere sahip muhasebeciler muhtemelen bilgisayar programcılarının uygulayamayacağı bir şekilde kuralların gerçekte ne olduğunu biliyorlar (aynı zamanda muhasebeciler de olmadıkça). % 0,01 hatası muhtemelen bir dengeleme hatasına neden olur ve dengeleme hatası olması yasadışıdır.
Steve

9
Çünkü daha önce yeşil alan terimini hiç duymadım, araştırdım. Wikipedia, "önceki çalışmaların dayattığı herhangi bir kısıtlamanın bulunmadığı bir proje" olduğunu söylüyor.
Henrik Ripa,

9
@ Steve: Patronum yakın zamanda "greenfield" ile "brownfield" arasında tezat oluşturdu. Bazı projelerin daha çok "blackfield" gibi olduğunu belirttim ... :-D
DevSolar

25

Sorunuzu, potansiyel olarak pek çok fikri kapsadığı için seviyorum. Genel olarak, cevabın , muhtemelen ilgili türlere ve kendi özel durumunuzdaki olası değer aralığına bağlı olduğundan şüpheliyim .

İlk içgüdüm tarz üzerine düşünmek. Yeni sürümünüz kodunuzun okuyucusu için daha az net. Yeni sürümünüzün amacını belirlemek için bir veya iki saniye (veya belki de daha uzun) düşünmem gerektiğini düşünürdüm, oysa eski sürümünüz hemen açık. Okunabilirlik, kodun önemli bir niteliğidir, bu nedenle yeni sürümünüzün bir maliyeti vardır.

Yeni sürümün sıfıra bölmeyi önlediği konusunda haklısın. Kuşkusuz, bir koruyucu eklemenize gerek yoktur (çizgileri boyunca if (oldValue != 0)). Fakat bu mantıklı geliyor mu? Eski sürümünüz iki sayı arasındaki oranı yansıtıyor. Bölen sıfır ise, oranınız belirsizdir. Bu sizin durumunuzda daha anlamlı olabilir, yani. Bu durumda bir sonuç vermemelisiniz.

Taşmalara karşı koruma tartışmalıdır. Bunun newValueher zaman olduğundan daha büyük olduğunu biliyorsanız oldValue, o zaman belki de bu tartışmayı yapabilirsiniz. Bununla birlikte, (oldValue * SOME_CONSTANT)taşabileceği durumlar da olabilir . Bu yüzden burada fazla kazanç görmüyorum.

Daha iyi performans elde ettiğinize dair bir argüman olabilir, çünkü çarpma bölmeden daha hızlı olabilir (bazı işlemcilerde). Bununla birlikte, bunun için önemli bir kazanç elde etmek için bunun gibi birçok hesaplama yapılması gerekecektir. erken optimizasyona dikkat edin.

Yukarıdakilerin hepsini yansıtarak, genel olarak, yeni sürümünüz ile eski sürümle karşılaştırıldığında, özellikle de netlikteki azalma göz önüne alındığında çok şey kazanılacağını sanmıyorum. Ancak, bazı yararların olduğu özel durumlar olabilir.


16
Ehm, keyfi çarpma işleminin keyfi bölmeden daha verimli olması, gerçek dünyadaki makineler için gerçekten işlemciye bağlı değildir.
Deduplicator

1
Tamsayı vs kayan nokta aritmetik sorunu da var. Oran kesirli ise, bölmenin bir döküm gerektiren kayan noktada yapılması gerekir. Oyuncuyu kaçırmak, istenmeyen bir hataya neden olur. Eğer fraksiyon iki küçük tam sayı arasında bir oransa, onları yeniden düzenlemek karşılaştırmanın tamsayı aritmetik olarak yapılmasını sağlar. (Hangi noktada iddialarınız geçerli olacaktır.)
saat

@ rwong Her zaman değil. Dışarıdaki birkaç dil, ondalık bölümü düşürerek tamsayı bölme işlemi yapar, bu nedenle oyuncular gerekmez.
T. Sar

@ T.Sar Anlattığınız teknik ve cevabında anlatılan anlambilim farklıdır. Anlambilim, programcının cevabı kayan nokta mı yoksa kesirli bir değer mi amaçladığını; Tarif ettiğiniz teknik, bazen bir tamsayı bölme için mükemmel bir yaklaşım (ikame) olan karşılıklı çarpımla bölmedir. Sonuncu teknik tipik olarak bölen önceden bilindiğinde uygulanır, çünkü tamsayılı karşılıklılığın (2 ** 32 ile kaydırılmış) türetilmesi derleme zamanında yapılabilir. İşlem sırasında bunu yapmak faydalı olmaz çünkü işlem daha pahalı.
rwong

22

Hayır.

Muhtemelen bunu derdim prematüre optimizasyon olursa olsun için optimize ediyoruz bakılmaksızın, geniş anlamda, performans ifade genellikle atıfta veya başka bir şey gibi, optimize edilebileceğini olarak, kenar-sayımı , kod satırlarını veya daha da genel olarak “tasarım” gibi şeyler .

Standart bir çalışma prosedürü olarak bu tür bir optimizasyon uygulamak, kodunuzun anlamını tehlikeye sokar ve potansiyel olarak kenarları gizler . Sessizce ortadan kaldırılması uygun gördüğünüz kenar kasalarının yine de açıkça ele alınması gerekebilir . Ve gürültülü kenarlardaki (istisnaları atanlar), sessizce başarısız olanların üstündeki problemleri ayıklamak için kesinlikle daha kolaydır .

Ve bazı durumlarda, okunabilirlik, netlik veya açıklık açısından "optimizasyonu" yapmak bile avantajlıdır. Çoğu durumda, kullanıcılarınız, son durum işlemlerini veya istisna işlemelerini önlemek için birkaç satır kod veya CPU döngüsü kaydettiğinizi farketmez. Garip ya da sessizce diğer taraftan, kod başarısız olur en azından iş arkadaşlarınızı - insanları etkiler. (Ve ayrıca, bu nedenle, yazılımı oluşturma ve sürdürme maliyeti.)

Uygulamanın etki alanına ve özel soruna göre daha "doğal" olan ve varsayılan olarak okunan değer. Basit, açık ve deyimsiz tutun. Önemli kazançlar için ya da meşru bir kullanılabilirlik eşiği elde etmek için gerekli olanı optimize edin .

Ayrıca not edin: Derleyiciler çoğu zaman sizin için bölünmeyi en iyi duruma getirir - bunu yapmak güvenli olduğunda.


11
-1 Bu cevap, bölünmenin potansiyel tuzakları ile ilgili olan soruya tam olarak uymuyor - optimizasyonla ilgisi yok
Ben Cottrell

13
@BenCottrell Mükemmel uyuyor. Tuzak, bakım maliyeti açısından anlamsız performans optimizasyonlarına değer katıyor. Sorusuna itibaren "bu alışkanlığı problem var mı?" - Evet. Çabucak mutlak anlamsızca yazmaya yol açacaktır.
Michael

9
@Michael, soru da bu şeylerden herhangi birini sormuyor - her biri farklı anlam ve davranışlara sahip, ancak her ikisinin de aynı gereksinime uyması amaçlanan iki farklı ifadenin doğruluğunu soruyor .
Ben Cottrell

5
@BenCottrell Belki de bana sorunda nerede doğruluk hakkında herhangi bir şeyden bahsetmediğine işaret edebilirsiniz?
Michael

5
@BenCottrell Sadece 'Yapamam' demeliydin :)
Michael

13

Hangisini daha az buggy ve mantıklı mantıklı kullanın.

Genellikle , bir değişkene bölünmek zaten kötü bir fikirdir, çünkü genellikle bölen sıfır olabilir.
Bir sabit tarafından bölünme genellikle sadece mantıksal anlamın ne olduğuna bağlıdır.

İşte duruma bağlı olduğunu göstermek için bazı örnekler:

Bölüm iyi:

if ((ptr2 - ptr1) >= n / 3)  // good: check if length of subarray is at least n/3
    ...

Çarpma kötü:

if ((ptr2 - ptr1) * 3 >= n)  // bad: confusing!! what is the intention of this code?
    ...

Çarpma iyi:

if (j - i >= 2 * min_length)  // good: obviously checking for a minimum length
    ...

Bölüm kötü:

if ((j - i) / 2 >= min_length)  // bad: confusing!! what is the intention of this code?
    ...

Çarpma iyi:

if (new_length >= old_length * 1.5)  // good: is the new size at least 50% bigger?
    ...

Bölüm kötü:

if (new_length / old_length >= 2)  // bad: BUGGY!! will fail if old_length = 0!
    ...

2
Bağlama bağlı olduğuna katılıyorum, ancak ilk iki örnek çiftiniz son derece zayıf. Her iki durumda da birini diğerine tercih etmem.
Michael

6
@Michael: Uhm ... (ptr2 - ptr1) * 3 >= nİfade kadar kolay anlaşılır bir şey buldun ptr2 - ptr1 >= n / 3mu? Beyninizi tekrar gezdirmez ve iki işaretçi arasındaki farkı üç katına çıkarmanın anlamını deşifre etmeye çalışarak tekrar ayağa kalkmaz mı? Siz ve ekibiniz için gerçekten açıksa, o zaman size daha fazla güç veririm; Sadece yavaş azınlıkta olmalıyım.
Mehrdad,

2
Adı verilen değişken nve isteğe bağlı 3 sayısı her iki durumda da kafa karıştırıcıdır, ancak makul isimlerle değiştirilir, hayır, ikisini de kafa karıştırıcı bulmuyorum.
Michael

1
Bu örnekler gerçekten fakir değil… kesinlikle “aşırı fakir” değil - “makul isimler” yazsanız bile, onları kötü durumlar için değiştirdiğinizde daha az anlam ifade ediyorlar. Bir projede yeniysem, bazı üretim kodlarını düzeltmeye gittiğimde bu cevapta listelenen 'iyi' durumları görmeyi tercih ederim.
John-M,

3

“Mümkün olduğunda” bir şey yapmak çok nadiren iyi bir fikirdir.

Bir numaralı önceliğiniz doğruluk olmalı, ardından okunabilirlik ve bakım yapılabilir olmalıdır. Kısmi olarak çarpma ile bölmenin kör şekilde değiştirilmesi, doğruluk bölümünde çoğu zaman başarısız olur, bazen sadece nadir görülür ve bu nedenle vakaları bulmak zordur.

Doğru ve en okunaklı olanı yapın. En okunaklı şekilde kod yazmanın performans sorununa yol açtığına dair sağlam kanıtlarınız varsa, değiştirmeyi düşünebilirsiniz. Bakım, matematik ve kod incelemeleri arkadaşlarınızdır.


1

İlgili okunabilirliği kodunun, ben çarpma aslında olduğunu düşünüyorum daha bazı durumlarda okunabilir. Örneğin, newValueyüzde 5 veya daha fazla artış gösterip göstermediğini kontrol etmeniz gereken bir şey varsa oldValue, o zaman 1.05 * oldValuetest edeceğiniz bir eşiktir newValueve yazmak doğaldır.

    if (newValue >= 1.05 * oldValue)

Ancak , bu şekilde şeyleri reddettiğinizde negatif sayılara dikkat edin (ya bölmeyi çarpma ile değiştirme ya da çarpma ile bölme değiştirme). Dikkate aldığınız iki koşul oldValue, negatif olmama garantili ise ; ama varsayalım newValueki aslında -13.5 ve oldValue-10.1. Sonra

newValue/oldValue >= 1.05

doğru olarak değerlendirir , ancak

newValue >= 1.05 * oldValue

false olarak değerlendirir .


1

Çarpım kullanarak Invariant Integers'ın ünlü makalesine dikkat edin .

Eğer tamsayı değişmez ise derleyici aslında çarpma yapıyor! Bölünme değil. Bu 2 değerin gücü olmadan bile olur. 2 bölümün gücü açıkçası bit kaymaları kullanır ve bu nedenle daha da hızlıdır.

Ancak, değişmeyen tamsayılar için kodu optimize etmek sizin sorumluluğunuzdadır. Optimize etmeden önce, gerçekten gerçek bir darboğazı optimize ettiğinizden ve doğruluğun feda edilmediğinden emin olun. Tamsayı taşmasına dikkat edin.

Mikro optimizasyonu önemsiyorum, bu yüzden muhtemelen optimizasyon olasılıklarına bir göz atacağım.

Kodunuzun üzerinde çalıştığı mimarileri de düşünün. Özellikle ARM, son derece yavaş bir bölünmeye sahiptir; bölmek için bir işlev çağırmanız gerekir, ARM'de bölme talimatı yoktur.

Ayrıca, 32-bit mimarilerde, öğrendiğim gibi 64-bit bölme optimize edilmedi .


1

2. noktanızı almak, gerçekten çok küçük bir taşma önleyecektir oldValue. Bununla birlikte, eğer SOME_CONSTANTaynı zamanda çok küçükse, alternatif yönteminiz, değerin doğru bir şekilde gösterilemediği, aşağı akışla sonuçlanacaktır.

Ve tam tersine, eğer oldValueçok büyükse ne olur ? Aynı sorunlarınız var, tam tersi.

Taşma / taşma riskinden kaçınmak (veya en aza indirmek) istiyorsanız, en iyi yöntem, newValuebüyüklük oldValueveya yakınlığa en yakın olup olmadığını kontrol etmektir SOME_CONSTANT. Ardından uygun bölme işlemini seçebilirsiniz.

    if(newValue / oldValue >= SOME_CONSTANT)

veya

    if(newValue / SOME_CONSTANT >= oldValue)

ve sonuç en doğru olacaktır.

Sıfıra bölmek için deneyimlerime göre bu, matematikte "çözülmek" için neredeyse hiçbir zaman uygun değildir. Sürekli çeklerinizde sıfıra bölüştüyseniz, neredeyse kesin olarak bazı analizler gerektiren bir durum vardır ve bu verilere dayanan hesaplamalar anlamsızdır. Açık bir sıfıra bölme kontrolü hemen her zaman uygun harekettir. (Burada "neredeyse" dediğimi unutmayın, çünkü yanılmaz olduğumu iddia etmiyorum. 20 yıllık gömülü yazılımlar yazarken bunun için iyi bir neden gördüğümü hatırlamadığımı ve devam edeceğimi unutmayın. .)

Ancak, uygulamanızda gerçek bir taşma / taşma riski varsa, bu muhtemelen doğru çözüm değildir. Daha büyük olasılıkla, genellikle algoritmanızın sayısal stabilitesini kontrol etmelisiniz veya belki de daha yüksek hassasiyetli bir gösterime geçmelisiniz.

Eğer kanıtlanmış bir taşma / taşma riski yoksa, hiçbir şey için endişelenmiyorsunuzdur. Bu, kelimenin tam anlamıyla , bir bakıcıya neden gerekli olduğunu açıklayan kodun yanındaki açıklamalarda sayılarla ihtiyacınızı kanıtlamanız gerektiği anlamına gelir . Başkalarının kodunu inceleyen bir ana mühendis olarak, bu konuda daha fazla çaba harcayan birine rastlarsam, şahsen daha az bir şey kabul etmem. Bu, erken optimizasyonun tam tersidir, ancak genel olarak aynı kök nedene sahip olacaktır - ayrıntı ile saplantı, işlevsel bir fark yaratmaz.


0

Koşullu aritmetiği anlamlı yöntem ve özelliklerle ifade edin. Sadece iyi adlandırma "A / B" ne söyleyecektir araçları , parametre kontrol ve hata işleme düzgünce çok orada gizleyebilirsiniz.

Önemli olarak, bu yöntemler daha karmaşık bir mantık içinde oluşturulduğundan, dışsal karmaşıklık çok yönetilebilir kalmaktadır.

Sorunun yanlış tanımlanmasından dolayı çarpma değişiminin makul bir çözüm olduğunu söyleyebilirim.


0

CPU'nun ALU'su (Aritmetik-Mantık Birimi) donanımda kullanılmasına rağmen algoritmalar yürüttüğü için çarpımları bölmelerle değiştirmenin iyi bir fikir olmadığını düşünüyorum. Daha yeni işlemcilerde daha karmaşık teknikler mevcuttur. Genellikle, işlemciler gereken saat döngülerini en aza indirmek için bit çiftleri işlemlerini paralel hale getirmeye çalışırlar. Çarpma algoritmaları oldukça etkili bir şekilde paralelleştirilebilir (daha fazla transistör gerekli olsa da). Bölme algoritmaları verimli olarak paralelleştirilemez. En verimli bölme algoritmaları oldukça karmaşıktır. Genellikle bit başına daha fazla saat döngüsü gerektirirler.

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.