Menghindari pernyataan if di dalam perulangan for?

116

Saya memiliki kelas yang disebut Writeryang memiliki fungsi writeVectorseperti:

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";
    }
}

Saya mencoba untuk tidak memiliki kode duplikat, sambil tetap mengkhawatirkan kinerjanya. Dalam fungsinya, saya melakukan if (index)pengecekan pada setiap putaran saya for-loop, meskipun hasilnya selalu sama. Ini bertentangan dengan "mengkhawatirkan kinerja".

Saya dapat dengan mudah menghindari ini dengan menempatkan centang di luar for-loop saya . Namun, saya akan mendapatkan banyak kode duplikat:

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

Jadi ini adalah solusi yang "buruk" untuk saya. Apa yang saya pikirkan, adalah dua fungsi pribadi, salah satunya keluar dari indeks dan kemudian memanggil yang lain. Yang lainnya hanya mengeluarkan nilainya. Namun, saya tidak tahu bagaimana menggunakannya dengan program saya, saya masih memerlukan ifcek untuk melihat mana yang harus dihubungi ...

Berdasarkan masalah tersebut, polimorfisme sepertinya solusi yang tepat. Tetapi saya tidak dapat melihat bagaimana saya harus menggunakannya di sini. Apa cara yang lebih disukai untuk memecahkan masalah semacam ini?

Ini bukan program nyata, saya hanya tertarik untuk mempelajari bagaimana masalah seperti ini harus diselesaikan.

Skamah One
sumber
8
@JonathonReinhart Mungkin sebagian orang ingin belajar pemrograman, dan penasaran bagaimana cara memecahkan masalah?
Skamah One
9
Saya telah memberikan pertanyaan ini +1. Pengoptimalan semacam ini mungkin tidak sering diperlukan, tetapi pertama, menunjukkan fakta ini dapat menjadi bagian dari jawaban, dan kedua, jenis pengoptimalan yang jarang terjadi masih sangat relevan dengan pemrograman.
jogojapan
31
Pertanyaannya adalah tentang desain yang baik yang menghindari duplikasi kode dan logika rumit di dalam loop. Ini adalah pertanyaan yang bagus, tidak perlu meremehkannya.
Ali
5
Ini adalah pertanyaan yang menarik, biasanya loop-tranformasi yang diteruskan dalam kompiler akan menyelesaikan ini dengan sangat efisien. jika fungsinya cukup kecil seperti ini, inliner akan mengurusnya dan kemungkinan besar akan mematikan cabang sepenuhnya. Saya lebih suka mengubah kode sampai inliner dengan senang hati menyebariskan kode daripada menyelesaikannya dengan template.
Alex
5
@JonathonReinhart: Hah? Revisi pertama dari pertanyaan hampir identik dengan yang ini. "Mengapa Anda peduli?" komentar 100% tidak relevan dengan semua revisi. Mengenai menegur Anda di depan umum - bukan hanya Anda, banyak orang di sini yang menyebabkan masalah ini. Ketika judulnya adalah "menghindari pernyataan if di dalam perulangan for" , seharusnya cukup jelas bahwa pertanyaannya bersifat umum, dan contohnya hanya untuk ilustrasi . Anda tidak membantu siapa pun saat Anda mengabaikan pertanyaan dan membuat OP tampak bodoh karena contoh ilustratif tertentu yang dia gunakan.
pengguna541686

Jawaban:

79

Lewati badan loop sebagai functor.Itu menjadi inline pada waktu kompilasi, tidak ada penalti kinerja.

Ide untuk meneruskan apa yang bervariasi ada di mana-mana di C ++ Standard Library. Itu disebut pola strategi.

Jika Anda diizinkan menggunakan C ++ 11, Anda dapat melakukan sesuatu seperti ini:

#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'; });
}

Kode ini tidak sempurna tetapi Anda mengerti.

Di C ++ 98 lama terlihat seperti ini:

#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;
}

Sekali lagi, kode tersebut jauh dari sempurna tetapi memberi Anda ide.

