C ve C ++ 'da neredeyse aynı kod arasındaki yürütme süresindeki büyük fark (x9)


85

Bu alıştırmayı www.spoj.com adresinden çözmeye çalışıyordum: FCTRL - Faktör

Gerçekten okumak zorunda değilsin, merak ediyorsan yap yeter :)

İlk önce C ++ 'da uyguladım (işte benim çözümüm):

#include <iostream>
using namespace std;

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library’s stdio buffers (from https://stackoverflow.com/a/22225421/5218277)

    cin >> num_of_inputs;

    while (num_of_inputs--)
    {
        cin >> fact_num;

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        cout << num_of_trailing_zeros << "\n";
    }

    return 0;
}

G ++ 5.1 için çözüm olarak yükledim

Sonuç şuydu: Time 0.18 Mem 3.3M C ++ yürütme sonuçları

Ama sonra zaman infazlarının 0.1'den az olduğunu iddia eden bazı yorumlar gördüm. Daha hızlı algoritma düşünemediğim için aynı kodu C'de uygulamaya çalıştım :

#include <stdio.h>

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    scanf("%d", &num_of_inputs);

    while (num_of_inputs--)
    {
        scanf("%d", &fact_num);

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        printf("%d", num_of_trailing_zeros);
        printf("%s","\n");
    }

    return 0;
}

Gcc 5.1 için çözüm olarak yükledim

Bu sefer sonuç şöyleydi: Time 0.02 Mem 2.1M C yürütme sonuçları

Şimdi kod hemen hemen aynı , buradastd::ios_base::sync_with_stdio(false); önerildiği gibi C ++ koduna C kütüphanesinin stdio tamponlarıyla senkronizasyonu kapatmak için ekledim . Ben de bölmek için çifte çağrı telafi etmek içinde .printf("%d\n", num_of_trailing_zeros);printf("%d", num_of_trailing_zeros); printf("%s","\n");operator<<cout << num_of_trailing_zeros << "\n";

Ama yine de C ++ koduna kıyasla x9 daha iyi performans ve daha düşük bellek kullanımı gördüm .

Neden?

DÜZENLE

I sabit unsigned longiçin unsigned intC kodu. Olmalıydı unsigned intve yukarıda gösterilen sonuçlar yeni ( unsigned int) sürümle ilgilidir.


