Tránh câu lệnh if bên trong vòng lặp for?


116

Tôi có một lớp được gọi là Writercó chức năng writeVectornhư vậy:

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

Tôi đang cố gắng không có mã trùng lặp trong khi vẫn lo lắng về hiệu suất. Trong hàm này, tôi đang if (index)kiểm tra mọi vòng for-loop của mình , mặc dù kết quả luôn giống nhau. Điều này chống lại "lo lắng về hiệu suất".

Tôi có thể dễ dàng tránh điều này bằng cách đặt séc bên ngoài for-loop của mình . Tuy nhiên, tôi sẽ nhận được vô số mã trùng lặp:

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

Vì vậy, đây là cả hai giải pháp "không tồi" đối với tôi. Những gì tôi đang nghĩ, là hai hàm riêng tư, một trong số chúng vượt quá chỉ mục và sau đó gọi hàm kia. Cái còn lại chỉ vượt quá giá trị. Tuy nhiên, tôi không thể tìm ra cách sử dụng nó với chương trình của mình, tôi vẫn cần ifkiểm tra xem nên gọi cái nào ...

Theo vấn đề, đa hình có vẻ là một giải pháp đúng. Nhưng tôi không thể thấy làm thế nào tôi nên sử dụng nó ở đây. Cách ưa thích để giải quyết loại vấn đề này là gì?

Đây không phải là một chương trình thực tế, tôi chỉ quan tâm đến việc tìm hiểu cách giải quyết loại vấn đề này.


8
@JonathonReinhart Có lẽ một số người muốn học lập trình và tò mò về cách giải quyết vấn đề?
Skamah One

9
Tôi đã cho câu hỏi này +1. Loại tối ưu hóa này thường có thể không cần thiết, nhưng trước hết, chỉ ra thực tế này có thể là một phần của câu trả lời và thứ hai, các loại tối ưu hóa hiếm hoi vẫn có liên quan nhiều đến lập trình.
jogojapan

31
Câu hỏi là về thiết kế tốt tránh trùng lặp mã và logic phức tạp bên trong vòng lặp. Đó là một câu hỏi hay, không cần phải từ chối nó.
Ali

5
Đó là một câu hỏi thú vị, thông thường các lần chuyển đổi vòng lặp trong trình biên dịch sẽ giải quyết điều này rất hiệu quả. nếu chức năng đủ nhỏ như chức năng này thì nội tuyến sẽ quan tâm đến nó và rất có thể sẽ giết nhánh hoàn toàn. Tôi thà thay đổi mã cho đến khi bộ nội dòng vui vẻ nội dòng mã hơn là giải quyết vấn đề này bằng các mẫu.
Alex

5
@JonathonReinhart: Hả? Bản sửa đổi đầu tiên của câu hỏi hầu như giống với bản này. "Tại sao bạn quan tâm?" nhận xét 100% không liên quan đến tất cả các bản sửa đổi. Đối với việc khiển trách bạn một cách công khai - không chỉ bạn mà có rất nhiều người ở đây gây ra vấn đề này. Khi tiêu đề là "tránh các câu lệnh if bên trong vòng lặp for" , rõ ràng là câu hỏi là chung chung và ví dụ chỉ để minh họa . Bạn không giúp được ai khi bỏ qua câu hỏi và khiến OP trông ngu ngốc vì ví dụ minh họa cụ thể mà anh ta đã sử dụng.
user541686

Câu trả lời:


79

Chuyền vào phần thân của vòng lặp như một cái máy quay. Nó được nội dung tại thời điểm biên dịch, không có hình phạt hiệu suất.

Ý tưởng chuyển những gì khác nhau là rất phổ biến trong Thư viện chuẩn C ++. Nó được gọi là mô hình chiến lược.

Nếu bạn được phép sử dụng C ++ 11, bạn có thể làm như sau:

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

Mã này không hoàn hảo nhưng bạn có được ý tưởng.

Trong C ++ 98 cũ, nó trông như thế này:

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

Một lần nữa, mã này không hoàn hảo nhưng nó cung cấp cho bạn ý tưởng.


4
for(int i=0;i<100;i++){cout<<"Thank you!"<<endl;}: D Đây là loại giải pháp tôi đang tìm kiếm, nó hoạt động như một sự quyến rũ :) Bạn có thể cải thiện nó với một vài ý kiến ​​(ban đầu có vấn đề với việc hiểu nó), nhưng tôi đã hiểu nó nên không có vấn đề gì :)
Skamah One

