Optimizasyon etkinken farklı kayan nokta sonucu - derleyici hatası?


109

Aşağıdaki kod, Visual Studio 2008 üzerinde optimizasyonlu ve optimizasyonsuz çalışır. Ancak, optimizasyon olmadan yalnızca g ++ üzerinde çalışır (O0).

#include <cstdlib>
#include <iostream>
#include <cmath>

double round(double v, double digit)
{
    double pow = std::pow(10.0, digit);
    double t = v * pow;
    //std::cout << "t:" << t << std::endl;
    double r = std::floor(t + 0.5);
    //std::cout << "r:" << r << std::endl;
    return r / pow;
}

int main(int argc, char *argv[])
{
    std::cout << round(4.45, 1) << std::endl;
    std::cout << round(4.55, 1) << std::endl;
}

Çıktı şöyle olmalıdır:

4.5
4.6

Ancak optimizasyonlu g ++ ( O1- O3) şu çıktıyı verir:

4.5
4.5

Ben eklerseniz volatilet önce anahtar kelime, çalışır, bu yüzden optimizasyon böcek bir tür olabilir?

G ++ 4.1.2 ve 4.4.4'te test edin.

İdeone üzerindeki sonuç: http://ideone.com/Rz937

Ve g ++ üzerinde test ettiğim seçenek basit:

g++ -O2 round.cpp

Daha ilginç sonuç, /fp:fastVisual Studio 2008'de seçeneği açsam bile , sonuç yine de doğrudur.

İlave soru:

Merak ediyordum, -ffloat-storeseçeneği her zaman açmalı mıyım?

Ben test gr ++ sürüm Çünkü sevk CentOs / Red Hat Linux 6 Redhat 5 ve CentOS / .

Programlarımın çoğunu bu platformlar altında derledim ve programlarımın içinde beklenmedik hatalara neden olacağından endişeliyim. Tüm C ++ kodumu araştırmak ve bu tür sorunları olup olmadığına dair kütüphaneleri kullanmak biraz zor görünüyor. Herhangi bir öneri?

/fp:fastVisual Studio 2008'in neden açılmış olsa bile hala çalıştığını merak eden var mı? Görünüşe göre Visual Studio 2008 bu problemde g ++ 'dan daha güvenilir mi?


51
Tüm yeni SO kullanıcıları için: BU, bir soruyu nasıl sorduğunuzdur. +1
tenfour

1
FWIW, MinGW kullanarak g ++ 4.5.0 ile doğru çıktıyı alıyorum.
Steve Blackwell

2
ideone 4.3.4 kullanan ideone.com/b8VXg
Daniel A. Beyaz

5
Rutininizin her tür çıktıyla güvenilir bir şekilde çalışmasının pek olası olmadığını aklımda tutmalısınız. Bir ikiye katlamayı tam sayıya yuvarlamanın aksine, bu, tüm gerçek sayıların temsil edilemeyeceği gerçeğine karşı savunmasızdır, bu nedenle bunun gibi daha fazla hata almayı beklemelisiniz.
Jakub Wieczorek

2
Hatayı yeniden oluşturamayanlar için: yorumlanmış hata ayıklama stmts'lerini açıklamayın, sonucu etkilerler.
n. zamirler 'm.

Yanıtlar:


91

Intel x86 işlemcileri dahili olarak 80 bit genişletilmiş hassasiyet kullanır, oysa doublenormalde 64 bit genişliğindedir. Farklı optimizasyon seviyeleri, CPU'daki kayan nokta değerlerinin belleğe ne sıklıkla kaydedildiğini ve böylece 80 bitlik hassasiyetten 64 bit hassasiyete yuvarlanmasını etkiler.

-ffloat-storeFarklı optimizasyon seviyeleriyle aynı kayan nokta sonuçlarını elde etmek için gcc seçeneğini kullanın .

Alternatif olarak, long double80 bit hassasiyetten 64 bit hassasiyete yuvarlamayı önlemek için gcc üzerinde normalde 80 bit genişliğinde olan türü kullanın .

man gcc hepsini söylüyor:

   -ffloat-store
       Do not store floating point variables in registers, and inhibit
       other options that might change whether a floating point value is
       taken from a register or memory.

       This option prevents undesirable excess precision on machines such
       as the 68000 where the floating registers (of the 68881) keep more
       precision than a "double" is supposed to have.  Similarly for the
       x86 architecture.  For most programs, the excess precision does
       only good, but a few programs rely on the precise definition of
       IEEE floating point.  Use -ffloat-store for such programs, after
       modifying them to store all pertinent intermediate computations
       into variables.

