Hiệu ứng của việc đặt hàng nếu khác người khác nếu báo cáo theo xác suất là gì?


187

Cụ thể, nếu tôi có một loạt if... else ifcâu lệnh và bằng cách nào đó tôi biết trước xác suất tương đối mà mỗi câu lệnh sẽ đánh giá true, có bao nhiêu sự khác biệt trong thời gian thực hiện để sắp xếp chúng theo thứ tự xác suất? Ví dụ, tôi nên thích điều này:

if (highly_likely)
  //do something
else if (somewhat_likely)
  //do something
else if (unlikely)
  //do something

đến đây?:

if (unlikely)
  //do something
else if (somewhat_likely)
  //do something
else if (highly_likely)
  //do something

Rõ ràng là phiên bản được sắp xếp sẽ nhanh hơn, tuy nhiên để dễ đọc hoặc tồn tại các hiệu ứng phụ, chúng tôi có thể muốn đặt hàng chúng không tối ưu. Thật khó để nói CPU sẽ làm tốt như thế nào với dự đoán nhánh cho đến khi bạn thực sự chạy mã.

Vì vậy, trong quá trình thử nghiệm điều này, cuối cùng tôi đã trả lời câu hỏi của riêng mình cho một trường hợp cụ thể, tuy nhiên tôi cũng muốn nghe những ý kiến ​​/ hiểu biết khác.

Quan trọng: câu hỏi này giả định rằng các iftuyên bố có thể được sắp xếp lại một cách tùy tiện mà không có bất kỳ ảnh hưởng nào khác đến hành vi của chương trình. Trong câu trả lời của tôi, ba bài kiểm tra có điều kiện loại trừ lẫn nhau và không tạo ra tác dụng phụ. Chắc chắn, nếu các tuyên bố phải được đánh giá theo một thứ tự nhất định để đạt được một số hành vi mong muốn, thì vấn đề hiệu quả là phải tranh luận.


35
bạn có thể muốn thêm một lưu ý rằng các điều kiện là loại trừ lẫn nhau, nếu không hai phiên bản không tương đương
idclev 463035818

28
Thật thú vị khi một câu hỏi tự trả lời có hơn 20 câu trả lời với câu trả lời khá kém, trong một giờ. Không gọi bất cứ điều gì trên OP nhưng upvoters nên cẩn thận khi nhảy vào đoàn xe. Câu hỏi có thể thú vị, nhưng kết quả đáng ngờ.
luk32

3
Tôi tin rằng điều này có thể được mô tả như một hình thức đánh giá ngắn mạch bởi vì đánh một so sánh phủ nhận việc đánh một so sánh khác. Cá nhân tôi ủng hộ việc thực hiện như thế này khi so sánh nhanh, giả sử là boolean, có thể ngăn tôi đi vào một so sánh khác có thể liên quan đến thao tác chuỗi nặng tài nguyên, biểu thức chính quy hoặc tương tác cơ sở dữ liệu.
MonkeyZeus

11
Một số trình biên dịch cung cấp khả năng thu thập số liệu thống kê về các nhánh được lấy và đưa chúng trở lại trình biên dịch để cho phép nó thực hiện tối ưu hóa tốt hơn.

11
Nếu hiệu suất như vấn đề này đối với bạn, có lẽ bạn nên thử Tối ưu hóa theo hướng dẫn hồ sơ và so sánh kết quả thủ công của bạn với kết quả của trình biên dịch
Justin

Câu trả lời:


96

Theo nguyên tắc chung, hầu hết nếu không phải tất cả các CPU Intel đều cho rằng các nhánh chuyển tiếp không được thực hiện lần đầu tiên khi chúng nhìn thấy chúng. Xem công việc của Godbolt .

Sau đó, nhánh đi vào bộ đệm dự đoán nhánh và hành vi trong quá khứ được sử dụng để thông báo dự đoán nhánh trong tương lai.

Vì vậy, trong một vòng lặp chặt chẽ, ảnh hưởng của việc điều chỉnh sai sẽ tương đối nhỏ. Công cụ dự đoán chi nhánh sẽ tìm hiểu tập hợp các nhánh nào có khả năng nhất và nếu bạn có khối lượng công việc không hề nhỏ trong vòng lặp, những khác biệt nhỏ sẽ không tăng thêm nhiều.

Trong mã chung, hầu hết các trình biên dịch theo mặc định (thiếu lý do khác) sẽ đặt hàng mã máy được sản xuất gần như cách bạn đặt hàng trong mã của mình. Do đó, nếu các câu lệnh là nhánh chuyển tiếp khi chúng thất bại.

Vì vậy, bạn nên sắp xếp các chi nhánh của mình theo thứ tự giảm khả năng để có được dự đoán chi nhánh tốt nhất từ ​​"lần gặp đầu tiên".

Một microbenchmark lặp đi lặp lại nhiều lần trong một tập hợp các điều kiện và công việc tầm thường sẽ bị chi phối bởi các hiệu ứng nhỏ của số lượng lệnh và tương tự, và rất ít trong các vấn đề dự đoán chi nhánh tương đối. Vì vậy, trong trường hợp này, bạn phải lập hồ sơ , vì quy tắc ngón tay cái sẽ không đáng tin cậy.

Trên hết, vector hóa và nhiều tối ưu hóa khác áp dụng cho các vòng lặp nhỏ hẹp.

Vì vậy, trong mã chung, hãy đặt mã có khả năng nhất trong ifkhối và điều đó sẽ dẫn đến một vài lỗi dự đoán nhánh không được lưu trong bộ nhớ cache. Trong các vòng lặp chặt chẽ, hãy làm theo quy tắc chung để bắt đầu, và nếu bạn cần biết thêm, bạn có rất ít sự lựa chọn ngoài việc lập hồ sơ.