Ali
sumber
4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Ini adalah jenis solusi yang saya cari, ini bekerja dengan sangat baik :) Anda dapat memperbaikinya dengan sedikit komentar (awalnya sulit memahaminya), tapi saya mengerti jadi tidak masalah :)
Skamah One
1
Saya senang ini membantu! Silakan periksa pembaruan saya dengan kode C ++ 11, ini kurang membengkak dibandingkan dengan versi C ++ 98.
Ali
3
Nitpick: ini baik-baik saja dalam contoh kasus OP karena badan loop sangat kecil, tetapi jika lebih besar (bayangkan selusin baris kode, bukan hanya satu cout << e << "\n";) masih akan ada beberapa duplikasi kode.
syam
3
Mengapa overloading struktur dan operator digunakan dalam contoh C ++ 03? Mengapa tidak membuat dua fungsi saja dan meneruskan petunjuknya kepada mereka?
Malcolm
2
@Malcol Inlining. Jika mereka adalah struktur, kemungkinan pemanggilan fungsi bisa sebaris. Jika Anda meneruskan penunjuk fungsi, kemungkinan panggilan tersebut tidak dapat disebariskan.
Ali
40

Dalam fungsinya, saya melakukan pemeriksaan if (indeks) pada setiap putaran for-loop saya, meskipun hasilnya selalu sama. Ini bertentangan dengan "mengkhawatirkan kinerja".

Jika memang demikian, branch predictor tidak akan memiliki masalah dalam memprediksi hasil (konstan). Dengan demikian, ini hanya akan menyebabkan overhead ringan untuk kesalahan prediksi dalam beberapa iterasi pertama. Tidak ada yang perlu dikhawatirkan dalam hal performa

Dalam hal ini saya menganjurkan untuk menjaga tes di dalam loop untuk kejelasan.

Marc Claesen
sumber
3
Ini hanya sebuah contoh, saya di sini untuk mempelajari bagaimana masalah seperti ini harus diselesaikan. Saya hanya ingin tahu, bahkan tidak membuat program nyata. Seharusnya disebutkan dalam pertanyaan.
Skamah One
40
Dalam hal ini, perlu diingat bahwa pengoptimalan prematur adalah akar dari segala kejahatan . Saat memprogram, selalu fokus pada keterbacaan kode dan pastikan orang lain memahami apa yang Anda coba lakukan. Hanya pertimbangkan pengoptimalan mikro dan berbagai peretasan setelah membuat profil program Anda dan mengidentifikasi hotspot . Anda tidak boleh mempertimbangkan pengoptimalan tanpa menetapkan kebutuhannya. Seringkali, masalah kinerja tidak seperti yang Anda harapkan.
Marc Claesen
3
Dan dalam contoh khusus ini (oke, dipahami, ini hanya sebuah contoh) sangat mungkin bahwa waktu yang dihabiskan untuk kontrol loop dan jika pengujian hampir tidak terlihat di samping waktu yang dihabiskan untuk IO. Ini sering menjadi masalah dengan C ++: memilih antara keterbacaan dengan biaya pemeliharaan dan efisiensi (hipotetis).
kriss
8
Anda berasumsi bahwa kode dijalankan pada prosesor yang memiliki prediksi cabang untuk memulai. Mayoritas sistem yang menjalankan C ++ tidak. (Meskipun, mungkin sebagian besar sistem dengan fungsi yang berguna std::cout)
Ben Voigt
2
-1. Ya, prediksi cabang akan bekerja dengan baik di sini. Ya, kondisi tersebut mungkin benar-benar diangkat di luar loop oleh kompilator. Ya, POITROAE. Tetapi cabang dalam satu lingkaran adalah hal berbahaya yang sering memiliki dampak kinerja, dan saya tidak berpikir mengabaikannya hanya dengan mengatakan "prediksi cabang" adalah nasihat yang baik jika seseorang benar-benar peduli dengan kinerja. Contoh yang paling menonjol adalah bahwa kompilator vektorisasi akan membutuhkan predikasi untuk menangani ini, menghasilkan kode yang kurang efisien daripada untuk loop tanpa cabang.
Oak
35

Untuk memperluas jawaban Ali, yang benar-benar benar tetapi masih menduplikasi beberapa kode (bagian dari badan perulangan, sayangnya hal ini hampir tidak dapat dihindari saat menggunakan pola strategi) ...

