Kayan nokta değerlerini karşılaştırmak ne kadar tehlikelidir?


391

Çözünürlük bağımsız koordinat sistemi nedeniyle UIKitkullanımlarını biliyorum CGFloat.

Ancak her zaman örneğin olmadığını kontrol etmek istiyorum frame.origin.xedilmektedir 0beni hasta hissettiren:

if (theView.frame.origin.x == 0) {
    // do important operation
}

Is not CGFloatile karşılaştırırken yanlış pozitif karşı savunmasız ==, <=, >=, <, >? Bu bir kayan nokta ve kesin olmayan problemleri var: 0.0000000000041örneğin.

Objective-CBunu karşılaştırma sırasında dahili olarak ele almak mı origin.xyoksa sıfır olarak okunan a'nın 0doğru ile karşılaştırılmaması olabilir mi?

Yanıtlar:


466

Her şeyden önce, kayan nokta değerleri davranışlarında "rastgele" değildir. Tam karşılaştırma, birçok gerçek dünya kullanımında mantıklı olabilir ve mantıklıdır. Ancak kayan nokta kullanacaksanız, nasıl çalıştığının farkında olmanız gerekir. Gerçek sayılar gibi çalışır gibi kayan nokta varsayım yan tarafında erring hızlı bir şekilde kırmak kodu alacak. Kayan nokta sonuçlarının yanılsama tarafındaki erringler, kendileriyle ilişkili büyük rastgele tüylere sahiptir (buradaki cevapların çoğunun önerdiği gibi), ilk başta çalışıyor gibi görünen ancak büyük büyüklükteki hatalara ve kırık köşe vakalarına sahip olan kodu alacaktır.

Her şeyden önce, kayan nokta ile programlamak istiyorsanız, bunu okumalısınız:

Her Bilgisayar Bilimcisinin Kayan Nokta Aritmetiği Hakkında Bilmesi Gerekenler

Evet, hepsini oku. Bu çok fazla bir yük ise, okumalarınız için zamanınız olana kadar hesaplamalarınızda tamsayılar / sabit nokta kullanmalısınız. :-)

Şimdi, bununla birlikte, tam kayan nokta karşılaştırmalarıyla ilgili en büyük sorunlar şöyledir:

  1. Değerlerin çoğu da kaynağında yazabilir veya giriş okuyabilirsiniz gerçeği scanfya strtod, var olmayan kayan nokta değerleri olarak ve sessizce yakın yakınlaştırılmasına dönüştürülür. Bu demon9733'ün cevabından bahsediyordu.

  2. Gerçek sonucu temsil etmek için yeterli hassasiyete sahip olmadığı için birçok sonucun yuvarlanması. Bunu görebileceğiniz kolay bir örnek ekleme x = 0x1fffffeve y = 1kayan reklamlardır. Burada, xmantiste 24 bit hassasiyete sahiptir (ok) ve ysadece 1 bit vardır, ancak bunları eklediğinizde, bitleri çakışan yerlerde değildir ve sonuç 25 bit hassasiyete ihtiyaç duyar. Bunun yerine yuvarlanır ( 0x2000000varsayılan yuvarlama modunda).

  3. Doğru sonuç için sonsuz sayıda yere ihtiyaç duyulması nedeniyle birçok sonucun yuvarlanması. Bu, 1/3 (sonsuz sayıda yer aldığı ondalıktan bildiğiniz) gibi rasyonel sonuçları da içerir, ancak 1/10 (5'in gücü olmadığı için ikili olarak da sonsuz sayıda yer alır), mükemmel bir kare olmayan her şeyin karekökü gibi irrasyonel sonuçların yanı sıra.

  4. Çift yuvarlama. Bazı sistemlerde (özellikle x86), kayan nokta ifadeleri nominal türlerinden daha yüksek hassasiyetle değerlendirilir. Bu, yukarıdaki yuvarlama türlerinden biri gerçekleştiğinde, ilk önce daha yüksek hassasiyetli türe yuvarlama, ardından son türe yuvarlama olmak üzere iki yuvarlama adımı alacağınız anlamına gelir. Örnek olarak, 1.49 değerini bir tamsayıya (1) yuvarlarsanız ondalık basamakta ne olacağını düşünün; ilk önce bir ondalık basamağa (1.5) yuvarlarsanız, ardından bu sonucu bir tamsayıya (2) yuvarlarsanız ne olacağını düşünün. Bu aslında kayan nokta ile başa çıkmak için en nazik alanlardan biridir, çünkü derleyicinin davranışı (özellikle GCC gibi buggy, uygun olmayan derleyiciler için) tahmin edilemez.

  5. Transendental işlevleri ( trig, exp, logvs.) doğru yuvarlatılmış sonuçlara sahip olduğu belirtilmemiştir; sonuç sadece son bir hassasiyet yerinde (genellikle 1ul olarak adlandırılır ) bir birim içinde doğru olarak belirtilir .

Kayan nokta kodu yazarken, sonuçların hatalı olmasına neden olabilecek sayılarla ne yaptığınızı akılda tutmanız ve buna göre karşılaştırmalar yapmanız gerekir. Çoğu zaman bir "epsilon" ile karşılaştırmak mantıklı olacaktır, ancak bu epsilon , karşılaştırdığınız sayıların büyüklüğüne dayanmalıdır, mutlak bir sabit değil. (Mutlak sabit bir epsilonun işe yarayacağı durumlarda, bu, kayan nokta değil, sabit noktanın iş için doğru araç olduğunu güçlü bir şekilde gösterir!)

Düzenle: Özellikle, göreceli bir epsilon kontrolü şöyle görünmelidir:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y))

