GCC neden bu döngüde std :: vector :: boyutunun değişmeyeceğini varsayamıyor?


14

if (i < input.size() - 1) print(0);Bu döngüde optimize edilecek bir iş arkadaşına iddia ettim, böylece input.size()her yinelemede okunmaz, ancak durum böyle değil!

void print(int x) {
    std::cout << x << std::endl;
}

void print_list(const std::vector<int>& input) {
    int i = 0;
    for (size_t i = 0; i < input.size(); i++) {
        print(input[i]);
        if (i < input.size() - 1) print(0);
    }
}

Gcc seçeneklerine sahip Derleyici Gezgini'ne göre -O3 -fno-exceptionsaslında input.size()her bir yinelemeyi okuyor ve leaçıkarma yapmak için kullanıyoruz !

        movq    0(%rbp), %rdx
        movq    8(%rbp), %rax
        subq    %rdx, %rax
        sarq    $2, %rax
        leaq    -1(%rax), %rcx
        cmpq    %rbx, %rcx
        ja      .L35
        addq    $1, %rbx

İlginçtir, Rust'ta bu optimizasyon gerçekleşir. Her yinelemede azaltılmış ibir değişkenle değiştirilmiş gibi görünüyor jve test i < input.size() - 1gibi bir şeyle değiştiriliyor j > 0.

fn print(x: i32) {
    println!("{}", x);
}

pub fn print_list(xs: &Vec<i32>) {
    for (i, x) in xs.iter().enumerate() {
        print(*x);
        if i < xs.len() - 1 {
            print(0);
        }
    }
}

In Derleyici Explorer ilgili montaj şuna benzer:

        cmpq    %r12, %rbx
        jae     .LBB0_4

Kontrol ettim ve ben eminim r12olduğunu xs.len() - 1ve rbxsayaç olduğunu. Daha önce bir addfor rbxve bir movdış döngü vardır r12.

Bu neden? Görünüşe göre GCC satır içi yapabiliyorsa size()ve operator[]olduğu gibi, bunun size()değişmediğini de bilmeli . Ama belki GCC'nin optimize edici, onu bir değişkene çekmeye değmeyeceğine karar verir? Ya da belki de bunu güvensiz hale getirecek başka bir olası yan etkisi vardır - kimse biliyor mu?


1
Ayrıca printlnmuhtemelen karmaşık bir yöntemdir, derleyici printlnvektörü değiştirmediğini kanıtlamakta sorun yaşayabilir .
Mooing Duck

1
@MooingDuck: Başka bir iş parçacığı veri yarışı UB olacaktır. Derleyiciler can ve Varsayalım ki bu olmuyor. Buradaki sorun, satıriçi olmayan fonksiyon çağrısıdır cout.operator<<(). Derleyici, bu kara kutu işlevinin std::vectorbir genel öğeden bir başvuru almadığını bilmiyor .
Peter Cordes

@PeterCordes: Diğer iş parçacıklarının bağımsız bir açıklama olmadığı ve karmaşıklığının printlnya operator<<da anahtar olduğu konusunda haklısınız .
Mooing Duck

Derleyici bu harici yöntemlerin anlambilimini bilmiyor.
user207421

Yanıtlar:


10

Satır içi olmayan işlev çağrısı cout.operator<<(int), iyileştirici için kara bir kutudur (çünkü kütüphane sadece C ++ ile yazılmıştır ve tüm iyileştirici bir prototiptir; yorumlarda tartışmaya bakın). Küresel bir değişkenin işaret edebileceği herhangi bir hafızanın değiştirildiğini varsaymak zorundadır.

(Ya da std::endlçağrı. BTW, neden sadece a basmak yerine o noktadan bir sifonu zorla '\n'?)

