GCC neden * a * a * a * a * a - (a * a * a) * (a * a * a) 'yı optimize etmiyor?


2120

Bilimsel bir uygulama üzerinde bazı sayısal optimizasyonlar yapıyorum. Fark ettiğim bir şey, GCC'nin çağrıyı pow(a,2)derleyerek optimize edeceğidir a*a, ancak çağrı pow(a,6)optimize edilmemiştir ve aslında kütüphane işlevini çağıracaktır pow, bu da performansı büyük ölçüde yavaşlatır. (Buna karşılık, çalıştırılabilir Intel C ++ Derleyicisiicc kitaplık çağrısını ortadan kaldıracaktır pow(a,6).)

Ne hakkında merak ediyorum ben değiştirilmesinin olmasıdır pow(a,6)ile a*a*a*a*a*aGCC 4.5.1 ve seçenekleri "kullanarak -O3 -lm -funroll-loops -msse4", bu 5 kullanır mulsdtalimatları:

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13

eğer yazarsam (a*a*a)*(a*a*a), üretecek

movapd  %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm14, %xmm13
mulsd   %xmm13, %xmm13

çarpma komutlarının sayısını 3'e düşürür icc.

Derleyiciler bu optimizasyon numarasını neden tanımıyor?


13
"Tanıma gücü (a, 6)" ne anlama geliyor?
Varun Madiath

659
Um ... biliyorsun a a a a a ve (a a a) * (a a * a) kayan nokta sayıları ile aynı değil, değil mi? Bunun için -funsafe-math veya -ffast-math gibi bir şey kullanmanız gerekir.
Damon

106
David Goldberg tarafından "Her Bilgisayar Bilimcisinin Kayan Nokta Aritmetiği Hakkında Bilmesi Gerekenler" bölümünü okumanızı öneririz: download.oracle.com/docs/cd/E19957-01/806-3568/… Az önce yürüdüğün katran ocağı!
Phil Armstrong

189
Mükemmel makul bir soru. 20 yıl önce aynı genel soruyu sordum ve bu dar boğazı ezerek Monte Carlo simülasyonunun yürütme süresini 21 saatten 7 saate düşürdüm. İç döngüdeki kod, işlemde 13 trilyon kez yürütüldü, ancak simülasyonu bir gece penceresine aldı. (aşağıdaki cevaba bakınız)

23
Belki (a*a)*(a*a)*(a*a)de karışıma atın . Aynı sayıda çarpma, ama muhtemelen daha doğrudur.
Rok Kralj

Yanıtlar:


2738

Çünkü Kayan Nokta Matematik İlişkisel değil . İşlenenleri kayan nokta çarpımında gruplama şeklinizin cevabın sayısal doğruluğu üzerinde etkisi vardır.

Sonuç olarak, çoğu derleyici, yanıtın aynı kalacağından emin olmadıkça veya onlara söylemediğiniz sürece sayısal doğrulukla ilgilenmediğiniz sürece kayan nokta hesaplamalarını yeniden düzenlemek konusunda çok muhafazakar olurlar. Örneğin: seçenek reassociate kayan nokta operasyonlarına gcc verir gcc'nin, hatta hız karşı hassasiyeti daha da agresif tradeoffs verir seçeneği.-fassociative-math-ffast-math


10
Evet. -Fast-math ile böyle bir optimizasyon yapıyor. İyi bir fikir! Ancak kodumuz hızdan daha fazla doğrulukla ilgili olduğundan, onu geçmemek daha iyi olabilir.
xis

19
IIRC C99, derleyicinin bu tür "güvensiz" FP optimizasyonlarını yapmasına izin verir, ancak GCC (x87 dışında bir şeyde) IEEE 754'ü takip etmek için makul bir girişimde bulunur - "hata sınırları" değildir; tek bir doğru cevap var .
tc.

14
Uygulama ayrıntıları powne burada ne de orada; bu cevap referans bile vermiyor pow.
Stephen Canon

14
@nedR: ICC varsayılan olarak yeniden ilişkilendirmeye izin verir. Standartlara uygun davranışlar elde etmek istiyorsanız -fp-model precise, ICC ile ayarlamanız gerekir . clangve yeniden ilişkilendirme gcciçin sıkı uyumluluk varsayılanıdır.
Stephen Canon