X86_64 olarak derleyiciler için SSE kasa kullanma kurar floatve doublehiçbir genişletilmiş hassasiyet kullanılır ve bu sorun oluşmaz, böylece, varsayılan olarak.

gccderleyici seçeneği bunu-mfpmath kontrol eder.


20
Sanırım cevap bu. 4,55 sabiti, 64 bitte en yakın ikili gösterim olan 4,54999999999999'a dönüştürülür; 10 ile çarpın ve tekrar 64 bite yuvarlayın ve 45,5 elde edersiniz. Yuvarlama adımını 80 bitlik bir kayıtta tutarak atlarsanız, 45.4999999999999 ile sonuçlanırsınız.
Mark Ransom

Teşekkürler, bu seçeneği bile bilmiyorum. Ama merak ediyordum, her zaman -float-store seçeneğini açmalı mıyım? Test ettiğim g ++ sürümü CentOS / Redhat 5 ve CentOS / Redhat 6 ile birlikte gönderildiği için birçok programımı bu platformlar altında derledim, bunun programlarımda beklenmedik hatalara neden olacağından endişeliyim.
ayı

5
@Bear, hata ayıklama ifadesi muhtemelen değerin bir kayıttan belleğe aktarılmasına neden olur.
Mark Ransom

2
@Bear, normalde uygulamanız, 64 bitlik bir kaymanın yetersiz veya taşması ve üretmesi beklendiğinde çok küçük veya çok büyük değerlerde çalışmadığı sürece genişletilmiş hassasiyetten yararlanmalıdır inf. İyi bir kural yoktur, birim testleri size kesin bir cevap verebilir.
Maxim Egorushkin

2
@bear Genel bir kural olarak, mükemmel bir şekilde öngörülebilir sonuçlara ihtiyacınız varsa ve / veya bir insanın kağıt üzerinde toplamları yapmasının tam olarak ne olacağıysa, kayan noktadan kaçınmalısınız. -float-store bir öngörülemezlik kaynağını ortadan kaldırır, ancak bu sihirli bir mermi değildir.
plugwash

10

Çıktı şöyle olmalıdır: 4.5 4.6 Sonsuz kesinliğe sahipseniz veya ikili tabanlı kayan nokta gösterimi yerine ondalık tabanlı bir aygıtla çalışıyorsanız çıktı bu olurdu. Ama değilsin. Çoğu bilgisayar, ikili IEEE kayan nokta standardını kullanır.

Maxim Yegorushkin'in cevabında belirttiği gibi, sorunun bir kısmı bilgisayarınızın dahili olarak 80 bitlik bir kayan nokta gösterimi kullanıyor olmasıdır. Yine de bu, sorunun sadece bir kısmı. Sorunun temeli, n.nn5 formunun herhangi bir sayısının tam bir ikili kayan gösterime sahip olmamasıdır. Bu köşe durumları her zaman kesin olmayan sayılardır.

Yuvarlamanızın bu köşe durumlarını güvenilir bir şekilde yuvarlayabilmesini gerçekten istiyorsanız, n.n5, n.nn5 veya n.nnn5 vb. (Ancak n.5 değil) gerçeğini ele alan bir yuvarlama algoritmasına ihtiyacınız vardır. yanlış. Bazı giriş değerlerinin yukarı veya aşağı yuvarlanıp yuvarlanmadığını ve bu köşe durumu ile karşılaştırmaya dayalı olarak yuvarlanmış veya aşağı yuvarlanmış değeri döndürüp döndürmediğini belirleyen köşe durumunu bulun. Ve optimize eden bir derleyicinin, bulunan bu köşeyi genişletilmiş bir hassas kayıt listesine koymamasına dikkat etmeniz gerekir.

Kesin olmadıkları halde Excel'in Kayan sayıları nasıl başarılı bir şekilde Yuvarladığını görün ? böyle bir algoritma için.

Ya da köşedeki kasaların bazen hatalı bir şekilde yuvarlanacağı gerçeğiyle yaşayabilirsiniz.


6