1
Tôi rất vui vì nó đã giúp! Vui lòng kiểm tra bản cập nhật của tôi với mã C ++ 11, nó ít cồng kềnh hơn so với phiên bản C ++ 98.
Ali

3
Nitpick: điều này tốt trong trường hợp ví dụ của OP vì phần thân vòng lặp rất nhỏ, nhưng nếu nó lớn hơn (hãy tưởng tượng hàng chục dòng mã thay vì chỉ một dòng cout << e << "\n";) sẽ vẫn có khá nhiều mã trùng lặp.
syam

3
Tại sao cấu trúc và nạp chồng toán tử được sử dụng trong ví dụ C ++ 03? Tại sao không chỉ tạo hai hàm và chuyển con trỏ đến chúng?
Malcolm

2
@Malcolm Nội tuyến. Nếu chúng là cấu trúc, rất có thể các lệnh gọi hàm có thể được nội tuyến. Nếu bạn chuyển một con trỏ hàm, rất có thể những lệnh gọi đó không thể được nội dòng.
Ali

40

Trong hàm này, tôi đang thực hiện kiểm tra if (chỉ mục) trên mỗi vòng lặp của tôi, mặc dù kết quả luôn giống nhau. Điều này chống lại "lo lắng về hiệu suất".

Nếu điều này thực sự là như vậy, bộ dự đoán nhánh sẽ không gặp vấn đề gì trong việc dự đoán kết quả (hằng số). Do đó, điều này sẽ chỉ gây ra chi phí nhẹ cho các sai sót trong vài lần lặp đầu tiên. Không có gì phải lo lắng về hiệu suất

Trong trường hợp này, tôi ủng hộ việc giữ thử nghiệm bên trong vòng lặp cho rõ ràng.


3
Đó chỉ là một ví dụ, tôi ở đây để tìm hiểu cách giải quyết loại vấn đề này. Tôi chỉ tò mò, thậm chí không tạo ra một chương trình thực sự. Nên đã đề cập đến nó trong câu hỏi.
Skamah One

40
Trong trường hợp đó, hãy nhớ rằng tối ưu hóa quá sớm là gốc rễ của mọi điều xấu . Khi lập trình, hãy luôn tập trung vào khả năng đọc mã và đảm bảo người khác hiểu bạn đang cố gắng làm gì. Chỉ xem xét các tối ưu hóa vi mô và các thủ thuật khác nhau sau khi lập hồ sơ chương trình của bạn và xác định các điểm phát sóng . Bạn không bao giờ nên xem xét tối ưu hóa mà không thiết lập nhu cầu cho chúng. Thông thường, các vấn đề về hiệu suất không như bạn mong đợi.
Marc Claesen

3
Và trong ví dụ cụ thể này (được hiểu, đây chỉ là một ví dụ), rất có thể thời gian dành cho điều khiển vòng lặp và nếu kiểm tra gần như vô hình bên cạnh thời gian dành cho IO. Đây thường là một vấn đề với C ++: lựa chọn giữa khả năng đọc với chi phí bảo trì và hiệu quả (giả định).
kriss

8
Bạn đang giả định rằng mã đang chạy trên một bộ xử lý có dự đoán nhánh để bắt đầu. Phần lớn các hệ thống chạy C ++ thì không. (Mặc dù, có lẽ đa số các hệ thống với một hữu ích std::coutdo)
Ben Voigt

2
-1. Có, dự đoán nhánh sẽ hoạt động tốt ở đây. Có, điều kiện thực sự có thể được trình biên dịch đưa ra bên ngoài vòng lặp. Vâng, POITROAE. Nhưng các nhánh trong vòng lặp một thứ nguy hiểm thường có tác động đến hiệu suất và tôi không nghĩ rằng việc loại bỏ chúng bằng cách chỉ nói "dự đoán nhánh" là một lời khuyên tốt nếu ai đó thực sự quan tâm đến hiệu suất. Ví dụ đáng chú ý nhất là một trình biên dịch vectơ sẽ cần dự đoán để xử lý điều này, tạo ra mã kém hiệu quả hơn so với các vòng lặp ít nhánh.
Oak

35

Để mở rộng về câu trả lời của Ali, câu trả lời là hoàn toàn chính xác nhưng vẫn trùng lặp một số mã (một phần của nội dung vòng lặp, điều này rất khó tránh khỏi khi sử dụng mẫu chiến lược) ...