Burada FLT_EPSILONbir sürekli float.h(ile değiştirin DBL_EPSILONiçin doubles veya LDBL_EPSILONiçin long doubles) veK size hesaplamaların biriken hata kesinlikle sınırlanmış şekildedir seçim bir sabittir Kson sırada birimlere (eğer emin değilseniz ve hata var bağlı hesaplama hakkı, Khesaplamalarınızın olması gerektiği gibi birkaç kat daha büyük yapın).

Son olarak, bunu kullanırsanız, sıfıra yakın bir miktar özel bakım gerekebilir, çünkü FLT_EPSILON denormaller için mantıklı olmadığı için . Hızlı bir düzeltme bunu yapmak olacaktır:

if (fabs(x-y) < K * FLT_EPSILON * fabs(x+y) || fabs(x-y) < FLT_MIN)

ve aynı şekilde DBL_MINiki kat kullanıldığında ikame edilir.


25
fabs(x+y)xve y( eğer ) farklı işarete sahipse sorunludur . Yine de, kargo kült karşılaştırmaları gelgit karşı iyi bir cevap.
Daniel Fischer

27
Farklı bir işaret varsa xve ysorun değil. Sağ taraf "çok küçük" olacaktır, ancak farklı işaretlere sahip olduklarından xve yyine de eşit karşılaştırmamalıdırlar. (Denormal kadar küçük sürece, ama sonra ikinci dava yakalar)
R. .. GitHub DURDURMAK BUZ 26:12

4
İfadenizi merak ediyorum: "özellikle GCC gibi buggy, uyumsuz derleyiciler için". Gerçekten GCC arabası ve aynı zamanda uyumsuz mu?
Nicolás Ozimica

3
Soru iOS olarak etiketlendiğinden, Apple'ın derleyicilerinin (hem clang hem de Apple'ın gcc derlemeleri) her zaman FLT_EVAL_METHOD = 0 kullandığını ve aşırı hassasiyet taşıma konusunda tamamen katı olmaya çalıştığını belirtmek gerekir. Herhangi bir ihlal tespit ederseniz, lütfen hata raporları gönderin.
Stephen Canon

17
"Her şeyden önce, kayan nokta değerleri davranışlarında" rastgele "değildir. Tam karşılaştırma, birçok gerçek dünya kullanımında mantıklı olabilir ve mantıklıdır." - Sadece iki cümle ve zaten +1 kazandı! Bu, kayan noktalarla çalışırken insanların yaptığı en rahatsız edici yanlış varsayımlardan biridir.
Christian Rau

36

