อะไรคือผลของการสั่งซื้อถ้า ... หากงบโดยความน่าจะเป็น


187

โดยเฉพาะอย่างยิ่งถ้าฉันมีชุดคำสั่งif... else ifและฉันก็รู้ว่าความน่าจะเป็นแบบสัมพัทธ์ที่แต่ละคำสั่งจะประเมินเป็นtrueเท่าใดความแตกต่างของเวลาการดำเนินการจึงทำให้เรียงลำดับตามความน่าจะเป็นเท่าใด ตัวอย่างเช่นฉันควรจะชอบสิ่งนี้:

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

สำหรับสิ่งนี้?:

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

ดูเหมือนว่ารุ่นที่เรียงจะเร็วกว่าอย่างไรก็ตามเพื่อความสะดวกในการอ่านหรือการมีผลข้างเคียงเราอาจต้องการเรียงลำดับแบบไม่เหมาะสม เป็นการยากที่จะบอกได้ว่า CPU จะทำอย่างไรกับการคาดคะเนสาขาจนกว่าคุณจะเรียกใช้รหัสจริง

ดังนั้นในการทดลองกับสิ่งนี้ฉันลงเอยด้วยการตอบคำถามของฉันเองสำหรับกรณีเฉพาะ แต่ฉันต้องการฟังความคิดเห็น / ข้อมูลเชิงลึกอื่น ๆ ด้วย

สำคัญ: คำถามนี้สันนิษฐานว่าifคำสั่งนั้นสามารถเรียงลำดับใหม่โดยพลการโดยไม่มีผลกระทบใด ๆ ต่อพฤติกรรมของโปรแกรม ในคำตอบของฉันการทดสอบตามเงื่อนไขสามข้อนั้นไม่เหมือนกันและไม่มีผลข้างเคียง แน่นอนถ้างบต้องได้รับการประเมินในลำดับที่แน่นอนเพื่อให้บรรลุพฤติกรรมที่ต้องการบางอย่างแล้วปัญหาของประสิทธิภาพคือ moot


35
คุณอาจต้องการเพิ่มบันทึกย่อว่าเงื่อนไขนั้นไม่สามารถเกิดขึ้นพร้อมกันได้มิฉะนั้นทั้งสองเวอร์ชันจะไม่เทียบเท่า
idclev 463035818

28
มันน่าสนใจทีเดียวที่คำถามที่ตอบตัวเองมีผู้โหวตมากกว่า 20 คนพร้อมคำตอบที่ไม่ดีในหนึ่งชั่วโมง อย่าเรียกอะไรเลยใน OP แต่ควรระวังการกระโดดบนรถเกวียน คำถามอาจจะน่าสนใจ แต่ผลลัพธ์ไม่แน่ใจ
luk32

3
ฉันเชื่อว่าสิ่งนี้สามารถอธิบายได้ว่าเป็นรูปแบบหนึ่งของการประเมินการลัดวงจรเพราะการกดปุ่มการเปรียบเทียบหนึ่งปฏิเสธปฏิเสธการเปรียบเทียบที่แตกต่างกัน ฉันชอบการดำเนินการแบบนี้เป็นการส่วนตัวเมื่อเปรียบเทียบอย่างรวดเร็วหนึ่งครั้งบูลีนสามารถป้องกันไม่ให้ฉันไปเปรียบเทียบที่แตกต่างกันซึ่งอาจเกี่ยวข้องกับการจัดการสตริงที่หนักหน่วงทรัพยากร regex หรือการโต้ตอบของฐานข้อมูล
MonkeyZeus

11
คอมไพเลอร์บางตัวเสนอความสามารถในการรวบรวมสถิติเกี่ยวกับสาขาและฟีดเหล่านี้กลับเข้าไปในคอมไพเลอร์เพื่อให้สามารถทำการปรับแต่งได้ดีขึ้น

11
หากประสิทธิภาพเช่นนี้สำหรับคุณคุณควรลองใช้ Profile Guided Optimization และเปรียบเทียบผลลัพธ์ด้วยตนเองกับผลลัพธ์ของคอมไพเลอร์
Justin

คำตอบ:


96

ตามกฎทั่วไปส่วนใหญ่หากไม่ใช่ซีพียู Intel ทั้งหมดจะถือว่าสาขาย่อยไปข้างหน้านั้นไม่ได้ถูกนำมาใช้ในครั้งแรกที่เห็น ดูการทำงานของ Godbolt

หลังจากนั้นสาขาจะเข้าสู่แคชการทำนายสาขาและพฤติกรรมที่ผ่านมาจะถูกใช้เพื่อแจ้งการทำนายสาขาในอนาคต

ดังนั้นในการวนรอบที่แน่นหนาผลของการลงมือทำก็จะค่อนข้างเล็ก ตัวทำนายสาขากำลังจะเรียนรู้ว่าชุดสาขาใดน่าจะเป็นไปได้มากที่สุดและหากคุณมีจำนวนงานที่ไม่ยุ่งยากในวงวนความแตกต่างเล็ก ๆ น้อย ๆ จะไม่เพิ่มขึ้นมากนัก

โดยทั่วไปรหัสคอมไพเลอร์ส่วนใหญ่จะไม่มีการสั่งเหตุผลอื่น ๆ เพื่อสั่งให้รหัสเครื่องที่ผลิตนั้นเป็นแบบที่คุณสั่งในรหัสของคุณ ดังนั้นหากข้อความเป็นไปข้างหน้าสาขาเมื่อพวกเขาล้มเหลว

ดังนั้นคุณควรสั่งสาขาของคุณเพื่อลดโอกาสในการได้รับการทำนายสาขาที่ดีที่สุดจาก "การเผชิญหน้าครั้งแรก"