Tất nhiên điều này tất cả đi ra ngoài cửa sổ nếu một số thử nghiệm rẻ hơn nhiều so với những người khác.


19
Cũng đáng để xem xét bản thân các bài kiểm tra đắt như thế nào: nếu một bài kiểm tra chỉ có khả năng cao hơn một chút, nhưng đắt hơn rất nhiều, thì có thể đáng để đặt bài kiểm tra khác lên trước, bởi vì sự tiết kiệm từ việc không làm bài kiểm tra đắt tiền có thể sẽ vượt trội hơn tiết kiệm từ dự đoán chi nhánh, v.v.
psmears

Các liên kết mà bạn cung cấp không hỗ trợ kết luận của bạn Theo nguyên tắc chung, hầu hết nếu không phải tất cả các CPU Intel giả chi nhánh về phía trước không được thực hiện lần đầu tiên họ nhìn thấy chúng . Trong thực tế, điều đó chỉ đúng với CPU Arrendale tương đối khó hiểu mà kết quả được hiển thị đầu tiên. Kết quả chính của Ivy Bridge và Haswell không hỗ trợ điều đó. Haswell trông rất gần với "luôn luôn dự đoán rơi" cho các nhánh không nhìn thấy và Ivy Bridge hoàn toàn không rõ ràng.
BeeOnRope

Người ta thường hiểu rằng CPU không thực sự sử dụng dự đoán tĩnh như trước đây. Thật vậy, Intel hiện đại có lẽ đang sử dụng một cái gì đó giống như một công cụ dự đoán TAGE xác suất. Bạn chỉ cần băm lịch sử chi nhánh vào các bảng lịch sử khác nhau và lấy một bảng phù hợp với lịch sử dài nhất. Nó sử dụng "thẻ" để cố gắng tránh răng cưa, nhưng thẻ chỉ có một vài bit. Nếu bạn bỏ lỡ ở tất cả các độ dài lịch sử, một số dự đoán mặc định có thể được thực hiện mà không nhất thiết phụ thuộc vào hướng chi nhánh (trên Haswell chúng ta có thể nói rõ ràng là không).
BeeOnRope

44

Tôi đã thực hiện bài kiểm tra sau để tính thời gian thực hiện hai if... else ifkhối khác nhau , một khối được sắp xếp theo thứ tự xác suất, khối còn lại được sắp xếp theo thứ tự ngược lại:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    long long sortedTime = 0;
    long long reverseTime = 0;

    for (int n = 0; n != 500; ++n)
    {
        //Generate a vector of 5000 random integers from 1 to 100
        random_device rnd_device;
        mt19937 rnd_engine(rnd_device());
        uniform_int_distribution<int> rnd_dist(1, 100);
        auto gen = std::bind(rnd_dist, rnd_engine);
        vector<int> rand_vec(5000);
        generate(begin(rand_vec), end(rand_vec), gen);

        volatile int nLow, nMid, nHigh;
        chrono::time_point<chrono::high_resolution_clock> start, end;

        //Sort the conditional statements in order of increasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 95) ++nHigh;               //Least likely branch
            else if (i < 20) ++nLow;
            else if (i >= 20 && i < 95) ++nMid; //Most likely branch
        }
        end = chrono::high_resolution_clock::now();
        reverseTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

        //Sort the conditional statements in order of decreasing likelyhood
        nLow = nMid = nHigh = 0;
        start = chrono::high_resolution_clock::now();
        for (int& i : rand_vec) {
            if (i >= 20 && i < 95) ++nMid;  //Most likely branch
            else if (i < 20) ++nLow;
            else if (i >= 95) ++nHigh;      //Least likely branch
        }
        end = chrono::high_resolution_clock::now();
        sortedTime += chrono::duration_cast<chrono::nanoseconds>(end-start).count();

    }

    cout << "Percentage difference: " << 100 * (double(reverseTime) - double(sortedTime)) / double(sortedTime) << endl << endl;
}

Sử dụng MSVC2017 với / O2, kết quả cho thấy phiên bản được sắp xếp nhanh hơn khoảng 28% so với phiên bản chưa được sắp xếp. Theo nhận xét của luk32, tôi cũng đã chuyển thứ tự của hai bài kiểm tra, điều này tạo ra sự khác biệt đáng chú ý (22% so với 28%). Mã được chạy trong Windows 7 trên Intel Xeon E5-2697 v2. Tất nhiên, đây là vấn đề rất cụ thể và không nên được hiểu là một câu trả lời kết luận.


9
OP nên cẩn thận, vì việc thay đổi một if... else iftuyên bố có thể có ảnh hưởng đáng kể đến cách logic chảy qua mã. Các unlikelykiểm tra có thể không đưa ra thường xuyên, nhưng có thể là một nhu cầu kinh doanh để kiểm tra sự unlikelykiện đầu tiên trước khi kiểm tra cho người khác.
Luke T Brooks

21
Nhanh hơn 30%? Bạn có nghĩa là nó nhanh hơn khoảng% của phần bổ sung nếu các câu lệnh không phải thực hiện? Có vẻ là một kết quả khá hợp lý.
UKMonkey

5
Làm thế nào bạn điểm chuẩn nó? Trình biên dịch nào, cpu, v.v.? Tôi khá chắc chắn rằng kết quả này không phải là di động.
luk32

12
Một vấn đề với microbenchmark này là CPU sẽ tìm ra nhánh nào có khả năng nhất và lưu trữ bộ đệm khi bạn lặp đi lặp lại trên nó. Nếu các nhánh không được kiểm tra trong một vòng lặp nhỏ hẹp, bộ đệm dự đoán nhánh có thể không có chúng trong đó và chi phí có thể cao hơn nhiều nếu CPU đoán sai với hướng dẫn bộ đệm dự đoán nhánh không.
Yakk - Adam Nevraumont