Được cho là trong trường hợp cụ thể này, sự trùng lặp mã không nhiều nhưng có một cách để giảm nó nhiều hơn, điều này rất hữu ích nếu phần thân hàm lớn hơn chỉ một vài lệnh .

Chìa khóa là sử dụng khả năng của trình biên dịch để thực hiện việc loại bỏ mã gấp / chết liên tục . Chúng ta có thể làm điều đó bằng cách ánh xạ thủ công giá trị thời gian chạy của thành giá trị thời gian indexbiên dịch (dễ thực hiện khi chỉ có một số trường hợp hạn chế - trong trường hợp này là hai trường hợp) và sử dụng đối số mẫu không phải kiểu được biết đến khi biên dịch -thời gian:

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

Bằng cách này, chúng tôi kết thúc với mã được biên dịch tương đương với ví dụ mã thứ hai của bạn (bên ngoài if/ bên trong for) nhưng không tự sao chép mã. Bây giờ chúng ta có thể làm cho phiên bản mẫu writeVectorphức tạp như chúng ta muốn, sẽ luôn có một đoạn mã duy nhất để duy trì.

Lưu ý cách phiên bản mẫu (sử dụng hằng số thời gian biên dịch ở dạng đối số mẫu không phải kiểu) và phiên bản không phải mẫu (nhận biến thời gian chạy làm đối số hàm) được nạp chồng. Điều này cho phép bạn chọn phiên bản phù hợp nhất tùy thuộc vào nhu cầu của mình, có cú pháp khá giống nhau, dễ nhớ trong cả hai trường hợp:

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
Hãy nhớ rằng bạn đã xóa mã trùng lặp với chi phí làm cho logic bên trong vòng lặp phức tạp hơn. Tôi thấy nó không tốt hơn cũng không tệ hơn những gì tôi đề xuất cho ví dụ đơn giản cụ thể này. +1 dù sao!
Ali

1
Tôi thích đề xuất của bạn vì nó cho thấy một khả năng tối ưu hóa khác. Rất có thể chỉ mục có thể là một hằng số mẫu ngay từ đầu. Trong trường hợp này, nó có thể được thay thế bằng một hằng số thời gian chạy bởi trình gọi writeVector và writeVector được thay đổi thành một số mẫu. Tránh bất kỳ thay đổi nào đối với mã gốc.
kriss

1
@kriss: Trên thực tế, giải pháp trước đây của tôi đã cho phép nếu bạn gọi doWriteVectortrực tiếp nhưng tôi đồng ý tên thật không may. Tôi chỉ cần thay đổi nó để có hai writeVectorhàm quá tải (một mẫu, một hàm thông thường) để kết quả đồng nhất hơn. Cám ơn vì sự gợi ý. ;)
syam

4
IMO đây là câu trả lời tốt nhất. +1
dùng541686 Ngày

1
@Mehrdad Ngoại trừ việc nó không trả lời câu hỏi ban đầu Tránh câu lệnh if bên trong vòng lặp for? Nó trả lời làm thế nào để tránh hình phạt hiệu suất. Đối với "sự trùng lặp", sẽ cần một ví dụ thực tế hơn với các trường hợp sử dụng để xem nó được tính toán tốt nhất như thế nào. Như tôi đã nói trước đây, tôi đã ủng hộ câu trả lời này.
Ali

0

Trong hầu hết các trường hợp, mã của bạn đã tốt về hiệu suất và khả năng đọc. Một trình biên dịch tốt có khả năng phát hiện các bất biến của vòng lặp và thực hiện các tối ưu hóa thích hợp. Hãy xem xét ví dụ sau đây rất gần với mã của bạ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));
}

Tôi đang sử dụng dòng lệnh sau để biên dịch nó:

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

Sau đó, hãy kết xuất lắp ráp:

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

Kết quả của tập hợp write_vectorlà:

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)

Chúng ta có thể thấy rằng khi hàm này bắt đầu, chúng ta kiểm tra giá trị và chuyển đến một trong hai vòng lặp có thể:

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

Tất nhiên, điều này chỉ hoạt động nếu trình biên dịch có khả năng phát hiện rằng một điều kiện là bất biến thực tế. Thông thường, nó hoạt động hoàn hảo đối với cờ và các hàm nội tuyến đơn giản. Nhưng nếu điều kiện là "phức tạp", hãy xem xét sử dụng các cách tiếp cận từ các câu trả lời khác.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.