เครื่องหมายขนาดเล็กที่วนซ้ำหลาย ๆ ครั้งในสภาวะที่กำหนดและทำงานที่ไม่สำคัญจะถูกครอบงำด้วยเอฟเฟกต์เล็ก ๆ น้อย ๆ ของการนับคำสั่งและสิ่งที่คล้ายกัน ดังนั้นในกรณีนี้คุณจะต้องโพรไฟล์เนื่องจากกฎของหัวแม่มือจะไม่น่าเชื่อถือ

ยิ่งไปกว่านั้น vectorization และการเพิ่มประสิทธิภาพอื่น ๆ ยังนำไปใช้กับลูปขนาดเล็ก

ดังนั้นในรหัสทั่วไปให้ใส่รหัสที่เป็นไปได้มากที่สุดในifบล็อกและนั่นจะส่งผลให้การคาดคะเนสาขาที่ไม่ได้แคชน้อยที่สุดนั้นหายไป ในลูปที่แน่นให้ทำตามกฎทั่วไปเพื่อเริ่มและถ้าคุณต้องการทราบข้อมูลเพิ่มเติมคุณมีทางเลือกน้อย แต่ต้องทำโปรไฟล์

โดยปกติสิ่งนี้จะออกไปนอกหน้าต่างถ้าการทดสอบบางอย่างถูกกว่าการทดสอบอื่น ๆ


19
นอกจากนี้ยังมีมูลค่าการพิจารณาว่าราคาแพงการทดสอบตัวเอง: ถ้าการทดสอบหนึ่งเป็นเพียงเล็กน้อยมีโอกาสมากขึ้น แต่จำนวนมากมีราคาแพงกว่าแล้วมันอาจจะมีมูลค่าการวางทดสอบอื่น ๆ ก่อนเพราะเงินฝากออมทรัพย์จากการไม่ได้ทำให้การทดสอบที่มีราคาแพงมีแนวโน้มที่จะเกินดุล เงินฝากออมทรัพย์จากการทำนายสาขา ฯลฯ
psmears

เชื่อมโยงคุณระบุไม่สนับสนุนข้อสรุปของคุณตามกฎทั่วไปส่วนใหญ่ถ้าไม่ทั้งหมด CPU ของ Intel ถือว่าสาขาไปข้างหน้าไม่ได้ถ่ายเป็นครั้งแรกที่พวกเขาเห็นพวกเขา ในความเป็นจริงนั้นเป็นจริงสำหรับ CPU Arrendale ที่ค่อนข้างคลุมเครือเท่านั้นซึ่งผลลัพธ์จะปรากฏขึ้นก่อน ผลลัพธ์ของ Ivy Bridge และ Haswell ไม่สนับสนุนสิ่งนั้นเลย Haswell ดูใกล้เคียงกับ "ทำนายการตกตลอด" สำหรับสาขาที่มองไม่เห็นและ Ivy Bridge ไม่ชัดเจนเลย
BeeOnRope

โดยทั่วไปเป็นที่เข้าใจกันว่าซีพียูไม่ได้ใช้การคาดการณ์แบบคงที่อย่างที่เคยทำในอดีต อันที่จริง Intel ในปัจจุบันอาจใช้บางอย่างเช่นตัวทำนาย TAGE ที่น่าจะเป็น คุณเพียงแฮชประวัติสาขาในตารางประวัติต่าง ๆ และนำหนึ่งที่ตรงกับประวัติศาสตร์ที่ยาวที่สุด มันใช้ "แท็ก" เพื่อพยายามหลีกเลี่ยงการใช้นามแฝง แต่แท็กมีเพียงไม่กี่บิต หากคุณพลาดความยาวของประวัติศาสตร์การคาดการณ์เริ่มต้นบางอย่างอาจไม่ได้ขึ้นอยู่กับทิศทางของสาขา (ใน Haswell เราสามารถพูดได้ว่ามันไม่ชัดเจน)
BeeOnRope

44

ฉันสร้างการทดสอบต่อไปนี้เพื่อดำเนินการตามเวลาของบล็อกif... สองอันที่แตกต่างกันelse ifหนึ่งอันเรียงตามลำดับความน่าจะเป็นอีกอันเรียงตามลำดับกลับกัน:

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

เมื่อใช้ MSVC2017 กับ / O2 ผลลัพธ์จะแสดงว่ารุ่นที่เรียงนั้นเร็วกว่ารุ่นที่ไม่เรียงลำดับประมาณ 28% ตามความคิดเห็นของ luk32 ฉันได้เปลี่ยนลำดับการทดสอบสองรายการซึ่งสร้างความแตกต่างที่เห็นได้ชัดเจน (22% เทียบกับ 28%) รหัสถูกรันภายใต้ Windows 7 บน Intel Xeon E5-2697 v2 แน่นอนว่าเป็นปัญหาที่เฉพาะเจาะจงมากและไม่ควรตีความว่าเป็นคำตอบสุดท้าย


9
OP ควรระวังแม้ว่าการเปลี่ยนif... else ifคำสั่งอาจมีผลกระทบอย่างมากต่อการไหลของตรรกะผ่านโค้ด การunlikelyตรวจสอบอาจไม่เกิดขึ้นบ่อยครั้ง แต่อาจมีธุรกิจที่ต้องตรวจสอบunlikelyสภาพก่อนที่จะตรวจสอบผู้อื่น
ลุคทีบรูคส์

21
เร็วขึ้น 30%? คุณหมายถึงว่ามันเร็วขึ้นโดยประมาณ% ของการเสริมถ้างบมันไม่ต้องทำ? ดูเหมือนว่าผลลัพธ์ที่สมเหตุสมผล
UKMonkey