6
Điểm chuẩn này không quá đáng tin cậy. Biên dịch với gcc 6.3.0 : g++ -O2 -march=native -std=c++14không mang lại lợi thế cho các câu điều kiện được sắp xếp, nhưng hầu hết thời gian, phần trăm chênh lệch giữa hai lần chạy là ~ 5%. Một vài lần, nó thực sự chậm hơn (do phương sai). Tôi khá chắc chắn rằng việc đặt hàng ifnhư thế này không đáng lo ngại; PGO có thể sẽ hoàn toàn xử lý bất kỳ trường hợp nào như vậy
Justin

30

Không, bạn không nên, trừ khi bạn thực sự chắc chắn rằng hệ thống mục tiêu bị ảnh hưởng. Theo mặc định đi bằng khả năng đọc.

Tôi rất nghi ngờ kết quả của bạn. Tôi đã sửa đổi ví dụ của bạn một chút, vì vậy việc thực hiện đảo ngược dễ dàng hơn. Ideone khá nhất quán cho thấy thứ tự ngược lại nhanh hơn, mặc dù không nhiều. Trên một số lần chạy nhất định thậm chí điều này đôi khi lật. Tôi muốn nói rằng kết quả là không kết luận. coliru báo cáo không có sự khác biệt thực sự là tốt. Tôi có thể kiểm tra CPU Exynos5422 trên odroid xu4 của mình sau này.

Có điều là các CPU hiện đại có các bộ dự báo nhánh. Có rất nhiều logic dành riêng cho việc tìm nạp trước cả dữ liệu và hướng dẫn, và CPU x86 hiện đại khá thông minh, khi nói đến điều này. Một số kiến ​​trúc mỏng hơn như ARM hoặc GPU có thể dễ bị tổn thương bởi điều này. Nhưng nó thực sự phụ thuộc rất nhiều vào cả trình biên dịch và hệ thống đích.

Tôi muốn nói rằng tối ưu hóa đặt hàng chi nhánh là khá mong manh và phù du. Làm điều đó chỉ như một số bước thực sự tinh chỉnh.

Mã số:

#include <chrono>
#include <iostream>
#include <random>
#include <algorithm>
#include <iterator>
#include <functional>

using namespace std;

int main()
{
    //Generate a vector of random integers from 1 to 100
    random_device rnd_device;
    mt19937 rnd_engine(rnd_device());
    uniform_int_distribution<int> rnd_dist(1, 100);
    auto gen = std::bind(rnd_dist, rnd_engine);
    vector<int> rand_vec(5000);
    generate(begin(rand_vec), end(rand_vec), gen);
    volatile int nLow, nMid, nHigh;

    //Count the number of values in each of three different ranges
    //Run the test a few times
    for (int n = 0; n != 10; ++n) {

        //Run the test again, but now sort the conditional statements in reverse-order of likelyhood
        {
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 95) ++nHigh;               //Least likely branch
              else if (i < 20) ++nLow;
              else if (i >= 20 && i < 95) ++nMid; //Most likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Reverse-sorted: \t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }

        {
          //Sort the conditional statements in order of likelyhood
          nLow = nMid = nHigh = 0;
          auto start = chrono::high_resolution_clock::now();
          for (int& i : rand_vec) {
              if (i >= 20 && i < 95) ++nMid;  //Most likely branch
              else if (i < 20) ++nLow;
              else if (i >= 95) ++nHigh;      //Least likely branch
          }
          auto end = chrono::high_resolution_clock::now();
          cout << "Sorted:\t\t\t" << chrono::duration_cast<chrono::nanoseconds>(end-start).count() << "ns" << endl;
        }
        cout << endl;
    }
}

Tôi nhận được sự khác biệt ~ 30% về hiệu suất khi tôi chuyển đổi thứ tự của các khối if được sắp xếp và sắp xếp ngược, như đã được thực hiện trong mã của bạn. Tôi không chắc tại sao Ideone và coliru không cho thấy sự khác biệt.
Carlton

Chắc chắn là thú vị. Tôi sẽ cố gắng để có được một số dữ liệu cho các hệ thống khác, nhưng có thể mất đến ngày cho đến khi tôi phải chơi xung quanh nó. Câu hỏi rất thú vị, đặc biệt là trong kết quả của bạn, nhưng chúng rất ngoạn mục tôi đã phải kiểm tra chéo nó.
luk32

Nếu câu hỏi là hiệu ứng là gì? Câu trả lời không thể là Không !
PJTraill

Vâng Nhưng tôi không nhận được thông báo cập nhật cho câu hỏi ban đầu. Họ đã đưa ra câu trả lời công thức lỗi thời. Lấy làm tiếc. Tôi sẽ chỉnh sửa nội dung sau, để chỉ ra nó trả lời câu hỏi ban đầu và hiển thị một số kết quả đã chứng minh điểm gốc.
luk32

Điều này đáng để lặp lại: "Theo mặc định đi bằng khả năng đọc." Viết mã có thể đọc được thường sẽ giúp bạn có kết quả tốt hơn so với việc cố gắng tăng cường hiệu suất nhỏ (về mặt tuyệt đối) bằng cách làm cho mã của bạn khó phân tích hơn.
Andrew Brēza

26

Chỉ cần 5 xu của tôi. Có vẻ như hiệu quả của việc đặt hàng nếu các câu lệnh nên phụ thuộc vào:

  1. Xác suất của mỗi câu lệnh if.

  2. Số lần lặp, do đó, bộ dự đoán nhánh có thể khởi động.

  3. Gợi ý trình biên dịch có khả năng / không có khả năng, tức là bố trí mã.