49
@xis, bu yanlış bir şey değildir -fassociative-math; bu sadece a*a*a*a*a*ave (a*a*a)*(a*a*a)farklı. Bu doğrulukla ilgili değil; standartlara uygunluk ve kesinlikle tekrarlanabilir sonuçlar, örneğin herhangi bir derleyicideki aynı sonuçlar. Kayan nokta sayıları kesin değildir. Derlemek nadiren uygunsuz -fassociative-math.
Paul Draper

652

Lambdageek doğru birleşebilirlik kayan noktalı sayılar için tutmaz çünkü "optimizasyonu" işareta*a*a*a*a*aetmek(a*a*a)*(a*a*a)değerini değiştirebilir. Bu yüzden C99 tarafından izin verilmez (kullanıcı tarafından özellikle izin verilmediği sürece, derleyici bayrağı veya pragma yoluyla). Genel olarak, programcının bir nedenden dolayı yaptıklarını yazdığı ve derleyicinin buna saygı duyması gerektiği varsayımıdır. İsterseniz(a*a*a)*(a*a*a), yazın.

Bu yazmak bir acı olabilir; Neden derleyici kullandığınız zaman [doğru olduğunu düşündüğünüz] doğru şeyi yapamıyor pow(a,6)? Çünkü bu yanlış bir şey olurdu . İyi bir matematik kütüphanesine sahip bir platformda, pow(a,6)her ikisinden de daha doğrua*a*a*a*a*a ya (a*a*a)*(a*a*a). Sadece bazı veriler sağlamak için, Mac Pro'mda küçük bir deneme yaptım ve [1,2) arasındaki tüm tek duyarlıklı kayan sayılar için bir ^ 6 değerini değerlendirmenin en kötü hatasını ölçtüm:

worst relative error using    powf(a, 6.f): 5.96e-08
worst relative error using (a*a*a)*(a*a*a): 2.94e-07
worst relative error using     a*a*a*a*a*a: 2.58e-07

powÇarpma ağacı yerine kullanmak , bir , 4 katına . Derleyiciler, kullanıcı tarafından lisanslanmadığı sürece (örn. Yoluyla -ffast-math) hatayı artıran "optimizasyonlar" yapmamalıdır (ve genellikle yapmamalıdır ).

GCC'nin bir satır içi çarpma ağacı oluşturması gereken __builtin_powi(x,n)bir alternatif sağladığını unutmayın pow( ). Performans için doğruluktan ödün vermek istiyorsanız, ancak hızlı matematiği etkinleştirmek istemiyorsanız bunu kullanın.


29
Ayrıca Visual C ++ 'ın pow ()' gelişmiş 'sürümünü sağlar unutmayın. Arayarak _set_SSE2_enable(<flag>)ile flag=1mümkünse, bu SSE2 kullanacaktır. Bu, doğruluğu biraz azaltır, ancak hızları artırır (bazı durumlarda). MSDN: _set_SSE2_enable () ve pow ()
TkTech

18
@TkTech: Herhangi bir azaltılmış doğruluk, kullanılan kayıtların boyutuna değil, Microsoft'un uygulamasına bağlıdır. Kütüphane yazarı bu kadar motive olmuşsa, yalnızca 32 bit kayıtlar kullanarak doğru yuvarlama yapmak mümkündür pow. Çoğu x87 tabanlı uygulamadan daha doğru olan SSE tabanlı powuygulamalar vardır ve ayrıca hız için bir miktar doğruluk sağlayan uygulamalar da vardır.
Stephen Canon

9
@TkTech: Tabii ki, doğruluktaki azalmanın SSE kullanımına özgü olmayan kütüphane yazarları tarafından yapılan seçimlerden kaynaklandığını açıkça belirtmek istedim.
Stephen Canon

7
Göreceli hataları hesaplamak için burada "altın standart" olarak ne kullandığınızı bilmek istiyorum - normalde olacağını umuyordum a*a*a*a*a*a, ama görünüşe göre durum böyle değil! :)
j_random_hacker

8
@j_random_hacker: Tek duyarlıklı sonuçları karşılaştırdığım için, bir altın standardı için çift duyarlık yeterlidir - a a a a a ve a'nin çiftte hesaplanan hatası, tek duyarlıklı hesaplamaların hatalarından çok daha küçüktür.
Stephen Canon

168