5
คุณเปรียบเทียบมันได้อย่างไร คอมไพเลอร์ซีพียูอื่น ๆ ฉันค่อนข้างมั่นใจว่าผลลัพธ์นี้ไม่สามารถพกพาได้
luk32

12
ปัญหาเกี่ยวกับ microbenchmark นี้คือ CPU กำลังจะหาว่าสาขาใดน่าจะเป็นไปได้มากที่สุดและทำการแคชเมื่อคุณวนซ้ำไปเรื่อย ๆ หากสาขาที่ไม่ได้ตรวจสอบในวงแคบขนาดเล็กแคชการทำนายสาขาอาจไม่มีสาขาดังกล่าวและค่าใช้จ่ายอาจสูงกว่านี้มากหาก CPU เดาผิดด้วยคำแนะนำแคชการทำนายสาขาเป็นศูนย์
Yakk - Adam Nevraumont

6
มาตรฐานนี้ไม่น่าเชื่อถือเกินไป การคอมไพล์ด้วยgcc 6.3.0 : g++ -O2 -march=native -std=c++14ให้ขอบเล็กน้อยกับคำสั่งเงื่อนไขที่เรียงลำดับ แต่ส่วนใหญ่แล้วความแตกต่างเปอร์เซ็นต์ระหว่างการรันทั้งสองคือ ~ 5% หลายครั้งมันช้ากว่าจริง ๆ (เนื่องจากความแปรปรวน) ฉันค่อนข้างแน่ใจว่าการสั่งซื้อifของแบบนี้ไม่คุ้มค่าที่จะกังวล PGO อาจจะจัดการกรณีดังกล่าวได้อย่างสมบูรณ์
Justin

30

ไม่คุณไม่ควรเว้นเสียแต่ว่าคุณจะแน่ใจจริงๆว่าระบบเป้าหมายได้รับผลกระทบ โดยค่าเริ่มต้นไปตามความสามารถในการอ่าน

ฉันสงสัยในผลลัพธ์ของคุณอย่างมาก ฉันได้ปรับตัวอย่างเล็กน้อยเพื่อให้การย้อนกลับทำได้ง่ายขึ้น Ideoneแสดงให้เห็นว่าการเรียงลำดับแบบย้อนกลับนั้นเร็วกว่าแม้ว่าจะไม่มากนัก ในบางการวิ่งแม้สิ่งนี้พลิกเป็นครั้งคราว ฉันจะบอกว่าผลสรุปไม่ได้ coliruรายงานว่าไม่แตกต่างกันเช่นกัน ฉันสามารถตรวจสอบ Exynos5422 CPU บน odroid xu4 ของฉันในภายหลัง

สิ่งคือซีพียูสมัยใหม่มีตัวพยากรณ์สาขา มีเหตุผลมากมายที่ทุ่มเทให้กับการดึงข้อมูลและคำแนะนำล่วงหน้าและซีพียู x86 สมัยใหม่ค่อนข้างฉลาดเมื่อมันมาถึงสิ่งนี้ สถาปัตยกรรมที่บางกว่าเช่น ARMs หรือ GPU อาจเสี่ยงต่อสิ่งนี้ แต่มันขึ้นอยู่กับทั้งระบบคอมไพเลอร์และระบบเป้าหมาย

ฉันจะบอกว่าการเพิ่มประสิทธิภาพการสั่งซื้อสาขาจะค่อนข้างบอบบางและไม่ถาวร ทำตามขั้นตอนการปรับจูนจริง ๆ เท่านั้น

รหัส:

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

ฉันได้รับประสิทธิภาพที่แตกต่างกัน ~ 30% เมื่อฉันสลับลำดับของ if-sort ที่เรียงลำดับและย้อนกลับเรียงเหมือนบล็อกที่ทำในรหัสของคุณ ฉันไม่แน่ใจว่าทำไม Ideone และ coliru จึงไม่แสดงความแตกต่าง
คาร์ลตัน

น่าสนใจอย่างแน่นอน ฉันจะพยายามรับข้อมูลบางอย่างสำหรับระบบอื่น แต่อาจใช้เวลาถึงวันจนกว่าฉันจะได้เล่นกับมัน คำถามน่าสนใจโดยเฉพาะอย่างยิ่งในแง่ของผลลัพธ์ของคุณ แต่พวกเขาน่าตื่นเต้นมากฉันต้องตรวจสอบมัน
luk32

หากคำถามคือผลกระทบคืออะไร? คำตอบไม่สามารถไม่ใช่ !
PJTraill

ได้. แต่ฉันไม่ได้รับการแจ้งเตือนสำหรับการอัปเดตของคำถามเดิม พวกเขาทำให้สูตรคำตอบล้าสมัย ขอโทษ ฉันจะแก้ไขเนื้อหาในภายหลังเพื่อชี้ให้เห็นว่ามันตอบคำถามเดิมและแสดงผลลัพธ์บางอย่างที่พิสูจน์แล้วว่าเป็นจุดเริ่มต้น
luk32

สิ่งนี้มีค่าซ้ำ: "โดยค่าเริ่มต้นไปตามการอ่าน" การเขียนโค้ดที่อ่านได้มักจะทำให้คุณได้ผลลัพธ์ที่ดีกว่าการพยายามเพิ่มประสิทธิภาพเล็กน้อย (ในแง่ที่แน่นอน) โดยการทำให้โค้ดของคุณยากขึ้นสำหรับมนุษย์ในการแยกวิเคราะห์
Andrew Brēza

26