0 tam olarak bir IEEE754 kayan nokta sayısı olarak temsil edilebildiğinden (veya şimdiye kadar çalıştığım fp numaralarının başka bir uygulamasını kullanarak) 0 ile karşılaştırma muhtemelen güvenlidir. Bununla birlikte, programınız theView.frame.origin.x0 olması gerektiğine inanmanız için nedeniniz olan ancak hesaplamanızın 0 olmasını garanti edemediği bir değer (örneğin ) hesaplarsa ısırılabilir .

Biraz açıklığa kavuşturmak için, aşağıdaki gibi bir hesaplama:

areal = 0.0

(diliniz veya sisteminiz bozulmazsa) (areal == 0.0) true değerini döndürecek ancak

areal = 1.386 - 2.1*(0.66)

olmayabilir.

Hesaplamalarınızın 0 olan değerler ürettiğinden (ve yalnızca 0 olması gereken değerleri üretmediklerinden) emin olabilirseniz, fp değerlerini 0 ile karşılaştırabilirsiniz. Kendinizi gereken dereceye kadar garanti edemezseniz , en iyi 'toleranslı eşitlik' yaklaşımına sadık kalın.

En kötü durumlarda, fp değerlerinin dikkatsiz karşılaştırması son derece tehlikeli olabilir: aviyonik, silah rehberliği, santral operasyonları, araç navigasyonu, hesaplamanın gerçek dünyayla buluştuğu neredeyse tüm uygulamaları düşünün.

Angry Birds için, o kadar tehlikeli değil.


11
Aslında, 1.30 - 2*(0.65)çift olarak temsil çünkü derleyici uygular 754 IEEE eğer belli 0.0 değerlendirir bir ifadenin mükemmel bir örneği, 0.65ve 1.30aynı Önemli basamaklar vardır ve iki ile çarpımı kesin olduğu açıktır.
Pascal Cuoq

7
Yine de bu rapordan tekrar alıyorum, bu yüzden ikinci örnek parçacığı değiştirdim.
Yüksek Performanslı Mark

22

Diğerlerinden biraz farklı bir cevap vermek istiyorum. Sorunuzu belirtildiği gibi cevaplamak için harikalar, ancak muhtemelen bilmeniz gerekenler veya gerçek probleminiz için değil.

Grafiklerde kayan nokta iyi! Ancak şamandıraları doğrudan karşılaştırmaya neredeyse hiç gerek yoktur. Bunu neden yapmanız gerekiyor? Grafikler aralıkları tanımlamak için kayan nokta kullanır. Ve bir şamandıranın şamandıralar tarafından da tanımlanan bir aralık içinde olup olmadığını karşılaştırmak her zaman iyi tanımlanmıştır ve sadece tutarlı, doğru veya kesin değil olması gerekir! Tüm grafik ihtiyaçları olan bir piksel (bu da bir aralıktır!) Atanabildiği sürece.

Eğer noktanızın [0..width [aralık dışında) olup olmadığını test etmek istiyorsanız, bu gayet iyi. Dahil etmeyi tutarlı bir şekilde tanımladığınızdan emin olun. Örneğin her zaman içeride tanımlayın (x> = 0 && x <genişlik). Aynı şey kavşak veya çarpma testleri için de geçerlidir.

Ancak, bir grafik koordinatını, örneğin bir pencerenin sabitlenmiş olup olmadığını görmek gibi bir tür bayrak olarak kötüye kullanıyorsanız, bunu yapmamalısınız. Bunun yerine grafik sunumu katmanından ayrı bir boole bayrağı kullanın.


13

Sıfıra hesaplanmış bir değer olmadığı sürece (yukarıdaki cevapta belirtildiği gibi) sıfıra karşılaştırmak güvenli bir işlem olabilir . Bunun nedeni sıfırın kayan noktada mükemmel bir şekilde temsil edilebilen bir sayı olmasıdır.

Mükemmel temsil edilebilir değerlerden bahsetmişken, iki güç kavramında 24 bitlik aralık elde edersiniz (tek hassasiyet). Yani 1, 2, 4, .5, .25 ve .125 gibi mükemmel bir şekilde temsil edilebilir. Tüm önemli bitleriniz 24 bitlik olduğu sürece altınsınız. Böylece 10.625 tam olarak tekrarlanabilir.

Bu harika, ama hızla baskı altında parçalanacak. Akla gelen iki senaryo: 1) Bir hesaplama söz konusu olduğunda. Sqrt (3) * sqrt (3) == 3'e güvenmeyin. Sadece bu şekilde olmayacak. Ve diğer cevapların önerdiği gibi, muhtemelen bir epsilon içinde olmayacak. 2) 2 gücü olmayan (NPOT) dahil olduğunda. Bu yüzden kulağa garip gelebilir, ancak 0.1 ikili olarak sonsuz bir seridir ve bu nedenle böyle bir sayı içeren herhangi bir hesaplama başlangıçtan itibaren kesin olmayacaktır.