Để khám phá những yếu tố đó, tôi đã điểm chuẩn các chức năng sau:

Ra lệnh_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] < check_point) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] == check_point) // very unlikely
        s += 1;
}

đảo ngược_ifs ()

for (i = 0; i < data_sz * 1024; i++) {
    if (data[i] == check_point) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (data[i] < check_point) // highly likely
        s += 3;
}

Ra lệnh_ifs_with_hint ()

for (i = 0; i < data_sz * 1024; i++) {
    if (likely(data[i] < check_point)) // highly likely
        s += 3;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
}

Reverseed_ifs_with_hints ()

for (i = 0; i < data_sz * 1024; i++) {
    if (unlikely(data[i] == check_point)) // very unlikely
        s += 1;
    else if (data[i] > check_point) // samewhat likely
        s += 2;
    else if (likely(data[i] < check_point)) // highly likely
        s += 3;
}

dữ liệu

Mảng dữ liệu chứa các số ngẫu nhiên trong khoảng từ 0 đến 100:

const int RANGE_MAX = 100;
uint8_t data[DATA_MAX * 1024];

static void data_init(int data_sz)
{
    int i;
        srand(0);
    for (i = 0; i < data_sz * 1024; i++)
        data[i] = rand() % RANGE_MAX;
}

Kết quả

Các kết quả sau đây dành cho Intel i5 @ 3,2 GHz và G ++ 6.3.0. Đối số đầu tiên là check_point (nghĩa là xác suất tính theo %% cho câu lệnh if rất có khả năng), đối số thứ hai là data_sz (tức là số lần lặp).

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/75/4                    4326 ns       4325 ns     162613
ordered_ifs/75/8                   18242 ns      18242 ns      37931
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612
reversed_ifs/50/4                   5342 ns       5341 ns     126800
reversed_ifs/50/8                  26050 ns      26050 ns      26894
reversed_ifs/75/4                   3616 ns       3616 ns     193130
reversed_ifs/75/8                  15697 ns      15696 ns      44618
reversed_ifs/100/4                  3738 ns       3738 ns     188087
reversed_ifs/100/8                  7476 ns       7476 ns      93752
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/75/4         3165 ns       3165 ns     218492
ordered_ifs_with_hints/75/8        13785 ns      13785 ns      50574
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205
reversed_ifs_with_hints/50/4        6573 ns       6572 ns     105629
reversed_ifs_with_hints/50/8       27351 ns      27351 ns      25568
reversed_ifs_with_hints/75/4        3537 ns       3537 ns     197470
reversed_ifs_with_hints/75/8       16130 ns      16130 ns      43279
reversed_ifs_with_hints/100/4       3737 ns       3737 ns     187583
reversed_ifs_with_hints/100/8       7446 ns       7446 ns      93782

Phân tích

1. Việc đặt hàng không thành vấn đề

Đối với các lần lặp 4K và (gần như) xác suất 100% của câu lệnh được yêu thích cao, sự khác biệt là rất lớn 223%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
reversed_ifs/100/4                  3738 ns       3738 ns     188087

Đối với các lần lặp 4K và xác suất 50% của tuyên bố rất thích, sự khác biệt là khoảng 14%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
reversed_ifs/50/4                   5342 ns       5341 ns     126800

2. Số lần lặp có vấn đề

Sự khác biệt giữa các lần lặp 4K và 8K cho (gần như) xác suất 100% của tuyên bố rất thích là khoảng hai lần (như mong đợi):

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs/100/8                   3381 ns       3381 ns     207612

Nhưng sự khác biệt giữa các lần lặp 4K và 8K cho xác suất 50% của câu lệnh được yêu thích cao là 5,5 lần:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/50/8                   25636 ns      25635 ns      27852

Tại sao lại như vậy? Bởi vì dự đoán chi nhánh bỏ lỡ. Đây là chi nhánh bỏ lỡ cho mỗi trường hợp nêu trên:

ordered_ifs/100/4    0.01% of branch-misses
ordered_ifs/100/8    0.01% of branch-misses
ordered_ifs/50/4     3.18% of branch-misses
ordered_ifs/50/8     15.22% of branch-misses

Vì vậy, trên i5 của tôi, bộ dự đoán nhánh thất bại một cách ngoạn mục đối với các nhánh không có khả năng và các tập dữ liệu lớn.

3. Gợi ý giúp Bit

Đối với các lần lặp 4K, kết quả có phần tệ hơn với xác suất 50% và tốt hơn một chút với xác suất gần 100%:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/4                    4660 ns       4658 ns     150948
ordered_ifs/100/4                   1673 ns       1673 ns     417073
ordered_ifs_with_hints/50/4         5551 ns       5551 ns     125160
ordered_ifs_with_hints/100/4        1575 ns       1575 ns     437687

Nhưng đối với các lần lặp 8K, kết quả luôn tốt hơn một chút:

---------------------------------------------------------------------
Benchmark                              Time           CPU Iterations
---------------------------------------------------------------------
ordered_ifs/50/8                   25636 ns      25635 ns      27852
ordered_ifs/100/8                   3381 ns       3381 ns     207612
ordered_ifs_with_hints/50/8        23191 ns      23190 ns      30028
ordered_ifs_with_hints/100/8        3130 ns       3130 ns     221205

Vì vậy, các gợi ý cũng có ích, nhưng chỉ một chút xíu.

Kết luận chung là: luôn luôn điểm chuẩn mã, vì kết quả có thể gây bất ngờ.

Mong rằng sẽ giúp.


1
i5 Nehalem? i5 Skylake? Chỉ nói "i5" là không cụ thể. Ngoài ra, tôi giả sử bạn đã sử dụng g++ -O2hoặc -O3 -fno-tree-vectorize, nhưng bạn nên nói như vậy.
Peter Cordes