แค่ 5 เซ็นต์ของฉัน ดูเหมือนว่าผลของการสั่งซื้อถ้างบควรขึ้นอยู่กับ:

  1. ความน่าจะเป็นของแต่ละคำสั่งถ้า

  2. จำนวนการวนซ้ำดังนั้นตัวทำนายสาขาสามารถเตะเข้า

  3. คำใบ้ของคอมไพเลอร์ดูเหมือนว่าไม่น่าจะเป็นเช่นรูปแบบโค้ด

ในการสำรวจปัจจัยเหล่านั้นฉันได้เปรียบเทียบฟังก์ชั่นต่อไปนี้:

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

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

ordered_ifs_with_hints ()

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

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

ข้อมูล

อาร์เรย์ข้อมูลมีตัวเลขสุ่มตั้งแต่ 0 ถึง 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;
}

ผลลัพธ์

ผลลัพธ์ต่อไปนี้สำหรับ Intel i5 @ 3,2 GHz และ G ++ 6.3.0 อาร์กิวเมนต์แรกคือ check_point (เช่นความน่าจะเป็น %% สำหรับคำสั่งที่มีโอกาสสูงมาก) อาร์กิวเมนต์ที่สองคือ data_sz (เช่นจำนวนการทำซ้ำ)

---------------------------------------------------------------------
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

การวิเคราะห์

1. การสั่งซื้อไม่สำคัญ

สำหรับการวนซ้ำ 4K และ (เกือบ) ความน่าจะเป็น 100% ของคำแถลงที่ชอบอย่างมากความแตกต่างนั้นใหญ่มาก 223%:

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

สำหรับการวนซ้ำ 4K และความน่าจะเป็น 50% ของข้อความที่ชอบอย่างมากความแตกต่างคือประมาณ 14%:

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

2. จำนวนการวนซ้ำนั้นมีความสำคัญ

ความแตกต่างระหว่างการทำซ้ำ 4K และ 8K สำหรับ (เกือบ) ความน่าจะเป็น 100% ของคำสั่งที่ชอบสูงคือประมาณสองครั้ง (ตามที่คาดไว้):

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

แต่ความแตกต่างระหว่างการทำซ้ำ 4K และ 8K สำหรับความน่าจะเป็น 50% ของข้อความที่ชอบมากคือ 5,5 ครั้ง:

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

ทำไมเป็นเช่นนั้น เนื่องจากการทำนายสาขาพลาด นี่คือสาขาที่ขาดหายไปสำหรับกรณีดังกล่าวข้างต้น:

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

ดังนั้นใน i5 ของฉันตัวพยากรณ์สาขาล้มเหลวอย่างน่าทึ่งสำหรับสาขาที่ไม่น่าจะเป็นไปได้และชุดข้อมูลขนาดใหญ่

3. คำแนะนำช่วยบิต

สำหรับการทำซ้ำ 4K ผลลัพธ์จะค่อนข้างแย่กว่าสำหรับความน่าจะเป็น 50% และค่อนข้างดีกว่าสำหรับความน่าจะเป็นที่ใกล้เคียง 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

แต่สำหรับการทำซ้ำ 8K ผลลัพธ์จะดีขึ้นเล็กน้อย:

---------------------------------------------------------------------
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

ดังนั้นคำแนะนำยังช่วย แต่เพียงเล็กน้อย

ข้อสรุปโดยรวมคือ: เปรียบเทียบเกณฑ์มาตรฐานเสมอเพราะผลลัพธ์อาจแปลกใจ

หวังว่าจะช่วย


1
i5 Nehalem i5 Skylake? เพียงแค่พูดว่า "i5" นั้นไม่เฉพาะเจาะจงมากนัก นอกจากนี้ฉันถือว่าคุณใช้g++ -O2หรือ-O3 -fno-tree-vectorizeแต่คุณควรพูดอย่างนั้น
Peter Cordes

น่าสนใจว่า with_hints ยังคงแตกต่างกันสำหรับการสั่งซื้อและกลับรายการ มันจะดีถ้าคุณเชื่อมโยงไปยังแหล่งใดที่หนึ่ง (เช่นลิงก์ Godbolt โดยเฉพาะอย่างยิ่งลิงก์แบบเต็มดังนั้นการตัดทอนลิงก์จึงไม่สามารถหมุนเวียนได้)
Peter Cordes

1
ความจริงที่ว่าตัวพยากรณ์สาขาสามารถคาดการณ์ได้ดีแม้ที่ขนาดข้อมูลอินพุต 4K คือสามารถ "ทำลาย" มาตรฐานโดยการจดจำผลลัพธ์ของสาขาข้ามลูปด้วยระยะเวลาเป็นพัน ๆเป็นเครื่องพิสูจน์ถึงพลังแห่งความทันสมัย ตัวพยากรณ์สาขา โปรดทราบว่าตัวคาดการณ์ค่อนข้างละเอียดอ่อนในบางกรณีเช่นการจัดตำแหน่งดังนั้นจึงยากที่จะสรุปข้อสรุปที่ชัดเจนเกี่ยวกับการเปลี่ยนแปลงบางอย่าง ตัวอย่างเช่นคุณสังเกตเห็นพฤติกรรมที่ตรงกันข้ามสำหรับคำใบ้ในกรณีต่าง ๆ แต่สามารถอธิบายได้ด้วยการสุ่มเลย์เอาต์โค้ดที่มีผลต่อตัวทำนาย
BeeOnRope

1
@ PeterCordes จุดหลักของฉันคือในขณะที่เราสามารถคาดการณ์ผลลัพธ์ของการเปลี่ยนแปลงได้ แต่เราก็ยังสามารถวัดประสิทธิภาพก่อนและหลังการเปลี่ยนแปลงได้ดีกว่า ... และคุณพูดถูกต้องฉันควรได้กล่าวว่ามันได้รับการปรับปรุงด้วย -O3 และโปรเซสเซอร์ คือ i5-4460 @ 3.20GHz
Andriy Berestovskyy