Örneğin , bildiği her şey için, std::vector<int> &inputbir global değişkene bir referanstır ve bu işlev çağrılarından biri, global var . (Ya da bir vector<int> *ptryerde bir küresel var, ya da static vector<int>başka bir derleme birimindeki bir işaretçiyi döndüren bir işlev ya da bir işlev bizim tarafımızdan bir referans geçirilmeden bu vektöre bir referans alabilir.

Eğer Adresinde alınmış hiç bir yerel değişken olsaydı, derleyici olabilir olmayan satır içi işlev çağrıları bunu mutasyona olamayacağını varsayalım. Çünkü herhangi bir global değişkenin bu nesneye bir işaretçi tutması mümkün olmazdı. ( Buna Kaçış Analizi denir ). Bu nedenle derleyici size_t iişlev çağrıları boyunca bir kayıtta tutabilir . ( int igölgelendirildiği size_t ive başka şekilde kullanılmadığı için optimize edilebilir ).

Aynı işlemi bir yerel ile de yapabilir vector(yani, taban, bitiş_boyutu ve bitiş_kapaklığı işaretçileri için.)

ISO C99 bu soruna bir çözüm vardır: int *restrict foo. Birçok C ++ derlenir destek int *__restrict foobellek tarafından işaret söz için fooolan sadece bu işaretçi aracılığıyla erişilebilir. En çok 2 diziyi alan işlevlerde kullanışlıdır ve derleyicinin çakışmayacağına dair söz vermek istersiniz. Böylece, bunu kontrol etmek ve bir geri dönüş döngüsü çalıştırmak için kod oluşturmadan otomatik vektörleştirebilir.

OP yorumları:

Rust'ta değiştirilemez bir referans, başka hiç kimsenin referansınız olan değeri mutasyona uğratmadığına dair küresel bir garantidir (C ++ 'a eşdeğer restrict)

Bu Rust'un neden bu optimizasyonu yapabileceğini ama C ++ yapamayacağını açıklıyor.


C ++ cihazınızı optimize etme

Açıkçası auto size = input.size();, fonksiyonunuzun üst kısmında bir kez kullanmalısınız , böylece derleyici bunun bir döngü değişmez olduğunu bilir. C ++ uygulamaları sizin için bu sorunu çözmez, bu yüzden kendiniz yapmanız gerekir.

Ayrıca const int *data = input.data();veri işaretçisi yüklerini de std::vector<int>"kontrol bloğundan" kaldırmanız gerekebilir . Optimizasyonun çok deyimsel olmayan kaynak değişiklikleri gerektirmesi talihsiz bir durumdur.

Rust, derleyici geliştiricilerinin derleyiciler için pratikte neyin mümkün olduğunu öğrendikten sonra tasarlanan çok daha modern bir dildir. CPU'ların yapabileceği bazı harika şeyleri portatif olarak açığa çıkarmak i32.count_ones, döndürmek, bit taraması vb. Dahil olmak üzere gerçekten başka şekillerde de gösterir std::bitset::count().


1
Vektör kod değerine göre alınırsa OP kodu hala teste tabi tutulur. Bu nedenle GCC bu durumda optimize edebilse de, bunu yapmaz.
Ceviz

1
Standart, operator<<bu işlenen türlerinin davranışını tanımlar ; yani Standart C ++ 'da bir kara kutu değildir ve derleyici belgelerin söylediklerini yaptığını varsayabilir. Belki de standart olmayan davranışlar ekleyerek kütüphane geliştiricilerini desteklemek istiyorlar ...
MM

2
Optimize edici, standardın zorunlu kıldığı davranışı besleyebilir, benim açımdan, bu optimizasyonun standart tarafından izin verildiği, ancak derleyici satıcısı bu optimizasyonu tarif ettiğiniz ve bu yöntemden vazgeçtiğiniz şekilde uygulamayı seçti
MM

2
@MM Rastgele nesne demedi, ben bir uygulama tanımlı vektör dedim. Standartta, bir uygulamanın, operatörün << değiştirdiği bir uygulama tanımlı vektöre sahip olmasını ve bu vektörün bir uygulama tanımlı şekilde erişmesine izin vermesini yasaklayan hiçbir şey yoktur. cout, kullanıcı tanımlı bir sınıftan bir nesnenin streambufakış kullanılarak ilişkilendirilmesine izin verir cout.rdbuf. Benzer şekilde türetilmiş bir nesne ostreamile ilişkilendirilebilir cout.tie.
Ross Ridge

2
@PeterCordes - Yerel vektörler hakkında o kadar emin olmazdım: herhangi bir üye işlevi sıra dışına çıkar çıkmaz, yerel olarak etkin bir şekilde kaçtı çünkü thisişaretçi dolaylı olarak geçti. Bu uygulamada kurucu kadar erken bir zamanda gerçekleşebilir. Bu basit döngüyü düşünün - Ben sadece gcc ana döngüsünü kontrol ettim (- ' L34:dan jne L34), ama kesinlikle vektör üyeleri kaçmış gibi davranıyor (her bir yinelemeden bellekten yüklüyor).
BeeOnRope
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.