Điều thú vị là with_hint vẫn khác với thứ tự so với đảo ngược. Sẽ tốt hơn nếu bạn liên kết với nguồn ở đâu đó. (ví dụ: liên kết Godbolt, tốt nhất là liên kết đầy đủ để rút ngắn liên kết không thể thối rữa.)
Peter Cordes

1
Việc người dự đoán chi nhánh có thể dự đoán tốt ngay cả ở kích thước dữ liệu đầu vào 4K, nghĩa là có thể "phá vỡ" điểm chuẩn bằng cách ghi nhớ kết quả của nhánh qua một vòng trong hàng nghìn là một minh chứng cho sức mạnh của hiện đại dự báo nhánh. Hãy nhớ rằng các yếu tố dự đoán khá nhạy cảm trong một số trường hợp đối với những thứ như căn chỉnh, vì vậy thật khó để đưa ra kết luận mạnh mẽ về một số thay đổi. Ví dụ, bạn nhận thấy hành vi ngược lại cho gợi ý trong các trường hợp khác nhau nhưng điều đó có thể được giải thích bằng cách gợi ý thay đổi bố cục mã ngẫu nhiên đã ảnh hưởng đến công cụ dự đoán.
BeeOnRope

1
@PeterCordes điểm chính của tôi là trong khi chúng ta có thể cố gắng dự đoán kết quả của một thay đổi, chúng ta vẫn đo lường hiệu suất tốt hơn trước và sau khi thay đổi ... Và bạn đã đúng, tôi nên đề cập rằng nó đã được tối ưu hóa với -O3 và bộ xử lý là i5-4460 @ 3,20GHz
Andriy Berestovskyy

19

Dựa trên một số câu trả lời khác ở đây, có vẻ như câu trả lời thực sự duy nhất là: nó phụ thuộc . Nó phụ thuộc vào ít nhất những điều sau đây (mặc dù không nhất thiết phải theo thứ tự quan trọng này):

  • Xác suất tương đối của từng chi nhánh. Đây là câu hỏi ban đầu đã được hỏi. Dựa trên các câu trả lời hiện có, dường như có một số điều kiện theo đó việc đặt hàng theo xác suất sẽ giúp ích, nhưng dường như không phải lúc nào cũng như vậy. Nếu xác suất tương đối không khác nhau nhiều, thì không có khả năng tạo ra bất kỳ sự khác biệt nào theo thứ tự chúng. Tuy nhiên, nếu điều kiện đầu tiên xảy ra 99,999% thời gian và điều tiếp theo là một phần nhỏ của những gì còn lại, thì tôi sẽ giả định rằng việc đặt nhiều khả năng đầu tiên sẽ có lợi về mặt thời gian.
  • Chi phí tính toán điều kiện đúng / sai cho mỗi chi nhánh. Nếu chi phí thời gian của việc kiểm tra các điều kiện thực sự cao đối với một chi nhánh so với chi nhánh khác, thì điều này có thể có tác động đáng kể đến thời gian và hiệu quả. Ví dụ: xem xét một điều kiện cần 1 đơn vị thời gian để tính toán (ví dụ: kiểm tra trạng thái của biến Boolean) so với điều kiện khác cần hàng chục, hàng trăm, hàng nghìn hoặc thậm chí hàng triệu đơn vị thời gian để tính toán (ví dụ: kiểm tra nội dung của một tệp trên đĩa hoặc thực hiện một truy vấn SQL phức tạp đối với cơ sở dữ liệu lớn). Giả sử mã kiểm tra các điều kiện theo thứ tự mỗi lần, các điều kiện nhanh hơn có lẽ phải là đầu tiên (trừ khi chúng phụ thuộc vào các điều kiện khác không thành công trước).
  • Trình biên dịch / Trình thông dịch Một số trình biên dịch (hoặc trình thông dịch) có thể bao gồm tối ưu hóa một loại khác có thể ảnh hưởng đến hiệu suất (và một số trong số này chỉ xuất hiện nếu một số tùy chọn nhất định được chọn trong quá trình biên dịch và / hoặc thực thi). Vì vậy, trừ khi bạn đang đo điểm chuẩn cho hai phần tổng hợp và thực thi mã giống hệt nhau trên cùng một hệ thống bằng cùng một trình biên dịch, trong đó điểm khác biệt duy nhất là thứ tự của các nhánh trong câu hỏi, bạn sẽ phải đưa ra một số độ trễ cho các biến thể của trình biên dịch.
  • Hệ điều hành / Phần cứng Như được đề cập bởi luk32 và Yakk, các CPU khác nhau có tối ưu hóa riêng (cũng như các hệ điều hành). Vì vậy, điểm chuẩn một lần nữa dễ bị biến đổi ở đây.
  • Tần suất thực thi khối mã Nếu khối bao gồm các nhánh hiếm khi được truy cập (ví dụ: chỉ một lần trong khi khởi động), thì có lẽ vấn đề rất nhỏ là bạn đặt thứ tự các nhánh. Mặt khác, nếu mã của bạn bị chặn ở khối mã này trong phần quan trọng của mã, thì việc đặt hàng có thể rất quan trọng (tùy thuộc vào điểm chuẩn).

Cách duy nhất để biết chắc chắn là đánh giá trường hợp cụ thể của bạn, tốt nhất là trên một hệ thống giống hệt (hoặc rất giống với) hệ thống dự định mà mã cuối cùng sẽ chạy. Nếu nó được dự định để chạy trên một tập hợp các hệ thống khác nhau với phần cứng, hệ điều hành khác nhau, v.v., thì đó là một ý tưởng tốt để điểm chuẩn qua nhiều biến thể để xem cái nào là tốt nhất. Nó thậm chí có thể là một ý tưởng tốt để mã được biên dịch với một thứ tự trên một loại hệ thống và một thứ tự khác trên một loại hệ thống khác.