Benzer bir durum: birçok derleyici olmaz optimize etmek a + b + c + diçin (a + b) + (c + d)(yani, belirli bir olarak değerlendirmek ve (bu ikinci salgılama daha ardışık edilebildiğinden bir iyileştirmedir) (((a + b) + c) + d)). Bu da köşe davalarından kaynaklanıyor:

float a = 1e35, b = 1e-5, c = -1e35, d = 1e-5;
printf("%e %e\n", a + b + c + d, (a + b) + (c + d));

Bu çıktılar 1.000000e-05 0.000000e+00


10
Bu tam olarak aynı değil. Çarpma / bölme sırasını değiştirmek (0'a bölme hariç) toplam / çıkarma değişim sırasından daha güvenlidir. Benim düşünceme göre, derleyici mults./divs'i ilişkilendirmeye çalışmalıdır. çünkü bunu yapmak toplam işlem sayısını azaltır ve performans kazancının yanı sıra hassas bir kazanç da sağlar.
CoffeDeveloper

4
@DarioOO: Daha güvenli değil. Çarpma ve bölme, üssün toplanması ve çıkarılması ile aynıdır ve sırayı değiştirmek, geçişlerin kolayca üsün olası aralığını aşmasına neden olabilir. (Tam olarak aynı değil, çünkü üs hassasiyet kaybına uğramıyor ... ama temsil hala oldukça sınırlı ve yeniden sıralama tekrarlanabilir değerlere yol açabilir)
Ben Voigt

8
Sanırım bazı matematik altyapısı eksik. 2 sayıyı çarpma ve bölme aynı miktarda hata verir. Çıkarma / toplama 2 sayıları özellikle 2 sayı büyüklük sırası farklı olduğunda daha büyük bir hata oluşturabilir, bu nedenle son hatada küçük bir değişiklik sağladığı için mul / divide sub / add'e göre daha güvenli bir şekilde yeniden düzenlenir.
CoffeDeveloper

8
@DarioOO: mul / div ile risk farklıdır: Yeniden sıralama, nihai sonuçta ihmal edilebilir bir değişiklik yapar veya üs bir noktada (daha önce olmadığı yerde) taşar ve sonuç büyük ölçüde farklıdır (potansiyel olarak + inf veya 0).
Peter Cordes

@GameDeveloper Öngörülemeyen yollarla hassas bir kazanç sağlamak çok sorunlu.
curiousguy

80

Fortran (bilimsel hesaplama için tasarlanmış) yerleşik bir güç operatörüne sahiptir ve bildiğim kadarıyla Fortran derleyicileri, tamsayı güçlere yükseltmeyi tanımladığınız şekilde benzer şekilde optimize eder. C / C ++ maalesef bir güç operatörü yok, sadece kütüphane fonksiyonu pow(). Bu, akıllı derleyicilerin powözel davranmasını ve özel durumlar için daha hızlı bir şekilde hesaplamasını engellemez , ancak daha az sıklıkta yaptıkları anlaşılıyor ...

Birkaç yıl önce tamsayı güçlerini en uygun şekilde hesaplamayı daha uygun hale getirmeye çalışıyordum ve aşağıdakileri buldum. C ++ değil, C değil ve yine de derleyicinin şeyleri nasıl optimize edeceği / satılacağı konusunda biraz akıllı olmasına bağlı. Her neyse, umarım pratikte faydalı bulabilirsin:

template<unsigned N> struct power_impl;

template<unsigned N> struct power_impl {
    template<typename T>
    static T calc(const T &x) {
        if (N%2 == 0)
            return power_impl<N/2>::calc(x*x);
        else if (N%3 == 0)
            return power_impl<N/3>::calc(x*x*x);
        return power_impl<N-1>::calc(x)*x;
    }
};

template<> struct power_impl<0> {
    template<typename T>
    static T calc(const T &) { return 1; }
};

template<unsigned N, typename T>
inline T power(const T &x) {
    return power_impl<N>::calc(x);
}

Meraklılar için açıklama: Bu, güçleri hesaplamak için en uygun yolu bulmaz, ancak en uygun çözümü bulmak NP-tam bir problemdir ve bu sadece küçük güçler için yapmaya değer (kullanmanın aksine)pow ), karışıklık için bir neden yoktur detay ile.

Sonra sadece power<6>(a) .

