Derleyici neden tahmin edilebilir bir toplama döngüsünü çarpmaya optimize edemiyor (veya yapmıyor)?


133

Bu, Mysticial'ın şu soruya verdiği parlak cevabı okurken akla gelen bir sorudur: Neden sıralı bir diziyi sıralanmamış bir diziden daha hızlı işlemek ?

İlgili türler için bağlam:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

Cevabında Intel Compiler'ın (ICC) bunu optimize ettiğini açıklıyor:

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

... buna eşdeğer bir şeye:

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

İyileştirici, bunların eşdeğer olduğunun farkındadır ve bu nedenle döngüleri değiştirerek dalı iç döngünün dışına hareket ettirir. Çok zeki!

Ama bunu neden yapmıyor?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

Umarım Mysticial (veya başka biri) eşit derecede parlak bir cevap verebilir. Diğer soruda tartışılan optimizasyonları daha önce hiç öğrenmemiştim, bu yüzden bunun için gerçekten minnettarım.


14
Bu muhtemelen sadece Intel'in bildiği bir şey. Optimizasyon geçişlerinin hangi sırayla çalıştığını bilmiyorum. Ve görünüşe göre, döngü değişiminden sonra döngü daraltma geçişi çalıştırmıyor.
Gizemli

7
Bu optimizasyon yalnızca veri dizisinde bulunan değerler değişmez ise geçerlidir. Örneğin, bellek her veri okuduğunuzda bir giriş / çıkış cihazına eşlenirse [0] farklı bir değer üretecektir ...
Thomas CG de Vilhena

2
Bu tam sayı mı yoksa kayan nokta mı? Kayan noktaya tekrarlanan toplama, çarpmadan çok farklı sonuçlar verir.
Ben Voigt

6
@Thomas: Veriler volatileolsaydı, döngü değişimi de geçersiz bir optimizasyon olurdu.
Ben Voigt

3
GNAT (GCC 4.6 ile Ada derleyicisi) O3'teki döngüleri değiştirmez, ancak döngüler değiştirilirse, onu çarpmaya dönüştürür.
prosfilaes

Yanıtlar:


105

Derleyici genellikle dönüştüremez

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

içine

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

çünkü ikincisi, birincisinin sahip olmadığı imzalı tam sayıların taşmasına neden olabilir. İmzalı ikinin tümleyen tam sayılarının taşması için garantili sarmalama davranışı olsa bile, sonucu değiştirir ( data[c]30000 ise, ürün -1294967296tipik 32 bit ints için çevrilirken, 100000 kez 30000 eklenir sum, eğer öyleyse sumtaşmaz , 3000000000 artar ). Aynı durumun, farklı sayılara sahip işaretsiz miktarlar için de geçerli olduğunu unutmayın.100000 * data[c] tipik olarak 2^32nihai sonuçta görünmemesi gereken bir indirgeme modülü getirecektir.

Onu dönüştürebilir

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

yine de, eğer her zamanki gibi long long, yeterince büyükseint .

Neden bunu yapmıyor, söyleyemem, sanırım Mysticial'ın söylediği buydu , "görünüşe göre, döngü-değişiminden sonra döngü çöküşü yapmıyor".

Döngü değişiminin kendisinin genellikle geçerli olmadığını unutmayın (işaretli tamsayılar için), çünkü

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

nerede taşmaya neden olabilir

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

olmaz. Burada koşer, çünkü şart her şeyi sağlıyordata[c] , eklenenlerin hepsinin aynı işarete sahip , yani biri taşarsa her ikisi de olur.