(Oh ve asıl soru sıfıra karşılaştırmaları belirtmiştir. -0.0'ın da mükemmel şekilde geçerli bir kayan nokta değeri olduğunu unutmayın.)


11

['Doğru cevap' seçim üzerine parlıyor K. Seçim K, seçim kadar geçici VISIBLE_SHIFTolmakla birlikte, herhangi bir display özelliğine dayanmadığı için seçim Kdaha az belirgindir VISIBLE_SHIFT. Böylece zehirinizi seçin - seçin Kveya seçin VISIBLE_SHIFT. Bu cevap seçmeyi savunuyor VISIBLE_SHIFTve seçmekte zorlanıyor K]

Tam olarak yuvarlak hatalar nedeniyle, mantıksal işlemler için 'kesin' değerlerin karşılaştırmasını kullanmamalısınız. Görsel bir ekrandaki belirli bir konumunuzda, konumun 0.0 veya 0.0000000003 olması önemli değildir - fark gözle görülemez. Yani mantığınız şöyle olmalıdır:

#define VISIBLE_SHIFT    0.0001        // for example
if (fabs(theView.frame.origin.x) < VISIBLE_SHIFT) { /* ... */ }

Ancak, sonunda, 'gözle görülmez' görüntü özelliklerinize bağlı olacaktır. Ekranı üst sınırlandırabiliyorsanız (yapabilmeniz gerekir); sonra VISIBLE_SHIFTo üst sınırın bir parçası olmayı seçin .

Şimdi, 'doğru cevap' dayanmaktadır, Kbu yüzden seçmeyi keşfedelim K. Yukarıdaki 'doğru cevap' diyor:

K, hesaplamalarınızda biriken hatanın son olarak K birimleri tarafından kesinlikle sınırlandırılacağı bir sabittir (ve hataya bağlı hesaplamayı doğru yaptığınızdan emin değilseniz, K'yi hesaplamalarınızdan birkaç kat daha büyük yapın olması gerektiğini söyle)

Yani ihtiyacımız var K. Eğer almak Kdaha zor, benim seçmekten daha az sezgisel ise, VISIBLE_SHIFTo zaman sizin için neyin işe yaradığına karar vereceksiniz. Bulmak Kiçin, bir dizi Kdeğere bakan bir test programı yazacağız, böylece nasıl davrandığını görebiliriz. K'Doğru cevap' kullanılabilir durumdaysa nasıl seçileceğinin açık olması gerekir . Hayır?

'Doğru cevap' ayrıntıları olarak kullanacağız:

if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)

Tüm K değerlerini deneyelim:

#include <math.h>
#include <float.h>
#include <stdio.h>

void main (void)
{
  double x = 1e-13;
  double y = 0.0;

  double K = 1e22;
  int i = 0;

  for (; i < 32; i++, K = K/10.0)
    {
      printf ("K:%40.16lf -> ", K);

      if (fabs(x-y) < K * DBL_EPSILON * fabs(x+y) || fabs(x-y) < DBL_MIN)
        printf ("YES\n");
      else
        printf ("NO\n");
    }
}
ebg@ebg$ gcc -o test test.c
ebg@ebg$ ./test
K:10000000000000000000000.0000000000000000 -> YES
K: 1000000000000000000000.0000000000000000 -> YES
K:  100000000000000000000.0000000000000000 -> YES
K:   10000000000000000000.0000000000000000 -> YES
K:    1000000000000000000.0000000000000000 -> YES
K:     100000000000000000.0000000000000000 -> YES
K:      10000000000000000.0000000000000000 -> YES
K:       1000000000000000.0000000000000000 -> NO
K:        100000000000000.0000000000000000 -> NO
K:         10000000000000.0000000000000000 -> NO
K:          1000000000000.0000000000000000 -> NO
K:           100000000000.0000000000000000 -> NO
K:            10000000000.0000000000000000 -> NO
K:             1000000000.0000000000000000 -> NO
K:              100000000.0000000000000000 -> NO
K:               10000000.0000000000000000 -> NO
K:                1000000.0000000000000000 -> NO
K:                 100000.0000000000000000 -> NO
K:                  10000.0000000000000000 -> NO
K:                   1000.0000000000000000 -> NO
K:                    100.0000000000000000 -> NO
K:                     10.0000000000000000 -> NO
K:                      1.0000000000000000 -> NO
K:                      0.1000000000000000 -> NO
K:                      0.0100000000000000 -> NO
K:                      0.0010000000000000 -> NO
K:                      0.0001000000000000 -> NO
K:                      0.0000100000000000 -> NO
K:                      0.0000010000000000 -> NO
K:                      0.0000001000000000 -> NO
K:                      0.0000000100000000 -> NO
K:                      0.0000000010000000 -> NO

Ah, yani 1e-13'ün 'sıfır' olmasını istiyorsam K 1e16 veya daha büyük olmalıdır.

İki seçeneğiniz olduğunu söyleyebilirim:

  1. Önerdiğim gibi, 'epsilon' değeri için mühendislik kararınızı kullanarak basit bir epsilon hesaplaması yapın . Grafik yapıyorsanız ve 'sıfır', görsel varlıklarınızı (resimler vb.) İncelemek ve epsilonun ne olabileceğini değerlendirmekten ziyade 'görünür bir değişiklik' anlamına gelir.
  2. Cargo-kült cevap verenin referansını okuyana kadar (ve işlem sırasında doktora derecenizi elde edene kadar) herhangi bir kayan noktalı hesaplama yapmaya çalışmayın ve ardından seçmek için sezgisel olmayan kararınızı kullanın K.

10
Çözünürlük bağımsızlığının bir yönü, derleme zamanında bir "görünür kaymanın" ne olduğundan emin olamamanızdır. Bir süper HD ekranda görünmeyen şey, küçük bir ekranda çok açık olabilir. En azından ekran boyutunun bir fonksiyonu haline getirilmelidir. Ya da başka bir ad verin.
Romain

1
Ancak en azından 'görünür kaydırma' seçilmesi, anlaşılması Kzor olan ve seçilmesi zor olan <doğru cevaplayıcıdan> farklı olarak kolayca anlaşılan ekran (veya çerçeve) özelliklerine dayanır .
GoZoner

5

Doğru soru: Cocoa Touch'taki puanlar nasıl karşılaştırılır?

Doğru cevap: CGPointEqualToPoint ().

Farklı bir soru: Hesaplanan iki değer aynı mı?

Burada yayınlanan cevap: Onlar değil.

Yakın olup olmadıklarını nasıl kontrol edebilirim? Yakın olup olmadıklarını kontrol etmek istiyorsanız, CGPointEqualToPoint () kullanmayın. Ancak, yakın olup olmadıklarını kontrol etmeyin. Gerçek dünyada mantıklı bir şey yapın, bir noktanın çizginin ötesinde olup olmadığını veya bir noktanın kürenin içinde olup olmadığını kontrol etmek gibi.


4

C standardını en son kontrol ettiğimde, çiftler (toplam 64 bit, 53 bit mantis) üzerinde kayan nokta işlemlerinin bu hassasiyetten daha doğru olması için herhangi bir gereklilik yoktu. Bununla birlikte, bazı donanımlar işlemleri daha hassas kayıtlarda yapabilir ve gereksinim, daha düşük sıralı bitleri (kayıtlara yüklenen sayıların kesinliğinin ötesinde) temizleme gerekliliği anlamına gelmediği şeklinde yorumlandı. Böylece, orada en son uyuyanlardan kayıtlarda kalanlara bağlı olarak, bu tür karşılaştırmaların beklenmedik sonuçlarını alabilirsiniz.

Bununla birlikte, her gördüğümde onu silmeye yönelik çabalarıma rağmen, çalıştığım kıyafetin gcc kullanılarak derlenen ve linux üzerinde çalışan çok sayıda C kodu var ve bu beklenmedik sonuçlardan hiçbirini çok uzun süre fark etmedik . Bunun gcc'nin bizim için düşük dereceli bitleri temizlediği, 80 bit kayıtların modern bilgisayarlarda bu işlemler için kullanılmadığı, standardın değiştiği veya ne olduğu nedeniyle olup olmadığı hakkında hiçbir fikrim yok. Kimsenin bölüm ve ayet teklif edip edemeyeceğini bilmek istiyorum.


1

Bu kodu float'ı sıfır ile karşılaştırmak için kullanabilirsiniz:

if ((int)(theView.frame.origin.x * 100) == 0) {
    // do important operation
}

Bu, 0.1 doğrulukla karşılaştırılacaktır, bu durumda CGFloat için yeterlidir.


İçin döküm int, temin olmayan theView.frame.origin.xbir bölgesindeki / bu dizi yakın inttanımlanmamış bir davranış (UB) neden olur ve - ya da bu durumda, aralığı 1 / 100'ü int.
chux - Eski Monica

Bunun gibi tamsayıya dönüştürmek için kesinlikle hiçbir neden yoktur. Chux'un dediği gibi UB için aralık dışı değerlerden potansiyel var; ve bazı mimarilerde bu sadece kayan noktada hesaplamayı yapmaktan çok daha yavaş olacaktır. Son olarak, 100 ile çarpmak 0.1 ile değil 0.01 hassasiyetle karşılaştırılacaktır.
Sneftel

0
-(BOOL)isFloatEqual:(CGFloat)firstValue secondValue:(CGFloat)secondValue{

BOOL isEqual = NO;

NSNumber *firstValueNumber = [NSNumber numberWithDouble:firstValue];
NSNumber *secondValueNumber = [NSNumber numberWithDouble:secondValue];

isEqual = [firstValueNumber isEqualToNumber:secondValueNumber];

return isEqual;

}


0

Ondalık basamak sayısını karşılaştırmak için aşağıdaki karşılaştırma işlevini kullanıyorum:

bool compare(const double value1, const double value2, const int precision)
{
    int64_t magnitude = static_cast<int64_t>(std::pow(10, precision));
    int64_t intValue1 = static_cast<int64_t>(value1 * magnitude);
    int64_t intValue2 = static_cast<int64_t>(value2 * magnitude);
    return intValue1 == intValue2;
}

// Compare 9 decimal places:
if (compare(theView.frame.origin.x, 0, 9)) {
    // do important operation
}

-6

Doğru şeyi söyleyebilirim, her sayıyı bir nesne olarak tanımlamak ve daha sonra bu nesnede üç şeyi tanımlamaktır: 1) bir eşitlik operatörü. 2) bir setAcceptableDifference yöntemi. 3) değerin kendisi. İki değerin mutlak farkı kabul edilebilir olarak ayarlanan değerden küçükse eşitlik operatörü true değerini döndürür.

Soruna uyacak şekilde nesneyi alt sınıflandırabilirsiniz. Örneğin, çapları 0.0001 inçten daha az farklıysa, 1 ila 2 inç arasındaki yuvarlak metal çubuklar eşit çapta kabul edilebilir. Böylece 0.0001 parametresi ile setAcceptableDifference öğesini çağırır ve ardından eşitlik operatörünü güvenle kullanırsınız.


1
Bu İyi Bir Yanıt Değil. İlk olarak, tüm "nesne şey" sorununuzu çözmek için hiçbir şey yapmaz. İkincisi, gerçekte “eşitlik” uygulaması aslında doğru değildir.
Tom Swirly

3
Tom, belki tekrar "nesne meselesi" ni düşünürsün. Yüksek hassasiyetle temsil edilen gerçek sayılarla eşitlik nadiren olur. Fakat kişinin eşitliği fikri size uygunsa uyarlanabilir. Geçersiz kılınabilen 'yaklaşık olarak eşit' bir operatör olsaydı daha iyi olurdu, ama olmazdı.
John White
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.