Bu, güç ayazmayı kolaylaştırır ( parens ile 6 sn yazmanıza gerek yoktur ) ve telafi toplamı-ffast-math gibi hassas bir şeye sahip olmanız durumunda bu tür bir optimizasyona sahip olmanızı sağlar (işlem sırasının gerekli olduğu bir örnek) .

Muhtemelen bunun C ++ olduğunu unutabilir ve sadece C programında kullanabilirsiniz (eğer bir C ++ derleyicisi ile derlenirse).

Umarım bu yararlı olabilir.

DÜZENLE:

Derleyicimden aldığım şey bu:

İçin a*a*a*a*a*a,

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0

İçin (a*a*a)*(a*a*a),

    movapd  %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm1, %xmm0
    mulsd   %xmm0, %xmm0

İçin power<6>(a),

    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm0, %xmm1

36
En iyi güç ağacını bulmak zor olabilir, ancak sadece küçük güçler için ilginç olduğundan, açık cevap bir kez önceden hesaplamak (Knuth 100'e kadar bir tablo sağlar) ve bu sabit kodlu tabloyu kullanmaktır (gcc'nin powi için dahili olarak yaptığı şey budur) .
Marc Glisse

7
Modern işlemcilerde hız gecikme ile sınırlıdır. Örneğin, çarpma işleminin sonucu beş döngüden sonra elde edilebilir. Bu durumda, biraz güç yaratmanın en hızlı yolunu bulmak daha zor olabilir.
gnasher729

3
Ayrıca, göreceli yuvarlama hatası için en düşük üst sınırı veya en düşük ortalama göreceli yuvarlama hatasını veren güç ağacını bulmayı deneyebilirsiniz.
gnasher729

1
Boost ayrıca bunun için desteğe sahiptir, örneğin boost :: math :: pow <6> (n); Bence ortak faktörleri çıkararak çarpım sayısını azaltmaya çalışıyor.
gast128


62

GCC aslında optimize etmez a*a*a*a*a*aetmek (a*a*a)*(a*a*a)bir tamsayı olduğunda. Bu komutla denedim:

$ echo 'int f(int x) { return x*x*x*x*x*x; }' | gcc -o - -O2 -S -masm=intel -x c -

