Bir for döngüsü içinde if deyiminden kaçınmak mı?


116

Böyle Writerbir işlevi olan bir sınıfım var writeVector:

void Drawer::writeVector(vector<T> vec, bool index=true)
{
    for (unsigned int i = 0; i < vec.size(); i++) {
        if (index) {
            cout << i << "\t";
        }
        cout << vec[i] << "\n";
    }
}

Hala performans konusunda endişelenirken, yinelenen bir koda sahip olmamaya çalışıyorum. İşlevde, sonuç her zaman aynı olsa da, döngümün if (index)her turunda kontrol yapıyorum for. Bu, "performans konusunda endişelenmeye" aykırıdır.

forÇeki-döngümün dışına yerleştirerek bunu kolayca önleyebilirim . Ancak, bir sürü yinelenen kod alacağım:

void Drawer::writeVector(...)
{
    if (index) {
        for (...) {
            cout << i << "\t" << vec[i] << "\n";
        }
    }
    else {
        for (...) {
            cout << vec[i] << "\n";
        }
    }
}

Yani bunların ikisi de benim için "kötü" çözümler. Düşündüğüm şey, biri dizinin dışında kalan ve sonra diğerini arayan iki özel fonksiyon. Diğeri sadece değeri aşıyor. Ancak, programımla nasıl kullanacağımı çözemiyorum, ifhangisini arayacağımı görmek için yine de kontrole ihtiyacım var ...

Soruna göre polimorfizm doğru bir çözüm gibi görünüyor. Ama burada nasıl kullanmalıyım göremiyorum. Bu tür bir sorunu çözmenin tercih edilen yolu nedir?

Bu gerçek bir program değil, sadece bu tür bir problemin nasıl çözülmesi gerektiğini öğrenmekle ilgileniyorum.


8
@JonathonReinhart Belki bazı insanlar programlama öğrenmek istiyor ve problemleri nasıl çözeceklerini merak ediyorlar?
Skamah One 01

9
Bu soruyu +1 verdim. Bu tür bir optimizasyon genellikle gerekli olmayabilir, ancak ilk olarak, bu gerçeğe işaret etmek cevabın bir parçası olabilir ve ikinci olarak, nadir optimizasyon türleri hala programlama ile oldukça ilgilidir.
jogojapan

31
Soru, döngü içinde kod tekrarını ve karmaşık mantığı ortadan kaldıran iyi bir tasarımla ilgilidir. Bu iyi bir soru, olumsuz oy vermeye gerek yok.
Ali

5
Bu ilginç bir soru, genellikle derleyicideki döngü dönüşümleri bunu çok verimli bir şekilde çözecektir. işlev bunun gibi yeterince küçükse, inliner bununla ilgilenecek ve büyük olasılıkla dalı tamamen öldürecektir. Bunu şablonlarla çözmek yerine, inliner kodu mutlu bir şekilde satır içi yapana kadar kodu değiştirmeyi tercih ederim.
Alex

5
@JonathonReinhart: Ha? Sorunun ilk revizyonu bununla hemen hemen aynıdır. "Neden umursuyorsun?" yorum tüm revizyonlarla % 100 ilgisizdir . Sizi alenen azarlamaya gelince - sadece siz değil, burada bu soruna neden olan birçok insan var. Başlığı "döngü için iç ifadeleri ise kaçınma" olduğunda , oldukça açık olmalı soru jenerik olduğunu ve örnek için sadece resimde . Soruyu görmezden geldiğinizde ve kullandığı belirli açıklayıcı örnek yüzünden OP'yi aptal gibi gösterdiğinizde kimseye yardım etmiyorsunuz.
user541686

Yanıtlar:


79

Döngünün gövdesini bir functor olarak geçirin. Derleme zamanında sıraya dizilir, performans kesintisi olmaz.

Değişen şeyi aktarma fikri C ++ Standart Kitaplığı'nda her yerde bulunur. Buna strateji kalıbı denir .

C ++ 11'i kullanma izniniz varsa, şöyle bir şey yapabilirsiniz:

#include <iostream>
#include <set>
#include <vector>

template <typename Container, typename Functor, typename Index = std::size_t>
void for_each_indexed(const Container& c, Functor f, Index index = 0) {

    for (const auto& e : c)
        f(index++, e);
}