Farklı derleyicilerin farklı en iyileştirme ayarları vardır. Bu daha hızlı optimizasyon ayarlarından bazıları, IEEE 754'e göre katı kayan nokta kurallarını korumaz . Visual Studio belirli bir ayar vardır /fp:strict, /fp:precise, /fp:fast, nerede /fp:fastneler yapılabileceğini üzerinde standardını ihlal ediyor. Bunu bulabilirsiniz bu bayrak tür ortamlarda optimizasyonu kontrol budur. GCC'de davranışı değiştiren benzer bir ayar da bulabilirsiniz.

Bu durumda, derleyiciler arasında farklı olan tek şey, GCC'nin daha yüksek optimizasyonlarda varsayılan olarak en hızlı kayan nokta davranışını arayacağıdır, oysa Visual Studio daha yüksek optimizasyon seviyelerinde kayan nokta davranışını değiştirmez. Bu nedenle, gerçek bir hata olmayabilir, ancak açtığınızı bilmediğiniz bir seçeneğin amaçlanan davranışı olabilir.


4
-ffast-mathGCC için bir anahtar var ve -Oalıntıdan bu yana optimizasyon seviyelerinin hiçbiri tarafından açılmıyor : "matematik fonksiyonları için IEEE veya ISO kurallarının / özelliklerinin tam olarak uygulanmasına bağlı olan programlar için yanlış çıktıya neden olabilir."
Mat

@Mat: Üzerinde -ffast-mathbirkaç şey daha denedim ve g++ 4.4.3hala sorunu yeniden oluşturamıyorum.
NPE

Güzel: ile -ffast-mathben alırım 4.5daha büyük optimizasyon düzeyleri için her iki durumda da 0.
Kerrek SB

: (Düzeltme alıyorum 4.5ile -O1ve -O2ancak birlikte -O0ve -O3GCC 4.4.3 de ancak birlikte -O1,2,3GCC 4.6.1 yılında.)
Kerrek SB

4

Hatayı yeniden oluşturamayanlar için: yorumlanmış hata ayıklama stmts'lerini açıklamayın, sonucu etkilerler.

Bu, sorunun hata ayıklama ifadeleriyle ilgili olduğu anlamına gelir. Ve çıktı ifadeleri sırasında değerlerin kayıtlara yüklenmesinden kaynaklanan bir yuvarlama hatası var gibi görünüyor, bu nedenle diğerleri bunu düzeltebileceğinizi buldu.-ffloat-store

İlave soru:

Merak ediyordum, her zaman -ffloat-storeseçeneği açmalı mıyım?

Flippant olmak gerekirse, bazı programcılar açmak kalmamasıdır bir nedeni olmalı -ffloat-storeaksi seçeneği (aynı şekilde bazı programcılar bir nedeni olmalı var olamazdı, do açmak -ffloat-store). Her zaman açmanızı veya her zaman kapatmanızı önermem. Açmak bazı optimizasyonları engeller, ancak kapatmak, aldığınız davranış türüne izin verir.

Ancak, genel olarak, ikili kayan nokta sayıları (bilgisayarın kullandığı gibi) ile ondalık kayan nokta sayıları (insanların aşina olduğu) arasında bazı uyumsuzluklar vardır ve bu uyumsuzluk, elde ettiğiniz şeye benzer davranışlara neden olabilir (açık olmak gerekirse, Eğer edilir alıyoruz değil bu uyuşmazlığı nedeniyle ancak benzer davranış olabilir ) olmak. Mesele şu ki, kayan nokta ile uğraşırken zaten bazı belirsizlikleriniz olduğundan -ffloat-store, bunun onu daha iyi ya da daha kötü hale getirdiğini söyleyemem .