Memang dalam kasus khusus ini duplikasi kode tidak banyak tetapi ada cara untuk menguranginya lebih banyak lagi, yang berguna jika badan fungsi lebih besar dari hanya beberapa instruksi .

Kuncinya adalah menggunakan kemampuan kompiler untuk melakukan penghapusan kode lipat / mati secara konstan . Kita dapat melakukannya dengan memetakan nilai runtime secara manual ke nilai indexwaktu kompilasi (mudah dilakukan jika jumlah kasus terbatas - dua dalam kasus ini) dan menggunakan argumen template non-tipe yang dikenal pada kompilasi -waktu:

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);
}

Dengan cara ini kita berakhir dengan kode terkompilasi yang setara dengan contoh kode kedua Anda (luar if/ dalam for) tetapi tanpa menduplikasi kode itu sendiri. Sekarang kita dapat membuat versi template writeVectorserumit yang kita inginkan, akan selalu ada satu kode yang harus dipertahankan.

Perhatikan bagaimana versi template (yang mengambil konstanta waktu kompilasi dalam bentuk argumen template non-tipe) dan versi non-template (yang menggunakan variabel runtime sebagai argumen fungsi) kelebihan beban. Ini memungkinkan Anda untuk memilih versi yang paling relevan tergantung pada kebutuhan Anda, memiliki sintaks yang agak mirip dan mudah diingat dalam kedua kasus:

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
syam
sumber
2
Harap diingat bahwa Anda menghapus duplikasi kode dengan mengorbankan membuat logika di dalam loop menjadi lebih rumit. Saya melihatnya tidak lebih baik atau lebih buruk dari apa yang saya usulkan untuk contoh sederhana ini. +1 lagian!
Ali
1
Saya suka proposal Anda karena menunjukkan kemungkinan pengoptimalan lainnya. Sangat mungkin bahwa indeks bisa menjadi konstanta template sejak awal. Dalam hal ini, ini bisa diganti dengan konstanta runtime oleh pemanggil writeVector dan writeVector diubah ke beberapa template. Menghindari perubahan lebih lanjut pada kode asli.
kriss
1
@kriss: Sebenarnya solusi saya sebelumnya udah udah dibolehkan kalo dipanggil doWriteVectorlangsung tapi saya setuju namanya disayangkan. Saya baru saja mengubahnya menjadi dua writeVectorfungsi yang kelebihan beban (satu template, yang lain fungsi biasa) sehingga hasilnya lebih homogen. Terima kasih untuk sarannya. ;)
syam
4
IMO ini adalah jawaban terbaik. +1
pengguna541686
1
@Mehrdad Kecuali itu tidak menjawab pertanyaan asli Menghindari pernyataan if di dalam perulangan for? Itu menjawab bagaimana menghindari penalti kinerja. Sedangkan untuk "duplikasi", contoh yang lebih realistis dengan kasus penggunaan akan diperlukan untuk melihat bagaimana hal itu difaktorkan dengan baik. Seperti yang saya katakan sebelumnya, saya memuji jawaban ini.
Ali
0

Dalam kebanyakan kasus, kode Anda sudah bagus untuk performa dan keterbacaan. Kompilator yang baik mampu mendeteksi invarian loop dan melakukan pengoptimalan yang sesuai. Pertimbangkan contoh berikut yang sangat dekat dengan kode Anda:

#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));
}

Saya menggunakan baris perintah berikut untuk mengkompilasinya:

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

Lalu, mari kita buang perakitan:

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

Hasil perakitan write_vectoradalah:

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)

Kita dapat melihat, bahwa atas permintaan fungsi, kita memeriksa nilainya dan melompat ke salah satu dari dua kemungkinan loop:

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

Tentu saja, ini hanya berfungsi jika kompiler mampu mendeteksi bahwa suatu kondisi adalah invarian aktual. Biasanya, ini berfungsi sempurna untuk flag dan fungsi inline sederhana. Tetapi jika kondisinya "kompleks", pertimbangkan untuk menggunakan pendekatan dari jawaban lain.

ivaigult
sumber