Derleyicinin bunu hesaba kattığından pek emin değilim (@Mysticial, data[c] & 0x80pozitif ve negatif değerler için doğru olabilecek veya gibi bir koşulla deneyebilir misin ?). Derleyicilerimin geçersiz optimizasyonlar yapmasını sağladım (örneğin, birkaç yıl önce, bir ICC (11.0, iirc) ' nin an 1.0/nolduğu yerde işaretli-32-bit-int-çift dönüşümü kullandım. Gcc'lerden yaklaşık iki kat daha hızlıydı çıktı. Ama yanlış, pek çok değer , oops'tan daha büyüktü .).nunsigned int2^31


4
MPW derleyicisinin 32K'dan daha büyük yığın çerçevelerine izin veren bir seçenek ekleyen bir sürümünü hatırlıyorum [önceki sürümler yerel değişkenler için @ A7 + int16 adresleme kullanılarak sınırlandırılmıştı]. 32K'nın altındaki veya 64K'nın üzerindeki yığın çerçeveler için her şeyi doğru yaptı, ancak 40K yığın çerçevesi için ADD.W A6,$A000, adres kayıtları ile kelime işlemlerinin eklemeden önce kelimeyi 32 bite genişlettiğini unutarak kullanacaktır . Sorun gidermek biraz zaman aldı, çünkü kodun ADDbununla A6'yı yığından
fırlattığı bir

3
... ve arayanın ilgilendiği tek kayıt, statik bir dizinin [yükleme zamanı sabiti] adresiydi. Derleyici, dizinin adresinin bir kayıt defterine kaydedildiğini biliyordu, böylece buna göre optimize edebiliyordu, ancak hata ayıklayıcı basitçe bir sabitin adresini biliyordu. Böylece, bir ifadeden önce MyArray[0] = 4;adresinin adresini kontrol edebilir MyArrayve ifadenin çalıştırılmasından önce ve sonra o konuma bakabilirdim; değişmezdi. Kod buna benzer bir şeydi move.B @A3,#4ve A3'ün MyArrayher zaman talimatın uygulandığı herhangi bir zamanı göstermesi gerekiyordu , ama olmadı. Eğlence.
supercat

o zaman clang neden bu tür bir optimizasyonu gerçekleştirir?
Jason S

Derleyici, bu yeniden yazmayı kendi iç ara temsillerinde gerçekleştirebilir, çünkü dahili ara gösterimlerinde daha az tanımsız davranışa sahip olmasına izin verilir.
user253751

48

Bu cevap, bağlantılı özel durum için geçerli değildir, ancak soru başlığı için geçerlidir ve gelecekteki okuyucular için ilginç olabilir:

Sonlu kesinlik nedeniyle, tekrarlanan kayan nokta toplama, çarpmaya eşdeğer değildir . Düşünmek:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

gösteri


10
Bu, sorulan sorunun cevabı değil. İlginç bilgilere rağmen (ve herhangi bir C / C ++ programcısı için bilinmesi gereken), bu forum değil ve buraya ait değil.
orlp

30
@nightcracker: StackOverflow'un belirtilen amacı, gelecekteki kullanıcılar için yararlı, aranabilir bir yanıt kitaplığı oluşturmaktır. Ve bu sorulan sorunun cevabı ... Bu cevabı orijinal poster için geçerli kılan bazı açıklanmamış bilgiler var. Yine de aynı soruyla başkaları için geçerli olabilir.
Ben Voigt

12
Bu var olabilir bir soru için cevap başlığında hayır, ama bir soru.
orlp

7
Dediğim gibi ilginç bir bilgi. Yine de hala önerecekleri Nota bana yanlış görünüyor üst cevabı sorunun bu haliyle artık, soruya cevap vermez . Intel Derleyicisinin optimize etmemeye karar vermesinin nedeni bu değil, basta.
orlp

4
@nightcracker: Bunun en iyi cevap olması bana da yanlış geliyor. Birisinin, puan açısından bunu aşan tam sayı durumu için gerçekten iyi bir cevap göndermesini umuyorum. Maalesef, tamsayı durumunda "yapamam" için bir cevap olduğunu düşünmüyorum, çünkü dönüşüm yasal olacaktır, bu yüzden "neden olmasın" ile baş başa kalıyoruz, bu da aslında " çok yerelleştirilmiş "yakın neden, çünkü belirli bir derleyici sürümüne özgüdür. Cevapladığım soru en önemlisi, IMO.
Ben Voigt

6

Derleyici, optimizasyonu yapan çeşitli geçişler içerir. Genellikle her geçişte ya ifadeler üzerinde bir optimizasyon ya da döngü optimizasyonları yapılır. Şu anda, döngü başlıklarına dayalı olarak döngü gövdesinin optimizasyonunu yapan bir model yoktur. Bunu tespit etmek zordur ve daha az yaygındır.

Yapılan optimizasyon döngü değişmez kod hareketiydi. Bu, bir dizi teknik kullanılarak yapılabilir.


4

Tamsayı Aritmetik'ten bahsettiğimizi varsayarak, bazı derleyicilerin bu tür bir optimizasyon yapabileceğini tahmin ediyorum.

Aynı zamanda, bazı derleyiciler bunu yapmayı reddedebilir çünkü tekrarlayan toplamayı çarpma ile değiştirmek kodun taşma davranışını değiştirebilir. İşaretsiz tamsayı türleri için, taşma davranışları dil tarafından tam olarak belirlendiğinden bir fark yaratmamalıdır. Ancak imzalı olanlar için (muhtemelen 2'nin tamamlayıcı platformunda değil) olabilir. İşaretli taşmanın aslında C'de tanımsız davranışa yol açtığı doğrudur, yani bu taşma anlamını tamamen göz ardı etmenin tamamen tamam olması gerekir, ancak tüm derleyiciler bunu yapacak kadar cesur değildir. Genellikle "C sadece üst düzey bir montaj dili" kalabalığından çok fazla eleştiri alır. (GCC, kesin-aliasing semantiğine dayalı optimizasyonları başlattığında ne olduğunu hatırlıyor musunuz?)

Tarihsel olarak, GCC kendisini bu kadar sert adımlar atmak için gerekenlere sahip bir derleyici olarak göstermiştir, ancak diğer derleyiciler, dil tarafından tanımlanmamış olsa bile algılanan "kullanıcı-amaçlı" davranışa bağlı kalmayı tercih edebilir.


Yanlışlıkla tanımlanmamış davranışa bağlı olup olmadığımı bilmeyi tercih ederim, ancak sanırım derleyicinin, taşma bir çalışma zamanı sorunu olacağından bilmesinin bir yolu yok: /
jhabbott

2
@jhabbott: IFF taşma meydana ardından tanımsız davranış yoktur. Davranışın tanımlanıp tanımlanmadığı çalışma zamanına kadar bilinmemektedir (sayıların çalışma zamanında girildiği varsayılarak): P.
orlp

3

Şimdi yapar - en azından clang şunları yapar :

long long add_100k_signed(int *data, int arraySize)
{
    long long sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

-O1 ile derlenir

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        movsxd  rdx, dword ptr [rdi + 4*rsi]
        imul    rcx, rdx, 100000
        cmp     rdx, 127
        cmovle  rcx, r8
        add     rax, rcx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

Tamsayı taşmasının bununla hiçbir ilgisi yoktur; tanımsız davranışa neden olan tamsayı taşması varsa, bu her iki durumda da olabilir. İşte bunun yerine kullanılan aynı türden bir işlevintlong :

int add_100k_signed(int *data, int arraySize)
{
    int sum = 0;

    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            for (int i = 0; i < 100000; ++i)
                sum += data[c];
    return sum;
}

-O1 ile derlenir

add_100k_signed:                        # @add_100k_signed
        test    esi, esi
        jle     .LBB0_1
        mov     r9d, esi
        xor     r8d, r8d
        xor     esi, esi
        xor     eax, eax
.LBB0_4:                                # =>This Inner Loop Header: Depth=1
        mov     edx, dword ptr [rdi + 4*rsi]
        imul    ecx, edx, 100000
        cmp     edx, 127
        cmovle  ecx, r8d
        add     eax, ecx
        add     rsi, 1
        cmp     r9, rsi
        jne     .LBB0_4
        ret
.LBB0_1:
        xor     eax, eax
        ret

2

Bu tür bir optimizasyonun önünde kavramsal bir engel var. Derleyici yazarları, kuvvetin azaltılması için çok çaba harcarlar - örneğin, çarpımları toplama ve kaydırmalarla değiştirmek. Çarpmanın kötü olduğunu düşünmeye alışırlar. Yani birinin diğer tarafa gitmesi gereken bir durum şaşırtıcı ve mantık dışıdır. Yani kimse uygulamayı düşünmüyor.


3
Bir döngüyü kapalı form hesaplamayla değiştirmek aynı zamanda güç azalmasıdır, değil mi?
Ben Voigt

Resmen, evet, sanırım, ama hiç kimsenin bu şekilde konuştuğunu duymadım. (Gerçi literatür konusunda biraz güncel değilim.)
zwol

1

Derleyicileri geliştiren ve sürdüren kişilerin çalışmalarına harcayacakları sınırlı bir zaman ve enerji vardır, bu nedenle genellikle kullanıcılarının en çok önem verdiği şeye odaklanmak isterler: iyi yazılmış kodu hızlı koda dönüştürmek. Saçma bir kodu hızlı koda dönüştürmenin yollarını bulmaya çalışmak istemiyorlar - kod incelemesi bunun için. Yüksek seviyeli bir dilde, önemli bir fikri ifade eden "aptalca" bir kod olabilir, bu da geliştiricilerin bunu hızlı hale getirme zamanına değer kılar - örneğin, kısa yol ormansızlaştırma ve akış füzyonu Haskell programlarının tembel olarak belirli türler etrafında yapılandırılmasına izin verir bellek ayırmayan sıkı döngüler halinde derlenecek veri yapıları üretti. Ancak bu tür bir teşvik, ilmekli toplamayı çarpmaya dönüştürmek için geçerli değildir. Hızlı olmasını istiyorsan,

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.