int main() {

    using namespace std;

    set<char> s{'b', 'a', 'c'};

    // indices starting at 1 instead of 0
    for_each_indexed(s, [](size_t i, char e) { cout<<i<<'\t'<<e<<'\n'; }, 1u);

    cout << "-----" << endl;

    vector<int> v{77, 88, 99};

    // without index
    for_each_indexed(v, [](size_t , int e) { cout<<e<<'\n'; });
}

Bu kod mükemmel değil ama fikri anladınız.

Eski C ++ 98'de şöyle görünür:

#include <iostream>
#include <vector>
using namespace std;

struct with_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << i << '\t' << e << '\n';
  }
};

struct without_index {
  void operator()(ostream& out, vector<int>::size_type i, int e) {
    out << e << '\n';
  }
};


template <typename Func>
void writeVector(const vector<int>& v, Func f) {
  for (vector<int>::size_type i=0; i<v.size(); ++i) {
    f(cout, i, v[i]);
  }
}

int main() {

  vector<int> v;
  v.push_back(77);
  v.push_back(88);
  v.push_back(99);

  writeVector(v, with_index());

  cout << "-----" << endl;

  writeVector(v, without_index());

  return 0;
}

Yine, kod mükemmel olmaktan uzaktır, ancak size fikir verir.


4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Aradığım türden bir çözüm bu, bir cazibe gibi çalışıyor :) Birkaç yorumla iyileştirebilirsiniz (ilk başta anlamakta sorun yaşadım), ama anladım çok sorun değil :)
Skamah One

1
Yardımcı olduğuna sevindim! Lütfen güncellememi C ++ 11 koduyla kontrol edin, C ++ 98 sürümüne kıyasla daha az şişirilmiş.
Ali

3
Nitpick: OP'nin örnek durumunda bu iyidir çünkü döngü gövdesi çok küçüktür, ancak daha büyük olsaydı (tek bir kod yerine bir düzine kod satırı hayal edin cout << e << "\n";) yine de epeyce kod kopyası olurdu.
syam

3
C ++ 03 örneğinde yapılar ve operatör aşırı yüklemesi neden kullanılıyor? Neden sadece iki işlev yapıp işaretçileri onlara iletmiyorsunuz?
Malcolm

2
@Malcolm Inlining. Yapı iseler, fonksiyon çağrılarının satır içinde olma ihtimali vardır. Bir işlev işaretçisini geçerseniz, bu çağrıların satır içine alınamama ihtimali vardır .
Ali

40

İşlevde, sonuç her zaman aynı olsa da, for-döngümün her turunda if (dizin) kontrolü yapıyorum. Bu "performans konusunda endişelenmeye" aykırıdır.

Eğer gerçekten durum böyleyse, dal tahmincisinin (sabit) sonucu tahmin etmede hiçbir problemi olmayacaktır. Bu nedenle, bu, ilk birkaç yinelemede yanlış tahminler için yalnızca hafif bir ek yüke neden olacaktır. Performans açısından endişelenecek bir şey yok

Bu durumda, açıklık için testi döngü içinde tutmayı savunuyorum.


3
Bu sadece bir örnek, bu tür problemlerin nasıl çözülmesi gerektiğini öğrenmek için buradayım. Sadece merak ediyorum, gerçek bir program bile oluşturmuyorum. Soruda bahsetmeliydim.
Skamah One

40
Bu durumda, erken optimizasyonun tüm kötülüklerin kökü olduğunu unutmayın . Programlarken her zaman kod okunabilirliğine odaklanın ve başkalarının ne yapmaya çalıştığınızı anladığından emin olun. Yalnızca programınızın profilini çıkardıktan ve etkin noktaları belirledikten sonra mikro optimizasyonları ve çeşitli saldırıları dikkate alın . Onlara ihtiyaç duymadan optimizasyonları asla düşünmemelisiniz. Çoğu zaman, performans sorunları olmasını beklediğiniz yerde değildir.
Marc Claesen

3
Ve bu özel örnekte (tamam, anlaşıldı, bu sadece bir örnek) büyük olasılıkla döngü kontrolü için harcanan zaman ve eğer test IO için harcanan zamanın yanında neredeyse görünmez. Bu genellikle C ++ ile ilgili bir sorundur: bakım maliyeti karşılığında okunabilirlik ve (varsayımsal) verimlilik arasında seçim yapma.
kriss

8
Kodun, başlangıç ​​için dal tahmini olan bir işlemcide çalıştığını varsayıyorsunuz. C ++ çalıştıran sistemlerin çoğu bunu yapmaz. (Yine de, muhtemelen işe yarayan sistemlerin çoğu std::cout)
Ben Voigt