19

ขึ้นอยู่กับบางส่วนของคำตอบอื่น ๆ ที่นี่ดูเหมือนว่าคำตอบเดียวที่จริงคือมันขึ้นอยู่กับ ขึ้นอยู่กับอย่างน้อยสิ่งต่อไปนี้ (แม้ว่าไม่จำเป็นในลำดับความสำคัญนี้):

  • ความน่าจะเป็นสัมพัทธ์ของแต่ละสาขา นี่คือคำถามเดิมที่ถาม จากคำตอบที่มีอยู่ดูเหมือนว่าจะมีเงื่อนไขบางอย่างที่การจัดเรียงตามความน่าจะเป็นช่วยได้ แต่ดูเหมือนจะไม่เป็นเช่นนั้นเสมอไป หากความน่าจะเป็นสัมพัทธ์ไม่แตกต่างกันมากมันก็ไม่น่าจะสร้างความแตกต่างอะไรกับสิ่งที่พวกเขาสั่งอย่างไรก็ตามถ้าเงื่อนไขแรกเกิดขึ้น 99.999% ของเวลาและอีกอันหนึ่งคือเศษส่วนของสิ่งที่เหลืออยู่ฉันจะ สมมติว่าการวางตำแหน่งที่มีโอกาสมากที่สุดก่อนจะเป็นประโยชน์ในแง่ของเวลา
  • ต้นทุนการคำนวณเงื่อนไขจริง / เท็จสำหรับแต่ละสาขา หากค่าใช้จ่ายเวลาในการทดสอบเงื่อนไขสูงมากสำหรับสาขาหนึ่งเมื่อเทียบกับสาขาอื่นนั่นก็น่าจะมีผลกระทบอย่างมากต่อเวลาและประสิทธิภาพ ตัวอย่างเช่นให้พิจารณาเงื่อนไขที่ใช้เวลา 1 หน่วยในการคำนวณ (เช่นการตรวจสอบสถานะของตัวแปรบูลีน) กับเงื่อนไขอื่นที่ใช้หน่วยเป็นสิบ, หลักแสน, หรือแม้กระทั่งล้านหน่วยเวลาในการคำนวณ (เช่นการตรวจสอบเนื้อหาของ ไฟล์บนดิสก์หรือดำเนินการค้นหา SQL ที่ซับซ้อนกับฐานข้อมูลขนาดใหญ่) สมมติว่ารหัสตรวจสอบเงื่อนไขตามลำดับในแต่ละครั้งเงื่อนไขที่เร็วกว่าน่าจะเป็นอันดับแรก (เว้นแต่พวกเขาจะขึ้นอยู่กับเงื่อนไขอื่น ๆ ที่ล้มเหลวก่อน)
  • คอมไพเลอร์ / ล่าม คอมไพเลอร์บางคน (หรือล่าม) อาจรวมถึงการเพิ่มประสิทธิภาพของประเภทอื่นที่อาจส่งผลกระทบต่อประสิทธิภาพ (และบางส่วนของเหล่านี้จะปรากฏเฉพาะในกรณีที่เลือกตัวเลือกบางอย่างในระหว่างการรวบรวมและ / หรือการดำเนินการ) ดังนั้นถ้าคุณกำลังทำการเปรียบเทียบสองคอมไพล์และเอ็กซีคิวต์ของรหัสที่เหมือนกันในระบบเดียวกันโดยใช้คอมไพเลอร์เดียวกันโดยที่ความแตกต่างเพียงอย่างเดียวคือคำสั่งของกิ่งไม้ที่มีปัญหานั้นคุณจะต้องเพิ่มระยะห่างสำหรับคอมไพเลอร์
  • ระบบปฏิบัติการ / ฮาร์ดแวร์ ตาม luk32 และ Yakk ที่กล่าวถึง, ซีพียูต่างๆมีการเพิ่มประสิทธิภาพของตัวเอง (เช่นเดียวกับระบบปฏิบัติการ) ดังนั้นการวัดจึงมีความอ่อนไหวต่อการเปลี่ยนแปลงอีกครั้งที่นี่
  • ความถี่ของการใช้รหัสบล็อก หากบล็อกที่มีสาขานั้นเข้าถึงได้ยาก (เช่นเพียงครั้งเดียวในระหว่างการเริ่มต้น) ก็อาจมีความสำคัญน้อยมากตามลำดับที่คุณวางสาขา ในทางกลับกันหากรหัสของคุณใช้ค้อนทุบบล็อกโค้ดนี้ระหว่างส่วนที่สำคัญของรหัสการสั่งซื้ออาจมีความสำคัญมาก (ขึ้นอยู่กับมาตรฐาน)

วิธีเดียวที่จะทราบได้อย่างชัดเจนก็คือการทำมาตรฐานกรณีเฉพาะของคุณโดยเฉพาะอย่างยิ่งในระบบที่คล้ายกับ (หรือคล้ายกันมาก) กับระบบที่ตั้งใจซึ่งรหัสจะทำงานในที่สุด หากมีวัตถุประสงค์เพื่อให้ทำงานบนชุดของระบบที่แตกต่างกันด้วยฮาร์ดแวร์ที่แตกต่างกันระบบปฏิบัติการ ฯลฯ ดังนั้นจึงเป็นความคิดที่ดีที่จะทำการวัดประสิทธิภาพในรูปแบบต่างๆ มันอาจเป็นความคิดที่ดีที่จะรวบรวมรหัสด้วยการสั่งซื้อหนึ่งรายการในระบบประเภทหนึ่งและการสั่งซื้อสินค้าอื่นในระบบประเภทอื่น