31
C ++ akışları tasarım gereği son derece yavaştır. Çünkü yavaş ve istikrarlı olan yarışı kazanır. : P (
alevlenmeden

7
Yavaşlık güvenlik veya uyumluluktan gelmez. Tüm akış bayraklarıyla fazlasıyla tasarlanmış.
Karoly Horvath

8
@AlexLop. std::ostringstreamÇıktıyı biriktirmek için a kullanmak ve sonunda std::cout hepsini tek seferde göndermek , zamanı 0,02'ye düşürür. std::coutBir döngüde kullanmak çevrelerinde daha yavaştır ve bunu iyileştirmenin basit bir yolu olduğunu düşünmüyorum.
Blastfurnace

6
Bu zamanlamaların ideon kullanılarak elde edilmiş olmasından başka kimse ilgilenmiyor mu?
ildjarn

6
@Olaf: Korkarım katılmıyorum, bu tür sorular seçilen tüm etiketler için çok fazla konuya bağlı. C ve C ++ genel olarak yeterince yakındır ki, performanstaki bu tür bir fark bir açıklama gerektirir. Bulduğumuza sevindim. Belki de sonuç olarak GNU libc ++ geliştirilmelidir.
chqrlie

Yanıtlar:


56

Her iki program da tamamen aynı şeyi yapıyor. Aynı kesin algoritmayı kullanırlar ve karmaşıklığı düşük olduğu için performansları çoğunlukla girdi ve çıktı işlemenin verimliliğine bağlıdır.

girdiyi scanf("%d", &fact_num);bir tarafta ve cin >> fact_num;diğer tarafta taramak, her iki şekilde de çok maliyetli görünmüyor. Aslında, dönüştürme türü derleme zamanında bilindiğinden ve doğru ayrıştırıcı doğrudan C ++ derleyicisi tarafından çağrılabildiğinden, C ++ 'da daha az maliyetli olmalıdır. Aynı durum çıktı için de geçerlidir. Hatta için ayrı bir çağrı bile yazarsınız printf("%s","\n");, ancak C derleyicisi bunu bir çağrı olarak derlemek için yeterince iyidir putchar('\n');.

Dolayısıyla, hem G / Ç'nin hem de hesaplamanın karmaşıklığına bakıldığında, C ++ sürümü C sürümünden daha hızlı olmalıdır.

Arabelleğe alınmasını tamamen devre dışı bırakmak, stdoutC uygulamasını C ++ sürümünden daha yavaş bir şeye yavaşlatır. AlexLop tarafından sondan fflush(stdout);sonra yapılan başka bir test printf, C ++ sürümüyle benzer performans sağlar. Arabelleğe almayı tamamen devre dışı bırakmak kadar yavaş değildir çünkü çıktı sisteme bir seferde bir bayt yerine küçük parçalar halinde yazılır.

Bu, C ++ kitaplığınızdaki belirli bir davranışa işaret ediyor gibi görünüyor: Sisteminizin cin ve coutçıktıyı coutgirdi talep edildiğinde temizliyor cin. Bazı C kitaplıkları da bunu yapar, ancak genellikle yalnızca terminalden okurken / yazarken. Www.spoj.com sitesi tarafından yapılan kıyaslama, muhtemelen girdi ve çıktıları dosyalara ve dosyalardan yönlendiriyor.

AlexLop başka bir test daha yaptı: bir vektördeki tüm girdileri aynı anda okumak ve ardından tüm çıktıları hesaplamak ve yazmak, C ++ sürümünün neden bu kadar yavaş olduğunu anlamaya yardımcı olur. Performansı C sürümüne yükseltir, bu benim düşüncemi kanıtlar ve C ++ biçimlendirme kodundaki şüpheyi ortadan kaldırır.

Blastfurnace tarafından yapılan başka bir test, tüm çıktıları bir std::ostringstream ve bunu sonunda tek bir patlamada yıkamak, C ++ performansını temel C sürümününkine . QED.

Giriş cinve çıkışı taramalıcout , çok verimsiz bir G / Ç işlemesine neden olur ve akış tamponlama şemasını bozar. performansı 10 kat düşürür.

Not: Algoritmanız yanlıştır, fact_num >= UINT_MAX / 5çünkü fives *= 5oluşmadan önce taşacak ve etrafı saracaktır > fact_num. Bunu, bu türlerden birinin daha büyük olması durumunda fivesbir unsigned longveya bir yaparak düzeltebilirsiniz . Biçim olarak da kullanın . Www.spoj.com'daki adamların kıyaslamalarında çok katı olmadığı için şanslısınız.unsigned long longunsigned int%uscanf

DÜZENLEME: Daha sonra vitaux tarafından açıklandığı gibi, bu davranış gerçekten C ++ standardı tarafından zorunlu kılınmıştır. varsayılan cinolarak bağlıdır cout. cinGiriş tamponunun yeniden doldurulması gereken bir giriş işlemi, coutbekleyen çıktının temizlenmesine neden olacaktır . OP'nin uygulamasında, sistematik bir şekilde yıkanıyor cingibi görünüyor cout, bu biraz fazla ve gözle görülür şekilde verimsiz.

Ilya Popov bunun için basit bir çözüm sundu: cinşunlara coutek olarak başka bir büyülü büyü yaparak çözülebilir std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

Ayrıca, böyle bir zorunlu yıkamanın bir satır sonu oluşturmak std::endlyerine kullanıldığında da meydana geldiğini unutmayın . Çıktı satırını daha C ++ deyimsel ve masum görünüme değiştirmek , performansı aynı şekilde düşürür.'\n'coutcout << num_of_trailing_zeros << endl;


2
Muhtemelen akış temizliği konusunda haklısınız. Çıktının std::ostringstreama'da toplanması ve sonunda bir kez çıkarılması, zamanı C sürümüyle eşitliğe indirir.
Blastfurnace

2
@ DavidC.Rankin: Bir varsayıma giriştim (cout cin okuduktan sonra kızardı), bunu kanıtlamanın bir yolunu buldum, AlexLop bunu uyguladı ve ikna edici kanıtlar veriyor, ancak Blastfurnace benim düşüncemi ve testlerini kanıtlamak için farklı bir yol buldu eşit derecede ikna edici kanıtlar verin. Kanıt olarak alıyorum, ancak elbette tamamen resmi bir kanıt değil, C ++ kaynak koduna bakıldığında olabilir.
chqrlie

2
ostringstreamÇıktı için kullanmayı denedim ve Zaman 0.02 QED verdi :). fact_num >= UINT_MAX / 5İYİ noktaya gelince !
Alex Lop.

1
Tüm girdileri a'da toplamak vectorve ardından hesaplamaları (olmadan ostringstream) işlemek aynı sonucu verir! Zaman 0.02. İkisini birleştirir vectorve ostringstreamdaha fazla geliştirmez. Aynı Zaman 0.02
Alex Lop.

2
Bu durumda bile çalışan daha basit bir düzeltme sizeof(int) == sizeof(long long): maksimum değere ulaşıp ulaşmadığını num_of_trailing_zeros += fact_num/fives;kontrol etmek için döngünün gövdesine bir test ekleyin fives:if (fives > UINT_MAX / 5) break;
chqrlie

44

Yapmak için bir püf iostreamler daha hızlı hem kullandığınızda cinve coutçağrı etmektir

cin.tie(nullptr);

Varsayılan olarak, herhangi bir şey girdiğinizde cintemizlenir cout. Aralıklı giriş ve çıkış yaparsanız performansa önemli ölçüde zarar verebilir. Bu, bazı komut istemlerini gösterdiğiniz ve ardından verileri beklediğiniz komut satırı arayüzü kullanımları için yapılır:

std::string name;
cout << "Enter your name:";
cin >> name;

Bu durumda, girişi beklemeye başlamadan önce komut isteminin gerçekten gösterildiğinden emin olmak istersiniz. Üstteki çizgi ile bu bağı kırarsınız cinve coutbağımsız olursunuz .

C ++ 11'den bu yana, iostreams ile daha iyi performans elde etmenin bir yolu da şunun gibi std::getlinebirlikte kullanmaktır std::stoi:

std::string line;
for (int i = 0; i < n && std::getline(std::cin, line); ++i)
{
    int x = std::stoi(line);
}

Bu yol performansta C-stiline yaklaşabilir veya hatta aşabilir scanf. Kullanılması getcharve özelliklegetchar_unlocked el yazısıyla yazılan ayrıştırmayla birlikte, yine de daha iyi performans sağlar.

PS. Çevrimiçi yargıçlar için yararlı olan, C ++ 'da sayı girmenin birkaç yolunu karşılaştıran bir gönderi yazdım , ancak bu yalnızca Rusça, üzgünüm. Bununla birlikte, kod örnekleri ve son tablo anlaşılır olmalıdır.


1
Çözümü için açıklama ve +1 için teşekkür ederiz, ancak önerilen alternatif std::readlineve std::stoifonksiyonel OP koduna eşdeğer değildir. Hem cin >> x;ve hem de scanf("%f", &x);beyaz boşlukları ayırıcı olarak kabul ederseniz, aynı satırda birden fazla sayı olabilir.
chqrlie

27

Sorun şu ki, cppreference'den alıntı yaparak :

std :: cin'den, çıktıdan std :: cerrah'a herhangi bir girdi veya program sonlandırma std :: cout.flush () çağrısını zorlar

Bunu test etmek kolaydır: değiştirirseniz

cin >> fact_num;

ile

scanf("%d", &fact_num);

ve aynı cin >> num_of_inputsfakat tutmak coutC gibi Birinci (ziyade iostream sürümü veya) Eğer C hemen hemen aynı performansı elde edersiniz ++ sürümü:

görüntü açıklamasını buraya girin

Aynısı, tutup cindeğiştirirseniz de olur

cout << num_of_trailing_zeros << "\n";

ile

printf("%d", num_of_trailing_zeros);
printf("%s","\n");

Basit bir çözüm çöz için coutve cinIlya Popov belirtildiği gibi:

cin.tie(nullptr);

Standart kitaplık uygulamalarına, her zaman olmamakla birlikte, belirli durumlarda temizleme çağrısını atlamalarına izin verilir. İşte C ++ 14 27.7.2.1.3'ten bir alıntı (chqrlie'ye teşekkürler):

Sınıf basic_istream :: sentry: İlk olarak, is.tie () bir boş gösterici değilse, işlev, çıktı dizisini herhangi bir ilişkili harici C akışıyla senkronize etmek için is.tie () -> flush () 'u çağırır. İs.tie () 'nin koyma alanı boşsa bu çağrı bastırılabilir. Ayrıca, bir uygulamanın bir is.rdbuf () -> underflow () çağrısı gerçekleşene kadar çağrıyı flush'a ertelemesine izin verilir. Nöbetçi nesne yok edilmeden önce böyle bir çağrı olmazsa, temizleme çağrısı tamamen ortadan kaldırılabilir.


Açıklama için teşekkürler. Yine de C ++ 14'ü alıntılamak 27.7.2.1.3: Sınıf basic_istream :: sentry : İlk olarak, eğer is.tie()bir boş gösterici değilse, fonksiyon is.tie()->flush()çıktı dizisini herhangi bir ilişkili harici C akışıyla senkronize etmek için çağırır . Bunun dışında, koyma alanı is.tie()boşsa bu çağrı bastırılabilir . Ayrıca, bir uygulamanın bir çağrı oluşana kadar çağrıyı temizlemeye ertelemesine izin verilir is.rdbuf()->underflow(). Nöbetçi nesne yok edilmeden önce böyle bir çağrı olmazsa, temizleme çağrısı tamamen ortadan kaldırılabilir.
chqrlie

C ++ ile her zamanki gibi işler göründüğünden daha karmaşıktır. OP'nin C ++ kitaplığı, Standardın izin verdiği kadar verimli değildir.
chqrlie

Cppreference bağlantısı için teşekkürler. Yine de baskı ekranındaki "yanlış cevap" ı sevmiyorum ☺
Alex Lop.

@AlexLop. Hata, "yanlış yanıt" sorunu düzeltildi =). Diğer cin'i güncellemeyi unuttum (bu yine de zamanlamayı etkilemez).
vitaut

@chqrlie Doğru, ancak yetersiz akış durumunda bile performans, standart çözüme kıyasla muhtemelen daha kötü olacaktır. Standart referans için teşekkürler.
vitaut
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.