2
-1. Evet, şube tahmini burada iyi çalışacak. Evet, koşul aslında derleyici tarafından döngünün dışına kaldırılmış olabilir. Evet, POITROAE. Ama bir döngü içinde dalları olan tehlikeli bir şey genellikle birisi gerçekten umurunda olmadığını performansı hakkında iyi bir tavsiye edilir performans etkisi vardır ve sadece "dal tahmini" diyerek bu işten sanmıyorum. En dikkate değer örnek, vektörleştiren bir derleyicinin bunu işlemek için tahmine ihtiyaç duyacağı ve dalsız döngülerden daha az verimli kod üretmesidir.
Meşe

35

Ali'nin cevabını genişletmek gerekirse, ki bu tamamen doğru ama yine de bazı kodları çoğaltır (döngü gövdesinin bir parçası, strateji modelini kullanırken maalesef bundan kaçınılamaz) ...

Bu özel durumda kabul edildiğinde, kod kopyası çok fazla değildir, ancak onu daha da azaltmanın bir yolu vardır, bu , işlev gövdesi birkaç talimattan daha büyükse işe yarar .

Anahtar, derleyicinin sürekli katlama / ölü kod eleme gerçekleştirme yeteneğini kullanmaktır . Bunu, çalışma zamanı değerini indexbir derleme zamanı değerine manuel olarak eşleyerek yapabiliriz (yalnızca sınırlı sayıda vaka olduğunda yapmak kolaydır - bu durumda iki) ve derleme sırasında bilinen tür olmayan bir şablon argümanı kullanabiliriz. -zaman:

template<bool index = true>
//                  ^^^^^^ note: the default value is now part of the template version
//                         see below to understand why
void writeVector(const vector<int>& vec) {
    for (size_t i = 0; i < vec.size(); ++i) {
        if (index) { // compile-time constant: this test will always be eliminated
            cout << i << "\t"; // this will only be kept if "index" is true
        }
        cout << vec[i] << "\n";
    }
}

void writeVector(const vector<int>& vec, bool index)
//                                            ^^^^^ note: no more default value, otherwise
//                                            it would clash with the template overload
{
    if (index) // runtime decision
        writeVector<true>(vec);
        //          ^^^^ map it to a compile-time constant
    else
        writeVector<false>(vec);
}

Bu şekilde, ikinci kod örneğinize (dış if/ iç for) eşdeğer, ancak kodu kendimiz çoğaltmadan derlenmiş bir kod elde ederiz. Artık şablon sürümünü writeVectoristediğimiz kadar karmaşık hale getirebiliriz, her zaman bakımı yapılacak tek bir kod parçası olacaktır.

Şablon sürümünün (tür olmayan bir şablon bağımsız değişkeni biçiminde bir derleme zamanı sabitini alan) ve şablon olmayan sürümün (işlev bağımsız değişkeni olarak bir çalışma zamanı değişkenini alan) nasıl aşırı yüklendiğine dikkat edin. Bu, her iki durumda da oldukça benzer, hatırlaması kolay bir sözdizimine sahip olarak ihtiyaçlarınıza bağlı olarak en uygun sürümü seçmenize olanak tanır:

writeVector<true>(vec);   // you already know at compile-time which version you want
                          // no need to go through the non-template runtime dispatching

writeVector(vec, index);  // you don't know at compile-time what "index" will be
                          // so you have to use the non-template runtime dispatching

writeVector(vec);         // you can even use your previous syntax using a default argument
                          // it will call the template overload directly

2
Döngünün içindeki mantığı daha karmaşık hale getirme pahasına kod çoğaltmasını kaldırdığınızı lütfen unutmayın. Bu basit örnek için önerdiğimden ne daha iyi ne de daha kötü görüyorum. Yine de +1!
Ali

1
Teklifinizi beğendim çünkü başka bir olası optimizasyonu gösteriyor. Endeksin baştan itibaren bir şablon sabiti olması çok olasıdır. Bu durumda, writeVector'ın çağırıcısı tarafından bir çalışma zamanı sabiti ile değiştirilebilir ve writeVector bir şablonla değiştirilir. Orijinal kodda herhangi bir değişiklik yapmaktan kaçınma.
kriss

1
@kriss: Aslında benim önceki çözümüm zaten doWriteVectordoğrudan aradıysanız ama adın talihsiz olduğuna katılıyorum. writeVectorSonucun daha homojen olması için onu iki aşırı yüklenmiş işleve (biri şablon, diğeri normal işlev) sahip olacak şekilde değiştirdim. Önerin için teşekkürler. ;)
syam