Quy tắc cá nhân của tôi (đối với hầu hết các trường hợp, trong trường hợp không có điểm chuẩn) là đặt hàng dựa trên:

  1. Điều kiện dựa trên kết quả của điều kiện trước,
  2. Chi phí tính toán điều kiện, sau đó
  3. Xác suất tương đối của từng chi nhánh.

13

Cách tôi thường thấy điều này được giải quyết cho mã hiệu suất cao là giữ thứ tự dễ đọc nhất, nhưng cung cấp gợi ý cho trình biên dịch. Đây là một ví dụ từ kernel Linux :

if (likely(access_ok(VERIFY_READ, from, n))) {
    kasan_check_write(to, n);
    res = raw_copy_from_user(to, from, n);
}
if (unlikely(res))
    memset(to + (n - res), 0, res);

Ở đây, giả định là kiểm tra truy cập sẽ vượt qua và không có lỗi nào được trả lại res. Cố gắng sắp xếp lại một trong hai điều này nếu các mệnh đề chỉ gây nhầm lẫn mã, nhưng macro likely()unlikely()thực sự giúp dễ đọc bằng cách chỉ ra trường hợp bình thường và trường hợp ngoại lệ là gì.

Việc triển khai Linux của các macro đó sử dụng các tính năng cụ thể của GCC . Có vẻ như trình biên dịch clang và Intel C hỗ trợ cùng một cú pháp, nhưng MSVC không có tính năng như vậy .


4
Điều này sẽ hữu ích hơn nếu bạn có thể giải thích cách xác định likely()unlikely()macro và bao gồm một số thông tin về tính năng trình biên dịch tương ứng.
Nate Eldredge

1
AFAIK, những gợi ý này "chỉ" thay đổi bố cục bộ nhớ của các khối mã và việc có hay không sẽ dẫn đến bước nhảy. Điều này có thể có lợi thế về hiệu suất, ví dụ như nhu cầu (hoặc thiếu) để đọc các trang bộ nhớ. Nhưng điều này không sắp xếp lại thứ tự các điều kiện trong một danh sách dài các if-if khác được đánh giá
Hagen von Eitzen

@HagenvonEitzen Hmm yeah, đó là một điểm tốt, nó không thể ảnh hưởng đến thứ tự else ifnếu trình biên dịch không đủ thông minh để biết rằng các điều kiện là loại trừ lẫn nhau.
jpa

7

Cũng phụ thuộc vào trình biên dịch của bạn và nền tảng bạn đang biên dịch.

Về lý thuyết, điều kiện rất có thể sẽ làm cho điều khiển nhảy càng ít càng tốt.

Thông thường, điều kiện có khả năng nhất phải là đầu tiên:

if (most_likely) {
     // most likely instructions
} else 

Các asm phổ biến nhất dựa trên các nhánh có điều kiện nhảy khi điều kiện là đúng . Mã C đó sẽ có khả năng được dịch sang mã giả như vậy:

jump to ELSE if not(most_likely)
// most likely instructions
jump to end
ELSE:

Điều này là do các bước nhảy làm cho cpu hủy bỏ đường ống thực thi và bị đình trệ vì bộ đếm chương trình đã thay đổi (đối với các kiến ​​trúc hỗ trợ các đường ống thực sự phổ biến). Sau đó là về trình biên dịch, có thể hoặc không thể áp dụng một số tối ưu hóa tinh vi về việc có điều kiện có lẽ nhất về mặt thống kê để có được điều khiển thực hiện ít bước nhảy hơn.


2
Bạn đã nói rằng nhánh có điều kiện xảy ra khi điều kiện là đúng, nhưng ví dụ "giả asm" cho thấy điều ngược lại. Ngoài ra, không thể nói rằng các bước nhảy có điều kiện (ít hơn tất cả các lần nhảy) làm tắc nghẽn đường ống vì các CPU hiện đại thường có dự đoán nhánh. Trong thực tế, nếu chi nhánh được dự đoán sẽ được thực hiện nhưng sau đó không được thực hiện, đường ống sẽ bị đình trệ. Tôi vẫn muốn cố gắng sắp xếp các điều kiện theo thứ tự giảm dần khả năng, nhưng những gì các trình biên dịch và CPU làm của nó là cao thực hiện phụ thuộc.
Arne Vogel

1
Tôi đặt không phải (hầu hết các khả năng), vì vậy nếu hầu hết là đúng thì sự kiểm soát sẽ tiếp tục mà không cần nhảy.
NoImaginationGuy

1
"Các asm phổ biến nhất dựa trên các nhánh có điều kiện nhảy khi điều kiện là đúng" .. ISAs đó sẽ là gì? Điều đó chắc chắn không đúng với x86 và ARM. Địa ngục cho các CPU ARM cơ bản (và các x86 rất cổ xưa, ngay cả đối với các bps phức tạp, chúng vẫn thường bắt đầu với giả định đó và sau đó thích nghi), bộ dự đoán nhánh giả định rằng một nhánh chuyển tiếp không được lấy và các nhánh ngược luôn luôn, vì vậy ngược lại với tuyên bố là đúng.
Voo

1
Các trình biên dịch tôi đã thử hầu hết đều sử dụng cách tiếp cận mà tôi đã đề cập ở trên cho một thử nghiệm đơn giản. Lưu ý rằng clangthực sự đã sử dụng một cách tiếp cận khác cho test2test3: vì các phương pháp phỏng đoán chỉ ra rằng một < 0hoặc == 0kiểm tra có khả năng là sai, nó đã quyết định sao chép phần còn lại của hàm trên cả hai đường dẫn, do đó nó có thể khiến condition == falseđường đi qua. Điều này chỉ khả thi vì phần còn lại của hàm là ngắn: trong test4tôi đã thêm một thao tác nữa và nó trở lại cách tiếp cận tôi đã nêu ở trên.
BeeOnRope