Gcc bayrakları çok şey var ama hiçbir şey fantezi. Anlamı: Stdin okuma; O2 optimizasyon seviyesini kullanın; ikili yerine çıktı derleme dil listesi; listede Intel montaj dili sözdizimi kullanılmalıdır; giriş C dilinde (genellikle giriş dosya uzantısından dil çıkarılır, ancak stdin'den okurken dosya uzantısı yoktur); ve stdout'a yaz.

İşte çıktının önemli kısmı. Montaj dilinde neler olduğunu gösteren bazı yorumlarla açıklama ekledim:

; x is in edi to begin with.  eax will be used as a temporary register.
mov  eax, edi  ; temp = x
imul eax, edi  ; temp = x * temp
imul eax, edi  ; temp = x * temp
imul eax, eax  ; temp = temp * temp

Ubuntu türevi Linux Mint 16 Petra'da GCC sistemini kullanıyorum. İşte gcc sürümü:

$ gcc --version
gcc (Ubuntu/Linaro 4.8.1-10ubuntu9) 4.8.1

Diğer posterlerin de belirttiği gibi, kayan nokta aritmetiği ilişkili olmadığı için bu seçenek kayan noktada mümkün değildir.


12
Bu, tamsayı çarpımı için yasaldır, çünkü ikinin tamamlayıcı taşması tanımsız davranıştır. Bir taşma olacaksa, yeniden sıralama işlemlerinden bağımsız olarak bir yerde gerçekleşecektir. Bu nedenle, taşma olmayan ifadeler aynı şekilde değerlendirilir, taşma ifadeleri tanımsız davranıştır, bu nedenle derleyicinin taşmanın gerçekleştiği noktayı değiştirmesi uygundur. gcc bunu da yapar unsigned int.
Peter Cordes

51

Çünkü 32 bit kayan nokta sayısı - 1.024 gibi - 1.024 değildir. Bilgisayarda, 1.024 bir aralıktır: (1.024-e) ila (1.024 + e), burada "e" bir hatayı temsil eder. Bazı insanlar bunu fark edemez ve aynı zamanda * a * 'da, rasgele kesinlikteki sayıların bu rakamlara bağlı herhangi bir hata olmadan çoğaltılmasını temsil ettiğine inanır. Bazı insanların bunu gerçekleştirememelerinin nedeni, belki de ilkokullarda kullandıkları matematik hesaplamalarıdır: sadece hatasız ideal sayılarla çalışmak ve çarpma yaparken sadece "e" yi görmezden gelmenin uygun olduğuna inanmak. "Float a = 1.2", "a * a * a" ve benzer C kodlarında "e" örtük olduğunu görmezler.

Programcıların çoğunluğu C ifadesinin a * a * a * a * a * a'nın gerçekten ideal sayılarla çalışmadığı fikrini tanıması (ve yürütebilmesi) durumunda, GCC derleyicisi "a * a * a * a * a * a "içine" t = (a * a); t * t * t "daha az sayıda çarpma gerektirir. Ancak maalesef GCC derleyicisi, kodu yazan programcının "a" nın hatasız veya hatasız bir sayı olduğunu düşünüp düşünmediğini bilmez. Ve böylece GCC sadece kaynak kodunun neye benzediğini yapacak - çünkü GCC "çıplak gözüyle" böyle görüyor.

... ne tür bir programcı olduğunuzu öğrendikten sonra, "-fast-math" anahtarını kullanarak GCC'ye "Hey, GCC, ne yaptığımı biliyorum!" Bu, GCC'nin * a * a * a * a * a'yı farklı bir metne dönüştürmesine izin verecektir - * a * a * a * a * a'dan farklı görünüyor - ancak yine de hata aralığındaki bir sayıyı hesaplıyor a * a * a * a * a * a. Bu tamam, çünkü ideal sayılarla değil, aralıklarla çalıştığınızı zaten biliyorsunuz.


52
Kayan nokta sayıları tamdır. Bunlar tam olarak beklediğiniz gibi değil. Dahası, epsilonlu teknik, gerçekte bir şeylerin nasıl ele alınacağına dair bir yaklaşımdır, çünkü beklenen gerçek hata mantisin ölçeğine görelidir, yani normalde yaklaşık 1 LSB'ye kadar çıkabilirsiniz, ancak bu dikkatli olmamanız durumunda yapılan her işlem kayan nokta ile önemsiz olmayan bir şey yapmadan önce sayısal bir analiste danışın. Mümkünse uygun bir kütüphane kullanın.
Donal Fellows

3
@DonalFellows: IEEE standardı, kayan nokta hesaplamalarının, kaynak işlenenler tam değerler olsaydı sonucun ne olacağıyla en iyi eşleşen sonucu vermesini gerektirir, ancak bu, gerçekte tam değerleri temsil ettikleri anlamına gelmez . Birçok durumda 0.1f'yi (1.677.722 +/- 0.5) / 16.777.216 olarak kabul etmek, bu belirsizliğin ima ettiği ondalık basamak sayısıyla tam miktar olarak (1.677.722 +/-) görüntülenmesi daha yararlıdır. 0.5) / 16.777.216 (24 ondalık basamağa görüntülenmelidir).
supercat

23
@supercat: IEEE-754 kayan nokta veriler bu noktada oldukça açıktır yapmak tam değerleri temsil; Madde 3.2 - 3.4 ilgili bölümlerdir. Elbette, bunları başka türlü yorumlamayı seçebilirsiniz, tıpkı 3 +/- 0.5 int x = 3anlamı olarak yorumlamayı seçebileceğiniz gibi x.
Stephen Canon

7
@supercat: Tamamen katılıyorum, ama bu Distanceonun sayısal değerine tam olarak eşit olmadığı anlamına gelmiyor ; sayısal değerin sadece modellenen bazı fiziksel niceliklere bir yaklaşım olduğu anlamına gelir.
Stephen Canon

10
Sayısal analiz için, kayan nokta sayılarını aralık olarak değil, kesin değerler (tam olarak istediğiniz değerler değildir) olarak yorumladığınızda, beyniniz size teşekkür edecektir. Örneğin, x, 4.5'ten küçük bir hata ile 4.5 etrafında bir yerdeyse ve (x + 1) - x değerini hesaplarsanız, "aralık" yorumlaması 0.8 - 1.2 arasında bir aralık bırakırken "tam değer" yorumlaması sonuç çift kesinlikte en fazla 2 ^ (- 50) hata ile 1 olacaktır.
gnasher729

34

Hiç bir poster kayan ifadelerin daralmasından bahsetmedi (ISO C standardı, 6.5p8 ve 7.12.2). Eğer FP_CONTRACTpragma olarak ayarlanırsa ON, derleyicinin aşağıdaki gibi bir ifadeyi dikkate almasına izin verilir:a*a*a*a*a*a tek bir işlem tam olarak tek bir yuvarlama ile değerlendirilmiş . Örneğin, bir derleyici onu daha hızlı ve daha doğru olan bir dahili güç işleviyle değiştirebilir. Davranış kısmen programcı tarafından doğrudan kaynak kodunda kontrol edildiğinden bu özellikle ilginçtir, ancak son kullanıcı tarafından sağlanan derleyici seçenekleri bazen yanlış kullanılabilir.

FP_CONTRACTPragmanın varsayılan durumu uygulama tanımlıdır, böylece bir derleyicinin bu tür optimizasyonları varsayılan olarak yapmasına izin verilir. Bu nedenle, IEEE 754 kurallarına sıkı sıkıya uyması gereken taşınabilir kod,OFF .

Bir derleyici bu pragmayı desteklemiyorsa, geliştiricinin bunu ayarlamayı seçmesi durumunda, bu tür bir optimizasyondan kaçınarak muhafazakar olmalıdır OFF.

GCC bu pragmayı desteklemez, ancak varsayılan seçeneklerle bunun olduğunu varsayar ON; bu nedenle, donanım FMA'sı olan hedefler için, eğer a*b+cfma (a, b, c) 'ye dönüşümü önlemek istiyorsa, (pragmayı -ffp-contract=offaçıkça ayarlamak için OFF) veya -std=c99( C standart versiyonu, burada C99, bu nedenle yukarıdaki paragrafı izleyin). Geçmişte, ikinci seçenek dönüşümü engellemiyordu, yani GCC bu noktada uymuyordu: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=37845


3
Uzun ömürlü popüler sorular bazen yaşlarını gösterir. Bu soru, GCC'nin yakın zamandaki C99 standardına tam olarak uymadığı için mazur görülebileceği 2011 yılında sorulmuş ve cevaplanmıştır. Tabii şimdi 2014, yani GCC… ahem.
Pascal Cuoq

Bunun yerine, kabul edilmiş bir cevap olmadan nispeten yeni kayan nokta sorularını cevaplamıyor olmalısınız mı? öksürük stackoverflow.com/questions/23703408 öksürük
Pascal Cuoq

Bunu buluyorum ... gcc'nin C99 kayan noktalı pragmaları uygulamadığını rahatsız ediyor.
David Monniaux

1
@DavidMonniaux pragmaları tanımı gereği uygulanması isteğe bağlıdır.
Tim Seguine

2
@TimSeguine Ancak bir pragma uygulanmazsa, varsayılan değerinin uygulama için en kısıtlayıcı olması gerekir. Sanırım David böyle düşünüyordu. GCC ile, eğer bir ISO C modu kullanılıyorsa bu artık FP_CONTRACT için sabittir : hala pragmayı uygulamaz, ancak bir ISO C modunda, şimdi pragmanın kapalı olduğunu varsayar.
vinc17

28

Lambdageek, şamandıra çarpımının ilişkilendirici olmadığını ve daha az doğruluk elde edebileceğini belirttiğinden, daha iyi doğruluk elde ettiğinizde, optimizasyona karşı tartışabilirsiniz, çünkü deterministik bir uygulama istiyorsunuz. Örneğin, her müşterinin aynı dünyayı simüle etmesi gereken oyun simülasyon istemcisi / sunucusunda, kayan nokta hesaplamalarının deterministik olmasını istersiniz.


3
@greggo Hayır, o zaman hala belirleyici. Kelimenin hiçbir anlamında rastgelelik eklenmez.
Alice

9
@Alice Burada Bjorn, farklı platformlarda ve farklı derleyici sürümlerinde vb (programcının kontrolünün ötesinde olabilecek dış değişkenler) aynı sonucu veren kod anlamında 'deterministik' kullanıyor olduğu açıktır - eksikliğin aksine çalışma anında gerçek sayısal rasgelelik. Bunun kelimenin tam anlamıyla kullanılmadığına işaret ediyorsanız, bununla tartışmayacağım.
greggo

5
@greggo Söylediklerini yorumlamanızda bile, bu hala yanlış; platformlardaki çoğu (hepsi olmasa da) işlemler için aynı özellikleri sağlamak için IEEE 754'ün tüm noktası budur. Şimdi, platformlar veya derleyici sürümlerinden bahsetmedi, bu da her uzak sunucu / istemcideki her bir işlemin aynı olmasını istiyorsanız geçerli bir endişe olacaktır .... ama bu onun ifadesinden belli değil. Daha iyi bir kelime "güvenilir bir şekilde benzer" veya başka bir şey olabilir.
Alice

8
@Alice, semantikleri tartışarak, sizin de dahil olmak üzere herkesin zamanını boşa harcıyorsunuz. Anlamı açıktı.
Lanaru

11
@Lanaru Standartların tüm anlamı IS anlambilimidir; anlamı kesin olarak belli değildi.
Alice

28

"Pow" gibi kütüphane fonksiyonları genellikle mümkün olan en düşük hatayı (jenerik durumda) verecek şekilde dikkatlice hazırlanır. Bu genellikle spline işlevlerine yaklaşarak elde edilir (Pascal'ın yorumuna göre en yaygın uygulama Remez algoritması kullanıyor gibi görünüyor )

temel olarak aşağıdaki işlem:

pow(x,y);

herhangi bir tek çarpma veya bölmedeki hatayla yaklaşık aynı büyüklükte doğal bir hataya sahiptir .

Aşağıdaki işlem sırasında:

float a=someValue;
float b=a*a*a*a*a*a;

tek bir çarpma hatasının 5 katından fazla olan doğal bir hata var(5 çarpımı birleştirdiğiniz için) veya bölme .

Derleyici, yaptığı optimizasyon türüne gerçekten dikkat etmelidir:

  1. optimize eğer pow(a,6)için a*a*a*a*a*ao may performansını artırmak, ancak büyük ölçüde kayan nokta sayıları için doğruluğunu azaltabilir.
  2. optimize eğer a*a*a*a*a*a hiçpow(a,6) o aslında doğruluğunu düşürebilir "a" hatası (2'nin kuvvetlerine veya bazı küçük tamsayı sayı) olmadan çarpma verir bazı özel değeri olduğundan
  3. optimize eğer pow(a,6)hiç (a*a*a)*(a*a*a)veya (a*a)*(a*a)*(a*a)hala orada kıyasla bir doğruluk kaybı olabilir powfonksiyonu.

Genel olarak, rastgele kayan nokta değerleri için "pow" nin sonunda yazabileceğiniz herhangi bir fonksiyondan daha iyi bir doğruluğa sahip olduğunu bilirsiniz, ancak bazı özel durumlarda çoklu çarpmaların daha iyi doğruluk ve performansa sahip olabileceği, geliştiricinin daha uygun olanı seçmesine bağlıdır, sonunda kodu yorumlayarak başka hiç kimse bu kodu "optimize edemez".

Mantıklı olan tek şey (kişisel görüş ve görünüşe göre herhangi bir optimizasyon veya derleyici bayrağı olmadan GCC'de bir seçim) "pow (a, 2)" yerine "a * a" koymak olmalıdır. Bir derleyici satıcısının yapması gereken tek aklı başında olan şey bu olurdu.


7
Downvoters bu cevabın mükemmel olduğunu fark etmelidir. Cevabımı desteklemek için düzinelerce kaynak ve belge önerebilirim ve muhtemelen kayan nokta hassasiyeti ile herhangi bir downvoter'dan daha fazla ilgileniyorum. StackOverflow'da, diğer cevapların kapsamadığı eksik bilgileri eklemek son derece makul, bu yüzden kibar olun ve nedenlerinizi açıklayın.
CoffeDeveloper

1
Bana öyle geliyor ki Stephen Canon'un cevabı söyleyeceklerinizi kapsıyor. Libms'in spline ile uygulandığı konusunda ısrar ediyorsunuz: daha tipik olarak argüman azaltmayı (uygulanan işleve bağlı olarak) artı katsayıları Remez algoritmasının az çok karmaşık varyantları tarafından elde edilen tek bir polinom kullanırlar. Kavşak noktalarındaki pürüzsüzlük, libm işlevleri için izlemeye değer bir objektif olarak kabul edilmez (eğer yeterince doğru sonuçlanırlarsa, alanın kaç parçaya bölündüğüne bakılmaksızın otomatik olarak oldukça pürüzsüzdürler).
Pascal Cuoq

Cevabınızın ikinci yarısı, derleyicilerin kaynak kodunun söylediklerini uygulayan kod üretmeleri gerektiği noktasını tamamen kaçırıyor, nokta. Ayrıca, "doğruluk" demek istediğinizde "hassas" kelimesini kullanırsınız.
Pascal Cuoq


27

Bu davanın hiç optimize edilmesini beklemezdim. Bir ifadenin, tüm işlemleri kaldırmak için yeniden gruplandırılabilen alt ifadeler içerdiği yerlerde çok sık olamaz. Derleyici yazarlarının zamanlarını, nadiren karşılaşılan bir uç vakayı kapsamak yerine, gözle görülür iyileşmelerle sonuçlanması muhtemel olan alanlara yatırmasını beklerim.

Diğer ifadelerden bu ifadenin gerçekten uygun derleyici anahtarlarıyla optimize edilebileceğini öğrenmek beni şaşırttı. Ya optimizasyon önemsizdir ya da çok daha yaygın bir optimizasyonun önemli bir örneğidir ya da derleyici yazarları son derece titizdi.

Burada yaptığınız gibi derleyiciye ipuçları vermede yanlış bir şey yok. Mikro optimizasyon sürecinin, hangi farkları getireceklerini görmek için ifadeleri ve ifadeleri yeniden düzenlemek normal ve beklenen bir parçasıdır.

Derleyici tutarsız sonuçlar (uygun anahtarlar olmadan) sunmak için iki ifadeyi dikkate almayı haklı çıkarsa da, bu kısıtlamaya bağlı kalmanıza gerek yoktur. Fark inanılmaz derecede küçük olacak - o kadar ki, fark sizin için önemliyse, ilk etapta standart kayan nokta aritmetiği kullanmamalısınız.


17
Başka bir yorumcunun belirttiği gibi, bu saçma olma noktasına doğru değildir; fark, maliyetin% yarısı ila% 10'u kadar olabilir ve sıkı bir döngüde çalıştırılırsa, önemsiz miktarda ek hassasiyet olabilecek şeyi elde etmek için boşa harcanan birçok talimata dönüşecektir. Monte carlo yaparken standart FP kullanmamanız gerektiğini söylemek, ülke genelinde almak için her zaman bir uçak kullanmanız gerektiğini söylemek gibidir; birçok dışsallığı yok sayar. Son olarak, bu nadir bir optimizasyon DEĞİLDİR; ölü kod analizi ve kod azaltma / yeniden düzenleme çok yaygındır.
Alice

21

Bu soruya zaten birkaç iyi cevap var, ancak eksiksizlik uğruna C standardının uygulanabilir bölümünün 5.1.2.2.3 / 15 (ki bu bölüm 1.9 / 9'daki bölüm ile aynı) olduğunu belirtmek istedim. C ++ 11 standardı). Bu bölüm operatörlerin ancak gerçekten ilişkisel veya değişmeli olmaları durumunda yeniden gruplandırılabileceğini belirtir.


12

gcc aslında bu optimizasyonu kayan nokta sayıları için bile yapabilir. Örneğin,

double foo(double a) {
  return a*a*a*a*a*a;
}

olur

foo(double):
    mulsd   %xmm0, %xmm0
    movapd  %xmm0, %xmm1
    mulsd   %xmm0, %xmm1
    mulsd   %xmm1, %xmm0
    ret

ile -O -funsafe-math-optimizations. Bu yeniden sıralama IEEE-754'ü ihlal ediyor, bu yüzden bayrağı gerektiriyor.

İmzalı tamsayılar, Peter Cordes'in bir yorumda işaret ettiği -funsafe-math-optimizationsgibi, tam olarak taşma olmadığında ve taşma olduğunda tanımsız davranışlar elde ettiğiniz için bu optimizasyonu olmadan yapabilir . Yani sen al

foo(long):
    movq    %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rdi, %rax
    imulq   %rax, %rax
    ret

sadece -O. İmzasız tamsayılar için, mod 2 güçlerini çalıştırdıkları için daha da kolaydır ve böylece taşma karşısında bile serbestçe yeniden sıralanabilirler.


1
Çift, int ve imzasız Godbolt bağlantısı . gcc ve clang her üçünü de aynı şekilde optimize eder (ile -ffast-math)
Peter Cordes

@PeterCordes Teşekkürler!
Charles
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.