กฎส่วนบุคคลของฉัน (สำหรับกรณีส่วนใหญ่ในกรณีที่ไม่มีมาตรฐาน) คือการสั่งซื้อตาม:

  1. เงื่อนไขที่ขึ้นอยู่กับผลลัพธ์ของเงื่อนไขก่อนหน้า
  2. ค่าใช้จ่ายในการคำนวณสภาพนั้น
  3. ความน่าจะเป็นสัมพัทธ์ของแต่ละสาขา

13

วิธีที่ฉันมักจะเห็นการแก้ไขนี้สำหรับรหัสประสิทธิภาพสูงคือการรักษาลำดับที่อ่านได้มากที่สุด แต่ให้คำแนะนำแก่คอมไพเลอร์ นี่คือตัวอย่างหนึ่งจากเคอร์เนล 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);

ที่นี่สมมุติฐานคือการตรวจสอบการเข้าถึงจะผ่านและไม่มีข้อผิดพลาดใดresๆ พยายามเรียงลำดับใด ๆ เหล่านี้ถ้าส่วนคำสั่งจะสับสนรหัส แต่จริง ๆ แล้วแมโครlikely()และunlikely()ช่วยให้สามารถอ่านได้โดยชี้ให้เห็นว่าเป็นกรณีปกติและอะไรคือข้อยกเว้น

การดำเนินงานลินุกซ์ของแมโครผู้ใช้คุณลักษณะเฉพาะ GCC มันดูเหมือนว่าเสียงดังกราวและ Intel C คอมไพเลอร์สนับสนุนไวยากรณ์เดียวกัน แต่MSVC ไม่ได้มีคุณสมบัติดังกล่าว


4
สิ่งนี้จะมีประโยชน์มากขึ้นถ้าคุณสามารถอธิบายวิธีการlikely()และunlikely()มาโครที่กำหนดไว้และรวมถึงข้อมูลบางอย่างเกี่ยวกับคุณสมบัติคอมไพเลอร์ที่เกี่ยวข้อง
Nate Eldredge

1
AFAIK คำแนะนำเหล่านี้ "เท่านั้น" เปลี่ยนเลย์เอาต์ของหน่วยความจำของบล็อคโค้ดและว่าใช่หรือไม่ใช่จะทำให้เกิดการกระโดด สิ่งนี้อาจมีข้อได้เปรียบด้านประสิทธิภาพเช่นสำหรับความต้องการ (หรือขาดไป) เพื่ออ่านหน้าหน่วยความจำ แต่นี่ไม่ได้จัดเรียงลำดับที่เงื่อนไขภายในรายการอื่น ๆ ของ if-ifs จะถูกประเมิน
Hagen von Eitzen

@ HagenvonEitzen อืมใช่นั่นเป็นจุดที่ดีมันไม่สามารถส่งผลกระทบต่อลำดับelse ifถ้าคอมไพเลอร์ไม่ฉลาดพอที่จะรู้ว่าเงื่อนไขนั้นไม่เหมือนกัน
jpa

7

ขึ้นอยู่กับคอมไพเลอร์และแพลตฟอร์มที่คุณกำลังรวบรวม

ตามทฤษฎีแล้วเงื่อนไขที่เป็นไปได้มากที่สุดควรทำให้การควบคุมกระโดดได้น้อยที่สุด

โดยทั่วไปแล้วเงื่อนไขที่เป็นไปได้มากที่สุดควรเป็นอันดับแรก:

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

ส่วนใหญ่เป็นที่นิยมของ asm จะขึ้นอยู่กับเงื่อนไขที่สาขากระโดดเมื่อเงื่อนไขเป็นจริง รหัส C นั้นน่าจะถูกแปลเป็น asm หลอกเช่น:

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

นี่เป็นเพราะการกระโดดทำให้ซีพียูยกเลิกไพพ์ไลน์การประมวลผลและแผงลอยเนื่องจากตัวนับโปรแกรมเปลี่ยนไป (สำหรับสถาปัตยกรรมที่รองรับไพพ์ไลน์ซึ่งเป็นเรื่องธรรมดาจริงๆ) จากนั้นเป็นเรื่องเกี่ยวกับคอมไพเลอร์ซึ่งอาจใช้หรือไม่ใช้การเพิ่มประสิทธิภาพที่ซับซ้อนบางอย่างเกี่ยวกับการมีเงื่อนไขทางสถิติที่น่าจะได้รับการควบคุมมากที่สุด


2
คุณระบุว่าสาขาตามเงื่อนไขเกิดขึ้นเมื่อเงื่อนไขเป็นจริง แต่ตัวอย่าง "pseudo asm" จะแสดงตรงกันข้าม นอกจากนี้ยังไม่สามารถพูดได้ว่าการกระโดดตามเงื่อนไข (กระโดดน้อยกว่ามาก) ทำให้ท่อส่งเพราะ CPU ที่ทันสมัยมักจะมีการทำนายสาขา ในความเป็นจริงหากคาดการณ์ว่าสาขาจะต้องดำเนินการ แต่ไม่ได้ดำเนินการแล้วท่อจะหยุดทำงาน ฉันยังคงพยายามเรียงลำดับเงื่อนไขตามลำดับความน่าจะเป็นที่ลดลง แต่สิ่งที่คอมไพเลอร์และ CPU สร้างขึ้นมานั้นขึ้นอยู่กับการนำไปปฏิบัติอย่างมาก
Arne Vogel