1
@ArneVogel - các nhánh được dự đoán chính xác không hoàn toàn làm tắc nghẽn đường ống trên các CPU hiện đại nhưng chúng vẫn thường tệ hơn đáng kể so với không được thực hiện: (1) chúng có nghĩa là luồng điều khiển không liền kề nên các hướng dẫn còn lại sau khi jmpkhông hữu ích vì vậy, việc tìm nạp / giải mã băng thông bị lãng phí (2) ngay cả khi dự đoán các lõi lớn hiện đại chỉ thực hiện một lần tìm nạp trên mỗi chu kỳ nên nó đặt giới hạn cứng là 1 nhánh / chu kỳ (Intel hiện đại OTOH có thể thực hiện 2 lần không lấy / chu kỳ) (3 ) khó dự đoán chi nhánh hơn để đối phó với các nhánh liên tiếp và trong trường hợp dự đoán nhanh + chậm ...
BeeOnRope

6

Tôi quyết định chạy lại bài kiểm tra trên máy của mình bằng mã Lik32. Tôi đã phải thay đổi nó do các cửa sổ hoặc trình biên dịch của tôi nghĩ rằng độ phân giải cao là 1ms, sử dụng

mingw32-g ++. exe -O3 -Wall -std = c ++ 11 -fexceptions -g

vector<int> rand_vec(10000000);

GCC đã thực hiện chuyển đổi tương tự trên cả hai mã gốc.

Lưu ý rằng chỉ có hai điều kiện đầu tiên được kiểm tra là điều kiện thứ ba phải luôn luôn đúng, GCC là một loại Sherlock ở đây.

Đảo ngược

.L233:
        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L219
.L293:
        mov     edx, DWORD PTR [rsp+104]
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
.L217:
        add     rax, 4
        cmp     r14, rax
        je      .L292
.L219:
        mov     edx, DWORD PTR [rax]
        cmp     edx, 94
        jg      .L293 // >= 95
        cmp     edx, 19
        jg      .L218 // >= 20
        mov     edx, DWORD PTR [rsp+96]
        add     rax, 4
        add     edx, 1 // < 20 Sherlock
        mov     DWORD PTR [rsp+96], edx
        cmp     r14, rax
        jne     .L219
.L292:
        call    std::chrono::_V2::system_clock::now()

.L218: // further down
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
        jmp     .L217

And sorted

        mov     DWORD PTR [rsp+104], 0
        mov     DWORD PTR [rsp+100], 0
        mov     DWORD PTR [rsp+96], 0
        call    std::chrono::_V2::system_clock::now()
        mov     rbp, rax
        mov     rax, QWORD PTR [rsp+8]
        jmp     .L226
.L296:
        mov     edx, DWORD PTR [rsp+100]
        add     edx, 1
        mov     DWORD PTR [rsp+100], edx
.L224:
        add     rax, 4
        cmp     r14, rax
        je      .L295
.L226:
        mov     edx, DWORD PTR [rax]
        lea     ecx, [rdx-20]
        cmp     ecx, 74
        jbe     .L296
        cmp     edx, 19
        jle     .L297
        mov     edx, DWORD PTR [rsp+104]
        add     rax, 4
        add     edx, 1
        mov     DWORD PTR [rsp+104], edx
        cmp     r14, rax
        jne     .L226
.L295:
        call    std::chrono::_V2::system_clock::now()

.L297: // further down
        mov     edx, DWORD PTR [rsp+96]
        add     edx, 1
        mov     DWORD PTR [rsp+96], edx
        jmp     .L224

Vì vậy, điều này không cho chúng ta biết nhiều ngoại trừ trường hợp cuối cùng không cần dự đoán chi nhánh.

Bây giờ tôi đã thử tất cả 6 kết hợp của if, top 2 là đảo ngược ban đầu và được sắp xếp. cao là> = 95, thấp là <20, giữa là 20-94 với 10000000 lần lặp mỗi lần.

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 44000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 46000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 43000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 48000000ns
high, mid, low: 44000000ns
low, mid, high: 44000000ns
mid, high, low: 45000000ns
low, high, mid: 45000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 47000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 46000000ns
low, high, mid: 44000000ns

high, low, mid: 43000000ns
mid, low, high: 46000000ns
high, mid, low: 45000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

high, low, mid: 42000000ns
mid, low, high: 46000000ns
high, mid, low: 44000000ns
low, mid, high: 45000000ns
mid, high, low: 45000000ns
low, high, mid: 44000000ns

1900020, 7498968, 601012

Process returned 0 (0x0)   execution time : 2.899 s
Press any key to continue.

Vậy tại sao thứ tự cao, thấp, med lại nhanh hơn (biên)

Bởi vì điều khó lường nhất là cuối cùng và do đó không bao giờ được chạy qua một công cụ dự đoán nhánh.

          if (i >= 95) ++nHigh;               // most predictable with 94% taken
          else if (i < 20) ++nLow; // (94-19)/94% taken ~80% taken
          else if (i >= 20 && i < 95) ++nMid; // never taken as this is the remainder of the outfalls.

Vì vậy, các nhánh sẽ được dự đoán lấy, lấy và phần còn lại với

6% + (0,94 *) 20% sai.

"Sắp xếp"

          if (i >= 20 && i < 95) ++nMid;  // 75% not taken
          else if (i < 20) ++nLow;        // 19/25 76% not taken
          else if (i >= 95) ++nHigh;      //Least likely branch