4
IMO bu en iyi cevaptır. +1
user541686

1
@Mehrdad Orijinal soruyu cevaplamaması dışında for döngüsü içinde if ifadesinden kaçınmak mı? Yine de performans cezasından nasıl kaçınılacağına cevap veriyor. "Çoğaltmaya" gelince, en iyi şekilde nasıl hesaba katılacağını görmek için kullanım senaryoları ile daha gerçekçi bir örnek gerekecektir. Daha önce de söylediğim gibi, bu yanıta olumlu oy verdim.
Ali

0

Çoğu durumda, kodunuz performans ve okunabilirlik için zaten iyidir. İyi bir derleyici döngü değişmezlerini algılayabilir ve uygun optimizasyonları yapabilir. Kodunuza çok yakın olan aşağıdaki örneği düşünün:

#include <cstdio>
#include <iterator>

void write_vector(int* begin, int* end, bool print_index = false) {
    unsigned index = 0;
    for(int* it = begin; it != end; ++it) {
        if (print_index) {
            std::printf("%d: %d\n", index, *it);
        } else {
            std::printf("%d\n", *it);
        }
        ++index;
    }
}

int my_vector[] = {
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
};


int main(int argc, char** argv) {
    write_vector(std::begin(my_vector), std::end(my_vector));
}

Derlemek için aşağıdaki komut satırını kullanıyorum:

g++ --version
g++ (GCC) 4.9.1
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
g++ -O3 -std=c++11 main.cpp

O zaman montajı terk edelim:

objdump -d a.out | c++filt > main.s

Sonuç derlemesi write_vector:

00000000004005c0 <write_vector(int*, int*, bool)>:
  4005c0:   48 39 f7                cmp    %rsi,%rdi
  4005c3:   41 54                   push   %r12
  4005c5:   49 89 f4                mov    %rsi,%r12
  4005c8:   55                      push   %rbp
  4005c9:   53                      push   %rbx
  4005ca:   48 89 fb                mov    %rdi,%rbx
  4005cd:   74 25                   je     4005f4 <write_vector(int*, int*, bool)+0x34>
  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>
  4005d3:   31 ed                   xor    %ebp,%ebp
  4005d5:   0f 1f 00                nopl   (%rax)
  4005d8:   8b 13                   mov    (%rbx),%edx
  4005da:   89 ee                   mov    %ebp,%esi
  4005dc:   31 c0                   xor    %eax,%eax
  4005de:   bf a4 06 40 00          mov    $0x4006a4,%edi
  4005e3:   48 83 c3 04             add    $0x4,%rbx
  4005e7:   83 c5 01                add    $0x1,%ebp
  4005ea:   e8 81 fe ff ff          callq  400470 <printf@plt>
  4005ef:   49 39 dc                cmp    %rbx,%r12
  4005f2:   75 e4                   jne    4005d8 <write_vector(int*, int*, bool)+0x18>
  4005f4:   5b                      pop    %rbx
  4005f5:   5d                      pop    %rbp
  4005f6:   41 5c                   pop    %r12
  4005f8:   c3                      retq   
  4005f9:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
  400600:   8b 33                   mov    (%rbx),%esi
  400602:   31 c0                   xor    %eax,%eax
  400604:   bf a8 06 40 00          mov    $0x4006a8,%edi
  400609:   48 83 c3 04             add    $0x4,%rbx
  40060d:   e8 5e fe ff ff          callq  400470 <printf@plt>
  400612:   49 39 dc                cmp    %rbx,%r12
  400615:   75 e9                   jne    400600 <write_vector(int*, int*, bool)+0x40>
  400617:   eb db                   jmp    4005f4 <write_vector(int*, int*, bool)+0x34>
  400619:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)

Fonksiyonun başlangıcında, değeri kontrol ettiğimizi ve iki olası döngüden birine atladığımızı görebiliriz:

  4005cf:   84 d2                   test   %dl,%dl
  4005d1:   74 2d                   je     400600 <write_vector(int*, int*, bool)+0x40>

Elbette, bu yalnızca bir derleyici bir koşulun gerçek değişmez olduğunu algılayabiliyorsa işe yarar. Genellikle, bayraklar ve basit satır içi işlevler için mükemmel şekilde çalışır. Ancak koşul "karmaşık" ise, diğer yanıtlardan yaklaşımları kullanmayı düşünün.

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.