Bunun yerine, çözmeye çalıştığınız sorunun diğer çözümlerine bakmak isteyebilirsiniz (maalesef, Koenig gerçek kağıda işaret etmiyor ve bunun için gerçekten bariz bir "kanonik" yer bulamıyorum, bu yüzden ben sizi Google'a göndermem gerekecek ).


Çıktı amacıyla yuvarlamıyorsanız, muhtemelen std::modf()(in cmath) ve std::numeric_limits<double>::epsilon()(in limits) değerlerine bakarım . Orijinal round()işlevi düşündüğümde, aramayı std::floor(d + .5)bu işlev için bir çağrı ile değiştirmenin daha temiz olacağına inanıyorum :

// this still has the same problems as the original rounding function
int round_up(double d)
{
    // return value will be coerced to int, and truncated as expected
    // you can then assign the int to a double, if desired
    return d + 0.5;
}

Sanırım bu şu gelişmeyi gösteriyor:

// this won't work for negative d ...
// this may still round some numbers up when they should be rounded down
int round_up(double d)
{
    double floor;
    d = std::modf(d, &floor);
    return floor + (d + .5 + std::numeric_limits<double>::epsilon());
}

Basit bir not: std::numeric_limits<T>::epsilon()"1'e eklenen ve 1'e eşit olmayan bir sayı oluşturan en küçük sayı" olarak tanımlanır. Genellikle göreceli bir epsilon kullanmanız gerekir (yani, "1" dışındaki sayılarla çalıştığınızı hesaba katmak için epsilon'u bir şekilde ölçeklendirin). Toplamı d, .5ve std::numeric_limits<double>::epsilon()böylece ekleme araçları gruplama, 1 yakın olmalıdır std::numeric_limits<double>::epsilon()ne yaptığımızı için doğru boyutu hakkında olacaktır. Bir şey varsa, std::numeric_limits<double>::epsilon()çok büyük olur (üçünün toplamı birden az olduğunda) ve yapmamamız gereken bazı sayıları yuvarlamamıza neden olabilir.


Bugünlerde düşünmelisiniz std::nearbyint().


"Göreli epsilon" 1 ulp (son sırada 1 birim) olarak adlandırılır. x - nextafter(x, INFINITY)x için 1 ulp ile ilgilidir (ama bunu kullanmayın; eminim köşe durumları vardır ve bunu ben uydurdum). Cppreference örneği, epsilon() ULP tabanlı göreceli bir hata elde etmek için ölçeklendirmenin bir örneğine sahiptir .
Peter Cordes

2
BTW, 2016'nın cevabı -ffloat-store: ilk etapta x87 kullanmayın. SSE2 matematiğini (64-bit ikili dosyalar veya -mfpmath=sse -msse2huysuz eski 32-bit ikili dosyalar yapmak için) kullanın, çünkü SSE / SSE2 ekstra hassasiyete sahip olmayan geçicilere sahiptir. doubleve floatXMM kayıtlarındaki değişkenler gerçekten IEEE 64-bit veya 32-bit formatındadır. (Kayıtların her zaman 80 bit olduğu ve belleğe kaydetmenin 32 veya 64 bit'e yuvarlandığı x87'nin aksine.)
Peter Cordes

3

SSE2 içermeyen bir x86 hedefine derliyorsanız, kabul edilen yanıt doğrudur. Tüm modern x86 işlemciler SSE2'yi destekler, bu nedenle bundan yararlanabiliyorsanız şunları yapmalısınız:

-mfpmath=sse -msse2 -ffp-contract=off

Bunu parçalayalım.

-mfpmath=sse -msse2. Bu, her ara sonucu belleğe kaydetmekten çok daha hızlı olan SSE2 kayıtlarını kullanarak yuvarlama gerçekleştirir. Bunun x86-64 için GCC'de zaten varsayılan olduğunu unutmayın . Gönderen GCC wiki :

SSE2'yi destekleyen daha modern x86 işlemcilerinde, derleyici seçeneklerini belirtmek, -mfpmath=sse -msse2tüm kayan ve çift işlemlerin SSE kayıtlarında gerçekleştirilmesini ve doğru şekilde yuvarlanmasını sağlar. Bu seçenekler ABI'yı etkilemez ve bu nedenle mümkün olduğunda öngörülebilir sayısal sonuçlar için kullanılmalıdır.

-ffp-contract=off. Ancak tam bir eşleşme için yuvarlamayı kontrol etmek yeterli değildir. FMA (kaynaştırılmış çarpma ekleme) talimatları, yuvarlama davranışını kaynaşmamış muadillerine göre değiştirebilir, bu yüzden onu devre dışı bırakmamız gerekir. Bu, GCC'de değil, Clang'da varsayılandır. Bu cevapla açıklandığı gibi :

Bir FMA'nın yalnızca bir yuvarlaması vardır (dahili geçici çarpma sonucu için sonsuz hassasiyeti etkin bir şekilde korur), ADD + MUL ise iki yuvarlama içerir.

FMA'yı devre dışı bırakarak, bir miktar performans (ve doğruluk) pahasına, hata ayıklama ve yayınlamayla tam olarak eşleşen sonuçlar elde ederiz. SSE ve AVX'in diğer performans avantajlarından hâlâ yararlanabiliyoruz.


1

Bu sorunu daha çok araştırdım ve daha fazla kesinlik getirebilirim. İlk olarak, x84_64'teki gcc'ye göre 4.45 ve 4.55'in tam temsilleri aşağıdaki gibidir (libquadmath ile son duyarlığı yazdırmak için):

float 32:   4.44999980926513671875
double 64:  4.45000000000000017763568394002504646778106689453125
doublex 80: 4.449999999999999999826527652402319290558807551860809326171875
quad 128:   4.45000000000000000000000000000000015407439555097886824447823540679418548304813185723105561919510364532470703125

float 32:   4.55000019073486328125
double 64:  4.54999999999999982236431605997495353221893310546875
doublex 80: 4.550000000000000000173472347597680709441192448139190673828125
quad 128:   4.54999999999999999999999999999999984592560444902113175552176459320581451695186814276894438080489635467529296875

Olarak Maxim yukarıda bahsedilen sorunun nedeni FPU kayıtlarının 80 bit boyutuna etmektir.

Peki sorun neden Windows'ta hiç oluşmuyor? IA-32'de, x87 FPU, 53 bitlik mantis için dahili bir kesinlik kullanmak üzere yapılandırıldı (toplam 64 bit boyutuna eşdeğer double). Linux ve Mac OS için, 64 bitlik varsayılan kesinlik kullanılmıştır (toplam 80 bit boyutuna eşdeğerdir:) long double. Öyleyse sorun, bu farklı platformlarda FPU'nun kontrol kelimesini değiştirerek mümkün olmalı veya olmamalıdır (talimatların sırasının hatayı tetikleyeceğini varsayarak). Sorun gcc'ye 323 hatası olarak bildirildi (en azından 92 numaralı yorumu okuyun!).

Mantis hassasiyetini Windows'ta göstermek için, bunu VC ++ ile 32 bitte derleyebilirsiniz:

#include "stdafx.h"
#include <stdio.h>  
#include <float.h>  

int main(void)
{
    char t[] = { 64, 53, 24, -1 };
    unsigned int cw = _control87(0, 0);
    printf("mantissa is %d bits\n", t[(cw >> 16) & 3]);
}

ve Linux / Cygwin'de:

#include <stdio.h>

int main(int argc, char **argv)
{
    char t[] = { 24, -1, 53, 64 };
    unsigned int cw = 0;
    __asm__ __volatile__ ("fnstcw %0" : "=m" (*&cw));
    printf("mantissa is %d bits\n", t[(cw >> 8) & 3]);
}

Gcc ile FPU kesinliğini Cygwin'de -mpc32/64/80yok sayılsa da ile ayarlayabileceğinizi unutmayın . Ancak, mantisin boyutunu değiştireceğini, ancak üslü olanı değiştirmeyeceğini ve kapıyı diğer farklı davranış türlerine açacağını unutmayın.

X86_64 mimarisinde SSE, tmandry tarafından söylendiği gibi kullanılır , bu nedenle eski x87 FPU'yu FP hesaplama için zorlamadıkça -mfpmath=387veya 32 bit modunda derlemediğiniz sürece -m32(multilib paketine ihtiyacınız olacaktır) sorun oluşmayacaktır. Linux'ta, farklı bayrak ve gcc sürümleri kombinasyonlarıyla sorunu yeniden oluşturabilirim:

g++-5 -m32 floating.cpp -O1
g++-8 -mfpmath=387 floating.cpp -O1

Windows veya Cygwin'de VC ++ / gcc / tcc ile birkaç kombinasyon denedim ama hata hiç ortaya çıkmadı. Sanırım üretilen talimat dizisi aynı değil.

Son olarak, 4.45 veya 4.55 ile bu sorunu önlemenin egzotik bir yolunun kullanmak olacağını unutmayın _Decimal32/64/128, ancak destek gerçekten az ... Sadece bir printf yapabilmek için çok zaman harcadım libdfp!


0

Kişisel olarak, aynı sorunu diğer yöne doğru da buldum - gcc'den VS'ye. Çoğu durumda, optimizasyondan kaçınmanın daha iyi olduğunu düşünüyorum. Değerli olduğu tek zaman, büyük kayan noktalı veri dizilerini içeren sayısal yöntemlerle uğraşırken. Parçalarına ayırdıktan sonra bile derleyicilerin seçimlerinden sık sık etkileniyorum. Çoğu zaman derleyicinin iç bilgilerini kullanmak veya montajı kendiniz yazmak daha kolaydır.

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.