Các nhánh sẽ được dự đoán với không lấy, không lấy và Sherlock.

25% + (0,75 *) 24% dự đoán sai

Cho chênh lệch 18-23% (đo chênh lệch ~ 9%) nhưng chúng ta cần tính chu kỳ thay vì dự đoán sai%.

Giả sử 17 chu kỳ bị phạt dự đoán sai đối với CPU Nehalem của tôi và mỗi lần kiểm tra cần 1 chu kỳ để ban hành (4-5 hướng dẫn) và vòng lặp cũng mất một chu kỳ. Các phụ thuộc dữ liệu là các bộ đếm và các biến vòng lặp, nhưng một khi các thông tin sai lệch sẽ không ảnh hưởng đến thời gian.

Vì vậy, để "đảo ngược", chúng ta có được thời gian (đây phải là công thức được sử dụng trong Kiến trúc máy tính: Phương pháp định lượng IIRC).

mispredict*penalty+count+loop
0.06*17+1+1+    (=3.02)
(propability)*(first check+mispredict*penalty+count+loop)
(0.19)*(1+0.20*17+1+1)+  (= 0.19*6.4=1.22)
(propability)*(first check+second check+count+loop)
(0.75)*(1+1+1+1) (=3)
= 7.24 cycles per iteration

và tương tự cho "sắp xếp"

0.25*17+1+1+ (=6.25)
(1-0.75)*(1+0.24*17+1+1)+ (=.25*7.08=1.77)
(1-0.75-0.19)*(1+1+1+1)  (= 0.06*4=0.24)
= 8.26

(8,26-7,24) / 8,26 = 13,8% so với ~ 9% được đo (gần với số đo!?!).

Vì vậy, sự rõ ràng của OP là không rõ ràng.

Với các thử nghiệm này, các thử nghiệm khác với mã phức tạp hơn hoặc phụ thuộc dữ liệu nhiều hơn chắc chắn sẽ khác nhau vì vậy hãy đo trường hợp của bạn.

Việc thay đổi thứ tự kiểm tra đã thay đổi kết quả nhưng điều đó có thể là do sự sắp xếp khác nhau của bắt đầu vòng lặp, lý tưởng nhất là 16 byte được căn chỉnh trên tất cả các CPU Intel mới hơn nhưng không phải trong trường hợp này.


4

Đặt chúng theo bất cứ thứ tự logic nào bạn thích. Chắc chắn, chi nhánh có thể chậm hơn, nhưng việc phân nhánh không phải là phần lớn công việc mà máy tính của bạn đang làm.

Nếu bạn đang làm việc với một phần quan trọng về hiệu năng của mã, thì chắc chắn sử dụng thứ tự logic, tối ưu hóa hướng dẫn hồ sơ và các kỹ thuật khác, nhưng đối với mã chung, tôi nghĩ rằng nó thực sự là một lựa chọn phong cách hơn.


6
Thất bại dự đoán chi nhánh là đắt tiền. Trong microbenchmarks, họ thuộc dự trù kinh phí , bởi vì x86s có một bảng lớn các nhân tố ảnh chi nhánh. Một vòng lặp chặt chẽ trong cùng điều kiện dẫn đến CPU hiểu rõ hơn bạn làm cái nào có khả năng nhất. Nhưng nếu bạn có các nhánh trên toàn bộ mã của mình, bạn có thể để bộ đệm dự đoán nhánh của bạn hết chỗ và cpu giả định bất cứ điều gì là mặc định. Biết những gì đoán mặc định là có thể lưu chu kỳ trên cơ sở mã của bạn.
Yakk - Adam Nevraumont

Câu trả lời của @Yakk Jack là câu trả lời đúng duy nhất ở đây. Không thực hiện tối ưu hóa làm giảm khả năng đọc nếu trình biên dịch của bạn có thể thực hiện tối ưu hóa đó. Bạn sẽ không thực hiện việc gấp liên tục, loại bỏ mã chết, hủy đăng ký vòng lặp hoặc bất kỳ tối ưu hóa nào khác nếu trình biên dịch của bạn làm điều đó cho bạn, phải không? Viết mã của bạn, sử dụng tối ưu hóa hướng dẫn hồ sơ (được thiết kế để giải quyết vấn đề này bởi vì các lập trình viên không đoán được) và sau đó xem trình biên dịch của bạn có tối ưu hóa nó hay không. Cuối cùng, bạn không muốn có bất kỳ chi nhánh nào trong mã quan trọng về hiệu suất.
Christoph Diegelmann

@Christoph Tôi sẽ không bao gồm mã tôi biết là đã chết. Tôi sẽ không sử dụng i++khi nào ++isẽ làm, bởi vì tôi biết rằng i++đối với một số trình vòng lặp rất khó để tối ưu hóa ++ivà sự khác biệt (đối với tôi) không thành vấn đề. Đây là về việc tránh bi quan; Đặt khối có khả năng đầu tiên là thói quen mặc định sẽ không làm giảm khả năng đọc đáng chú ý (và thực sự có thể giúp!), trong khi dẫn đến mã thân thiện với dự đoán chi nhánh (và do đó mang lại cho bạn mức tăng hiệu suất nhỏ đồng nhất không thể lấy lại được bằng cách tối ưu hóa vi mô sau này)
Yakk - Adam Nevraumont

3

Nếu bạn đã biết xác suất tương đối của câu lệnh if-other, thì với mục đích hiệu suất, tốt hơn là sử dụng cách đã sắp xếp, vì nó sẽ chỉ kiểm tra một điều kiện (điều kiện đúng).

Trong một cách chưa được sắp xếp, trình biên dịch sẽ kiểm tra tất cả các điều kiện không cần thiết và sẽ mất thời gian.

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.