1
ฉันใส่“ ไม่ (ไม่น่าจะเป็นไปได้มากที่สุด)” ดังนั้นถ้าส่วนใหญ่ไม่น่าจะเป็นจริงการควบคุมก็จะดำเนินต่อไปโดยไม่กระโดด
NoImaginationGuy

1
"asm ที่ได้รับความนิยมมากที่สุดขึ้นอยู่กับกิ่งก้านสาขาที่มีเงื่อนไขซึ่งกระโดดเมื่อเงื่อนไขเป็นจริง" .. ISAs อันไหนที่จะเป็น? แน่นอนว่ามันไม่เป็นความจริงสำหรับ x86 หรือสำหรับ ARM นรกสำหรับซีพียู ARM ขั้นพื้นฐาน (และ x86 ที่เก่าแก่มากแม้สำหรับ bps ที่ซับซ้อนพวกเขามักจะเริ่มต้นด้วยสมมติฐานนั้นและปรับเปลี่ยน) ตัวทำนายสาขาสันนิษฐานว่าสาขาไปข้างหน้าไม่ได้ถูกยึดและสาขาที่อยู่ข้างหลังเสมอ เป็นความจริง.
Voo

1
คอมไพเลอร์ที่ฉันพยายามส่วนใหญ่ใช้วิธีการที่ฉันกล่าวถึงข้างต้นสำหรับการทดสอบอย่างง่าย โปรดทราบว่าclangจริง ๆ แล้วใช้วิธีการที่แตกต่างกันtest2และtest3: เนื่องจากฮิวริสติกที่ระบุว่า a < 0หรือ== 0การทดสอบมีแนวโน้มที่จะเป็นเท็จจึงตัดสินใจที่จะโคลนส่วนที่เหลือของฟังก์ชันบนทั้งสองเส้นทางดังนั้นจึงสามารถทำให้condition == falseการตกผ่านเส้นทาง สิ่งนี้เป็นไปได้เพียงเพราะส่วนที่เหลือของฟังก์ชั่นสั้น: ในtest4ฉันเพิ่มการดำเนินการอีกหนึ่งครั้งและมันกลับไปที่วิธีที่ฉันอธิบายไว้ข้างต้น
BeeOnRope

1
@ArneVogel - ถูกทำนายสาขาดำเนินการไม่ได้โดยสิ้นเชิงแผงลอยท่อบนซีพียูที่ทันสมัย แต่พวกเขายังมักจะเลวร้ายยิ่งกว่าอย่างมีนัยสำคัญไม่ได้ดำเนินการดังนี้ (1) พวกเขาหมายถึงการควบคุมการไหลไม่ต่อเนื่องกันเพื่อให้ส่วนที่เหลือของคำแนะนำหลังจากที่jmpไม่ได้ มีประโยชน์ดังนั้นแบนด์วิดท์การเรียกใช้ / ถอดรหัสจะสูญเปล่า (2) แม้ว่าจะมีการคาดเดาคอร์บิ๊กคอร์สมัยใหม่ทำเพียงแค่ดึงข้อมูลหนึ่งครั้งต่อรอบดังนั้นมันจึง จำกัด ขีด จำกัด ของ 1 เอาสาขา / รอบ (OTOH ) มันยากสำหรับการคาดการณ์ของสาขาที่จะจัดการกับสาขาที่ต่อเนื่องและในกรณีของตัวทำนายที่รวดเร็วและช้า ...
BeeOnRope

6

ฉันตัดสินใจรันการทดสอบบนเครื่องของฉันเองโดยใช้รหัส Lik32 ฉันต้องเปลี่ยนเนื่องจาก windows หรือคอมไพเลอร์ของฉันคิดว่าความละเอียดสูงคือ 1ms ใช้

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

vector<int> rand_vec(10000000);

GCC ได้ทำการเปลี่ยนแปลงแบบเดียวกันกับรหัสต้นฉบับทั้งสอง

โปรดทราบว่ามีเพียงการทดสอบสองเงื่อนไขแรกเท่านั้นเนื่องจากเงื่อนไขที่สามต้องเป็นจริงเสมอ GCC เป็น Sherlock ที่นี่

ย้อนกลับ

.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

ดังนั้นนี่จึงไม่ได้บอกอะไรเรามากนักยกเว้นว่ากรณีสุดท้ายไม่จำเป็นต้องมีการทำนายสาขา

ตอนนี้ฉันลองชุดค่าผสมทั้งหมด 6 ค่าจากนั้นค่าบนสุด 2 ตัวเป็นแบบย้อนกลับและเรียงลำดับดั้งเดิม สูงคือ> = 95, ต่ำคือ <20, กลางคือ 20-94 พร้อมการทำซ้ำ 10,000,000 ครั้ง

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.

เหตุใดจึงมีคำสั่งซื้อสูง, ต่ำ, med จึงเร็วขึ้น (เล็กน้อย)

เพราะสิ่งที่ไม่สามารถคาดเดาได้มากที่สุดเป็นสิ่งสุดท้ายและดังนั้นจึงไม่เคยวิ่งผ่านตัวทำนายสาขา

          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.

ดังนั้นกิ่งไม้จะถูกทำนายเอาไปยึดและส่วนที่เหลือด้วย

6% + (0.94 *) 20% การคาดการณ์ผิดพลาด

"เรียง"

          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

กิ่งไม้จะถูกทำนายโดยที่ไม่ได้ถ่ายไม่ได้ถ่ายและ Sherlock

25% + (0.75 *) 24% การคาดการณ์ผิด

ให้ความแตกต่าง 18-23% (วัดความแตกต่างของ ~ 9%) แต่เราต้องคำนวณรอบแทนการคาดคะเน% ผิด

สมมติว่ามีการลงโทษที่ไม่ถูกต้อง 17 รอบบนซีพียู Nehalem ของฉันและการตรวจสอบแต่ละครั้งใช้เวลา 1 รอบในการออก (4-5 คำแนะนำ) และวนรอบใช้เวลาหนึ่งรอบเกินไป การพึ่งพาข้อมูลคือตัวนับและตัวแปรลูป แต่เมื่อการคาดการณ์ผิดไปจากที่ควรจะไม่ส่งผลต่อเวลา

ดังนั้นสำหรับ "ย้อนกลับ" เราจะได้เวลา (นี่ควรเป็นสูตรที่ใช้ในสถาปัตยกรรมคอมพิวเตอร์: วิธีเชิงปริมาณ 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

และเหมือนกันสำหรับ "เรียงลำดับ"

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% เทียบกับ ~ 9% ที่วัด (ใกล้กับที่วัด!?!)

ดังนั้นความชัดเจนของ OP จึงไม่ชัดเจน

ด้วยการทดสอบเหล่านี้การทดสอบอื่น ๆ ที่มีรหัสซับซ้อนมากขึ้นหรือการพึ่งพาข้อมูลมากขึ้นจะแตกต่างกันอย่างแน่นอนดังนั้นวัดเคสของคุณ

การเปลี่ยนลำดับของการทดสอบเปลี่ยนผลลัพธ์ แต่อาจเป็นเพราะการจัดแนวที่แตกต่างกันของการเริ่มลูปซึ่งควรจะเป็นแนว 16 ไบต์ในซีพียู Intel รุ่นใหม่ทั้งหมด แต่ไม่ได้อยู่ในกรณีนี้


4

ใส่ไว้ในลำดับที่คุณต้องการ แน่นอนว่าสาขาอาจช้าลง แต่การแยกสาขาไม่ควรใช้คอมพิวเตอร์เป็นส่วนใหญ่

หากคุณกำลังทำงานเกี่ยวกับส่วนสำคัญของประสิทธิภาพของรหัสแน่นอนว่าต้องใช้ลำดับตรรกะการเพิ่มประสิทธิภาพที่แนะนำโดยละเอียดและเทคนิคอื่น ๆ แต่สำหรับรหัสทั่วไปฉันคิดว่ามันเป็นตัวเลือกที่มีสไตล์มากกว่า


6
การทำนายสาขาล้มเหลวมีราคาแพง ใน microbenchmarks พวกเขาอยู่ภายใต้ costedเนื่องจาก x86s มีตัวทำนายสาขาขนาดใหญ่ การวนลูปที่แน่นหนาในสภาวะเดียวกันส่งผลให้ CPU รู้ดีกว่าที่คุณทำ แต่ถ้าคุณมีสาขาทั่วรหัสของคุณคุณสามารถมีแคชการคาดคะเนสาขาของคุณหมดสล็อตและซีพียูจะถือว่าอะไรก็ตามที่เป็นค่าเริ่มต้น การทราบว่าการเดาเริ่มต้นนั้นสามารถบันทึกรอบรหัสฐานของคุณได้ทั้งหมด
Yakk - Adam Nevraumont

@Yakk Jack คำตอบคือคำตอบเดียวที่ถูกต้องที่นี่ อย่าทำการปรับให้เหมาะสมที่ลดความสามารถในการอ่านถ้าคอมไพเลอร์ของคุณสามารถทำการปรับให้เหมาะสมนั้นได้ คุณจะไม่ทำการพับแบบคงที่การกำจัดโค้ดที่ไม่ทำงานการวนลูปที่ยังไม่เปิดออกหรือการเพิ่มประสิทธิภาพอื่น ๆ หากคอมไพเลอร์ของคุณทำเพื่อคุณใช่ไหม เขียนรหัสของคุณใช้การปรับให้เหมาะสมแบบกำหนดค่าด้วยโปรไฟล์แนะนำ (ซึ่งออกแบบมาเพื่อแก้ไขปัญหานี้เนื่องจากผู้เขียนคาดเดา) และดูว่าคอมไพเลอร์ของคุณปรับให้เหมาะสมหรือไม่ ในที่สุดคุณไม่ต้องการให้ branchess ใด ๆ ในโค้ดที่มีประสิทธิภาพที่สำคัญอยู่ดี
Christoph Diegelmann

@Christoph ฉันจะไม่รวมรหัสฉันรู้ว่าจะตาย ฉันจะไม่ใช้i++เมื่อ++iต้องทำเพราะฉันรู้ว่าi++สำหรับตัววนซ้ำบางตัวนั้นยากที่จะปรับให้เหมาะ++iและความแตกต่าง (สำหรับฉัน) ไม่สำคัญ นี่คือการหลีกเลี่ยงการมองในแง่ร้าย การวางบล็อกที่น่าจะเป็นอันดับแรกให้เป็นนิสัยเริ่มต้นจะไม่ทำให้การอ่านลดลงอย่างเห็นได้ชัด (และอาจช่วยได้จริง!) ในขณะที่ส่งผลให้โค้ดที่เป็นมิตรกับการคาดคะเนสาขา โดยการเพิ่มประสิทธิภาพไมโครภายหลัง)
Yakk - Adam Nevraumont

3

หากคุณทราบถึงความน่าจะเป็นสัมพัทธ์ของคำสั่ง if-else ดังนั้นเพื่อวัตถุประสงค์ด้านประสิทธิภาพการใช้วิธีเรียงลำดับจะดีกว่าเพราะจะตรวจสอบเงื่อนไขเดียวเท่านั้น (เงื่อนไขจริง)

คอมไพเลอร์จะตรวจสอบเงื่อนไขทั้งหมดโดยไม่จำเป็นและจะใช้เวลา

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.