เหตุใดการประมวลผลอาร์เรย์ที่เรียงลำดับจึงเร็วกว่าการประมวลผลอาร์เรย์ที่ไม่เรียงลำดับ


24444

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

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // Generate data
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! With this, the next loop runs faster.
    std::sort(data, data + arraySize);

    // Test
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // Primary loop
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}
  • หากไม่มีstd::sort(data, data + arraySize);รหัสจะทำงานใน 11.54 วินาที
  • ด้วยข้อมูลที่เรียงลำดับรหัสจะทำงานใน 1.93 วินาที

ตอนแรกฉันคิดว่านี่อาจเป็นเพียงภาษาหรือคอมไพเลอร์ผิดปกติดังนั้นฉันจึงลอง Java:

import java.util.Arrays;
import java.util.Random;

public class Main
{
    public static void main(String[] args)
    {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // Primary loop
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

ด้วยผลลัพธ์ที่คล้ายกัน แต่รุนแรงน้อยกว่า


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

  • เกิดอะไรขึ้น?
  • เหตุใดการประมวลผลอาร์เรย์ที่เรียงลำดับจึงเร็วกว่าการประมวลผลอาร์เรย์ที่ไม่เรียงลำดับ

รหัสกำลังรวมคำศัพท์อิสระบางคำดังนั้นคำสั่งนั้นไม่สำคัญ



16
@SachinVerma ปิดส่วนบนของหัวของฉัน: 1) JVM ในที่สุดอาจฉลาดพอที่จะใช้การเคลื่อนไหวตามเงื่อนไข 2) รหัสถูกผูกไว้กับหน่วยความจำ 200M นั้นใหญ่เกินไปที่จะใส่ในแคชของ CPU ดังนั้นประสิทธิภาพจะถูกคอขวดโดยแบนด์วิดธ์หน่วยความจำแทนการแตกแขนง
Mysticial

12
@ Mysticial ประมาณ 2) ฉันคิดว่าตารางการทำนายติดตามรูปแบบ (โดยไม่คำนึงถึงตัวแปรจริงที่ตรวจสอบสำหรับรูปแบบนั้น) และเปลี่ยนผลลัพธ์การทำนายตามประวัติ คุณช่วยบอกเหตุผลฉันหน่อยได้ไหมว่าทำไมอาร์เรย์ขนาดใหญ่สุดถึงไม่ได้รับประโยชน์จากการทำนายสาขา
Sachin Verma

15
@SachinVerma ทำเช่นนั้น แต่เมื่ออาเรย์นั้นมีขนาดใหญ่ปัจจัยที่ยิ่งใหญ่กว่าก็น่าจะเกิดขึ้นคือแบนด์วิธหน่วยความจำ หน่วยความจำไม่แบน การเข้าถึงหน่วยความจำช้ามากและมีแบนด์วิดท์ในจำนวนที่ จำกัด เพื่อลดความซับซ้อนของสิ่งต่าง ๆ มีเพียงไบต์จำนวนมากที่สามารถถ่ายโอนระหว่าง CPU และหน่วยความจำในระยะเวลาที่แน่นอน รหัสง่าย ๆ อย่างที่พบในคำถามนี้อาจจะถึงขีด จำกัด นั้นแม้ว่ามันจะชะลอตัวลงจากการคาดการณ์ที่ผิด สิ่งนี้ไม่ได้เกิดขึ้นกับอาเรย์ของ 32768 (128KB) เพราะมันพอดีกับแคช L2 ของซีพียู
ลึกลับ

13
มีข้อบกพร่องด้านความปลอดภัยใหม่ที่ชื่อว่า BranchScope: cs.ucr.edu/~nael/pubs/asplos18.pdf
Veve

คำตอบ:


31789

คุณตกเป็นเหยื่อของการทำนายสาขาล้มเหลว


การทำนายสาขาคืออะไร

พิจารณาทางแยกรถไฟ:

ภาพแสดงทางแยกรางรถไฟ ภาพโดย Mecanismo ผ่าน Wikimedia Commons ใช้ภายใต้ใบอนุญาตCC-By-SA 3.0

ตอนนี้เพื่อการโต้แย้งสมมติว่านี่เป็นยุค 1800 - ก่อนการสื่อสารทางไกลหรือวิทยุ

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

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

มีวิธีที่ดีกว่า? คุณเดาทิศทางของรถไฟที่จะไป!

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

หากคุณเดาถูกทุกครั้งรถไฟจะไม่หยุด
หากคุณเดาผิดบ่อยเกินไปรถไฟจะใช้เวลาหยุดเยอะสำรองและเริ่มใหม่


พิจารณา if-statement:ที่ระดับโปรเซสเซอร์มันเป็นคำสั่งสาขา:

สกรีนช็อตของโค้ดที่คอมไพล์ซึ่งมีคำสั่ง if

คุณเป็นโปรเซสเซอร์และคุณเห็นสาขา คุณไม่รู้ว่ามันจะไปทางไหน คุณทำอะไร? คุณหยุดการทำงานและรอจนกว่าคำแนะนำก่อนหน้านี้จะเสร็จสมบูรณ์ จากนั้นคุณดำเนินการต่อเส้นทางที่ถูกต้อง

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

มีวิธีที่ดีกว่า? คุณเดาทิศทางที่สาขาจะไป!

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

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


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

ดังนั้นคุณจะเดาอย่างมีกลยุทธ์เพื่อลดจำนวนครั้งที่รถไฟจะต้องสำรองและเดินไปอีกเส้นทางหนึ่งได้อย่างไร? คุณดูประวัติที่ผ่านมา! หากรถไฟเหลือ 99% ของเวลาคุณคาดเดาไปทางซ้าย ถ้ามันสลับกันคุณจะสลับการเดาของคุณ ถ้ามันไปทางเดียวทุก ๆ สามครั้งคุณก็เดาเหมือนกัน ...

คุณพยายามระบุรูปแบบและปฏิบัติตาม นี่เป็นวิธีการทำงานของเครื่องมือพยากรณ์สาขามากหรือน้อย

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

อ่านเพิ่มเติม: "สาขาทำนาย" บทความเกี่ยวกับวิกิพีเดีย


ดังที่ได้กล่าวไว้ข้างต้นผู้ร้ายคือข้อความสั่ง if:

if (data[c] >= 128)
    sum += data[c];

ขอให้สังเกตว่าข้อมูลจะถูกกระจายอย่างเท่าเทียมกันระหว่าง 0 และ 255 เมื่อเรียงลำดับข้อมูลประมาณครึ่งแรกของการทำซ้ำจะไม่ป้อน if-statement หลังจากนั้นพวกเขาทั้งหมดจะเข้าสู่คำสั่ง if

นี่เป็นมิตรกับผู้ทำนายสาขาเนื่องจากสาขาไปในทิศทางเดียวกันหลายต่อหลายครั้ง แม้แต่ตัวนับอิ่มตัวแบบง่าย ๆ ก็สามารถทำนายสาขาได้อย่างถูกต้องยกเว้นการวนซ้ำสองสามครั้งหลังจากที่มันเปลี่ยนทิศทาง

การสร้างภาพข้อมูลอย่างรวดเร็ว:

T = branch taken
N = branch not taken

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (easy to predict)

อย่างไรก็ตามเมื่อข้อมูลเป็นแบบสุ่มสมบูรณ์ตัวพยากรณ์สาขาจะไม่แสดงผลเนื่องจากไม่สามารถทำนายข้อมูลแบบสุ่มได้ ดังนั้นอาจมีการคาดคะเนผิดประมาณ 50% (ไม่ดีไปกว่าการคาดเดาแบบสุ่ม)

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

       = TTNTTTTNTNNTTTN ...   (completely random - hard to predict)

ดังนั้นสิ่งที่สามารถทำได้?

หากคอมไพเลอร์ไม่สามารถปรับสาขาให้เหมาะกับการย้ายตามเงื่อนไขคุณสามารถลองแฮ็กบางอย่างถ้าคุณเต็มใจที่จะเสียสละความสามารถในการอ่านเพื่อประสิทธิภาพ

แทนที่:

if (data[c] >= 128)
    sum += data[c];

ด้วย:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

สิ่งนี้จะกำจัดสาขาและแทนที่ด้วยการดำเนินการระดับบิตบางอย่าง

(โปรดทราบว่าการแฮ็คนี้ไม่เทียบเท่ากับคำสั่ง if-original อย่างเคร่งครัด แต่ในกรณีนี้ใช้ได้กับค่าอินพุตทั้งหมดของdata[])

มาตรฐาน: Core i7 920 @ 3.5 GHz

C ++ - Visual Studio 2010 - x64 ที่วางจำหน่าย

//  Branch - Random
seconds = 11.777

//  Branch - Sorted
seconds = 2.352

//  Branchless - Random
seconds = 2.564

//  Branchless - Sorted
seconds = 2.587

Java - NetBeans 7.1.1 JDK 7 - x64

//  Branch - Random
seconds = 10.93293813

//  Branch - Sorted
seconds = 5.643797077

//  Branchless - Random
seconds = 3.113581453

//  Branchless - Sorted
seconds = 3.186068823

ข้อสังเกต:

  • กับสาขา:มีความแตกต่างอย่างมากระหว่างข้อมูลที่เรียงและไม่เรียงลำดับ
  • ด้วยการแฮก:ไม่มีความแตกต่างระหว่างข้อมูลที่เรียงลำดับและไม่เรียงลำดับ
  • ในกรณี C ++ การแฮ็คจะช้ากว่าการแบรนช์เมื่อข้อมูลถูกเรียง

กฎทั่วไปของหัวแม่มือคือการหลีกเลี่ยงการแยกสาขาขึ้นอยู่กับข้อมูลในลูปที่สำคัญ


ปรับปรุง:

  • GCC 4.6.1 พร้อม-O3หรือ-ftree-vectorizex64 สามารถสร้างการย้ายแบบมีเงื่อนไข ดังนั้นจึงไม่มีความแตกต่างระหว่างข้อมูลที่เรียงและไม่เรียงลำดับ - ทั้งสองอย่างรวดเร็ว

    (หรือค่อนข้างเร็ว: สำหรับกรณีที่เรียงลำดับแล้วcmovอาจช้าลงโดยเฉพาะถ้า GCC วางไว้บนเส้นทางวิกฤติแทนที่จะเป็นเพียงaddโดยเฉพาะอย่างยิ่งใน Intel ก่อน Broadwell ที่cmovมีเวลาแฝงอยู่2 รอบ: การตั้งค่า gcc optimization -O3 ทำให้โค้ดช้ากว่า -O2 )

  • VC ++ 2010 /Oxเป็นไม่สามารถสร้างเงื่อนไขย้ายสาขานี้แม้ภายใต้

  • Intel C ++ Compiler (ICC) 11 ทำสิ่งอัศจรรย์ มันทำการแลกเปลี่ยนสองลูปดังนั้นจึงยกสาขาที่ไม่สามารถคาดเดาได้ให้กับลูปด้านนอก ดังนั้นไม่เพียง แต่จะรอดพ้นจากความผิดพลาดเท่านั้น แต่ยังเร็วเป็นสองเท่าของสิ่งที่ VC ++ และ GCC สามารถสร้างได้! กล่าวอีกนัยหนึ่ง ICC ใช้ประโยชน์จากการทดสอบลูปเพื่อเอาชนะมาตรฐาน ...

  • หากคุณให้รหัสที่ไม่มีสาขาของ Intel คอมไพเลอร์มันจะแสดงเวกเตอร์ที่ถูกต้อง ... และเร็วพอ ๆ กับสาขา (ที่มีการแลกเปลี่ยนลูป)

สิ่งนี้แสดงให้เห็นว่าแม้คอมไพเลอร์สมัยใหม่ที่พัฒนาแล้วจะแตกต่างกันอย่างมากในความสามารถในการปรับโค้ดให้เหมาะสม ...


256
ลองดูที่คำถามติดตามนี้: stackoverflow.com/questions/11276291/ … Intel Compiler ใกล้เข้ามาแล้วเพื่อกำจัดวงรอบนอกอย่างสมบูรณ์
Mysticial

24
@Master อย่างเป็นทางการรถไฟ / คอมไพเลอร์รู้ได้อย่างไรว่ามันเข้าสู่ทางที่ผิด?
onmyway133

26
@obe: กำหนดโครงสร้างหน่วยความจำแบบลำดับชั้นเป็นไปไม่ได้ที่จะบอกว่าค่าใช้จ่ายของแคชที่พลาดจะเป็นเท่าไหร่ มันอาจจะพลาดใน L1 และได้รับการแก้ไขใน L2 ช้าลงหรือพลาดใน L3 และได้รับการแก้ไขในหน่วยความจำระบบ อย่างไรก็ตามหากมีเหตุผลแปลก ๆ บางประการที่แคชนี้ทำให้หน่วยความจำในเพจที่ไม่ได้ใช้ในการโหลดจากดิสก์คุณมีจุดดี ... หน่วยความจำไม่มีเวลาในการเข้าถึงในช่วงมิลลิวินาทีประมาณ 25-30 ปี ;)
Andon M. Coleman

21
Rule of thumbสำหรับการเขียนโค้ดที่มีประสิทธิภาพในโปรเซสเซอร์ที่ทันสมัย: ทุกสิ่งที่ทำให้การเรียกใช้โปรแกรมของคุณเป็นปกติมากขึ้น (ไม่สม่ำเสมอน้อยกว่า) จะทำให้มีประสิทธิภาพมากขึ้น การเรียงลำดับในตัวอย่างนี้มีผลกระทบนี้เนื่องจากการคาดการณ์ของสาขา Access locality (แทนที่การเข้าถึงแบบสุ่มทั่วทั้งไกล) มีผลกระทบนี้เนื่องจากแคช
Lutz Prechelt

22
@Sandeep ใช่ โปรเซสเซอร์ยังคงมีการทำนายสาขา หากมีการเปลี่ยนแปลงอะไรมันเป็นคอมไพเลอร์ ทุกวันนี้ฉันคิดว่าพวกเขามีแนวโน้มที่จะทำสิ่งที่ ICC และ GCC (ภายใต้ -O3) ทำที่นี่นั่นคือเอาสาขาออก เมื่อพิจารณาว่าคำถามนี้มีความเป็นไปได้สูงเพียงใดมันเป็นไปได้มากที่คอมไพเลอร์ได้รับการอัปเดตเพื่อจัดการเคสในคำถามนี้โดยเฉพาะ ให้ความสนใจกับ SO อย่างแน่นอน และเกิดขึ้นกับคำถามนี้ที่ GCC อัปเดตภายใน 3 สัปดาห์ ฉันไม่เห็นว่าทำไมมันจะไม่เกิดขึ้นที่นี่เช่นกัน
Mysticial

4086

การพยากรณ์สาขา

ด้วยอาร์เรย์ที่เรียงลำดับเงื่อนไขdata[c] >= 128จะเป็นอันดับแรกfalseสำหรับช่วงค่าจากนั้นจะกลายเป็นtrueค่าในภายหลังทั้งหมด ง่ายต่อการคาดเดา ด้วยอาร์เรย์ที่ไม่เรียงลำดับคุณจะต้องชำระค่าใช้จ่ายในการแยกสาขา


105
การคาดคะเนสาขาทำงานได้ดีกว่าในอาร์เรย์ที่เรียงลำดับกับอาร์เรย์ที่มีรูปแบบแตกต่างกันหรือไม่ ตัวอย่างเช่นสำหรับอาร์เรย์ -> {10, 5, 20, 10, 40, 20, ... } องค์ประกอบถัดไปในอาร์เรย์จากรูปแบบคือ 80 อาร์เรย์ประเภทนี้จะถูกเร่งโดยการทำนายของสาขาใน องค์ประกอบถัดไปคือ 80 ที่นี่หากรูปแบบมีการติดตาม? หรือมันมักจะช่วยเฉพาะกับอาร์เรย์ที่เรียงลำดับ?
Adam Freeman

133
ดังนั้นโดยทั่วไปทุกอย่างที่ฉันเรียนรู้เกี่ยวกับ big-O นอกหน้าต่าง? ดีกว่าที่จะต้องเสียค่าใช้จ่ายในการคัดแยกกว่าต้นทุนการแยกย่อย?
Agrim Pathak

133
@AgrimPathak นั่นขึ้นอยู่กับ สำหรับอินพุตที่ไม่ใหญ่เกินไปอัลกอริทึมที่มีความซับซ้อนสูงกว่าจะเร็วกว่าอัลกอริทึมที่มีความซับซ้อนลดลงเมื่อค่าคงที่มีขนาดเล็กลงสำหรับอัลกอริทึมที่มีความซับซ้อนสูงกว่า จุดที่จุดคุ้มทุนสามารถทำนายได้ยาก นอกจากนี้เมื่อเปรียบเทียบกับบริเวณใกล้เคียงก็มีความสำคัญเช่นกัน Big-O เป็นสิ่งสำคัญ แต่ไม่ได้เป็นเพียงเกณฑ์เดียวสำหรับประสิทธิภาพ
Daniel Fischer

65
การคาดคะเนสาขาเกิดขึ้นเมื่อใด ภาษาจะรู้ได้อย่างไรว่าอาเรย์นั้นถูกเรียงลำดับ? ฉันกำลังนึกถึงสถานการณ์ของอาเรย์ที่ดูเหมือนว่า: [1,2,3,4,5, ... 998,999,1000, 3, 10001, 10002]? จะคลุมเครือ 3 เพิ่มเวลาทำงานหรือไม่ มันจะเป็นตราบเท่าที่อาร์เรย์ไม่ได้เรียงลำดับ?
Filip Bartuzi

63
การคาดคะเน @FilipBartuzi Branch เกิดขึ้นในตัวประมวลผลต่ำกว่าระดับภาษา (แต่ภาษาอาจเสนอวิธีที่จะบอกคอมไพเลอร์ว่ามีอะไรเป็นไปได้ดังนั้นคอมไพเลอร์สามารถเปล่งรหัสที่เหมาะสมกับสิ่งนั้น) ในตัวอย่างของคุณ out-of-order 3 จะนำไปสู่การคาดคะเนสาขา (สำหรับเงื่อนไขที่เหมาะสมโดยที่ 3 ให้ผลลัพธ์ที่แตกต่างกว่า 1,000) และดังนั้นการประมวลผลอาร์เรย์นั้นน่าจะใช้เวลานานหลายสิบสองหรือร้อยนาโนวินาทีนานกว่า อาร์เรย์ที่เรียงลำดับจะสังเกตได้ยาก เวลาใดที่ฉันต้องเสียค่าใช้จ่ายในการคาดคะเนความผิดพลาดสูงหนึ่งครั้งต่อการคาดคะเนผิด 1,000 ครั้งนั้นไม่มากนัก
Daniel Fischer

3310

เหตุผลว่าทำไมผลการดำเนินงานปรับปรุงอย่างมากเมื่อมีข้อมูลที่ถูกจัดเรียงคือว่าโทษทำนายสาขาจะถูกลบออกตามที่อธิบายไว้อย่างสวยงามในคำตอบของ Mysticial

ทีนี้ถ้าเราดูรหัส

if (data[c] >= 128)
    sum += data[c];

เราสามารถพบว่าความหมายของif... else...สาขาเฉพาะนี้คือการเพิ่มบางอย่างเมื่อมีเงื่อนไขเป็นที่พอใจ สาขาประเภทนี้สามารถเปลี่ยนเป็นคำสั่งย้ายแบบมีเงื่อนไขได้ง่ายซึ่งจะรวบรวมไว้ในคำสั่งการย้ายตามเงื่อนไข: cmovlในx86ระบบ สาขาและดังนั้นการลงโทษสาขาที่มีศักยภาพจะถูกลบออก

ในCดังนั้นC++คำสั่งซึ่งจะรวบรวมโดยตรง (โดยไม่ต้องเพิ่มประสิทธิภาพใด ๆ ) ลงในคำสั่งย้ายตามเงื่อนไขในการเป็นผู้ประกอบการที่ประกอบไปด้วยx86 ... ? ... : ...ดังนั้นเราจึงเขียนข้อความข้างต้นเป็นข้อความที่เทียบเท่า:

sum += data[c] >=128 ? data[c] : 0;

ในขณะที่ยังคงสามารถอ่านได้เราสามารถตรวจสอบปัจจัยเร่งความเร็วได้

สำหรับ Intel Core i7 -2600K @ 3.4 GHz และโหมดการเผยแพร่ Visual Studio 2010 มาตรฐานคือ (รูปแบบที่คัดลอกมาจาก Mysticial):

x86

//  Branch - Random
seconds = 8.885

//  Branch - Sorted
seconds = 1.528

//  Branchless - Random
seconds = 3.716

//  Branchless - Sorted
seconds = 3.71

x64

//  Branch - Random
seconds = 11.302

//  Branch - Sorted
 seconds = 1.830

//  Branchless - Random
seconds = 2.736

//  Branchless - Sorted
seconds = 2.737

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

ตอนนี้ให้ดูอย่างใกล้ชิดยิ่งขึ้นโดยการตรวจสอบx86ชุดประกอบที่พวกเขาสร้างขึ้น เพื่อความง่ายเราใช้สองฟังก์ชันmax1และmax2และ

max1ใช้สาขาที่มีเงื่อนไขif... else ...:

int max1(int a, int b) {
    if (a > b)
        return a;
    else
        return b;
}

max2ใช้ผู้ประกอบการที่ประกอบไปด้วย... ? ... : ...:

int max2(int a, int b) {
    return a > b ? a : b;
}

บนเครื่อง x86-64 ให้GCC -Sสร้างชุดประกอบด้านล่าง

:max1
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    -8(%rbp), %eax
    jle     .L2
    movl    -4(%rbp), %eax
    movl    %eax, -12(%rbp)
    jmp     .L4
.L2:
    movl    -8(%rbp), %eax
    movl    %eax, -12(%rbp)
.L4:
    movl    -12(%rbp), %eax
    leave
    ret

:max2
    movl    %edi, -4(%rbp)
    movl    %esi, -8(%rbp)
    movl    -4(%rbp), %eax
    cmpl    %eax, -8(%rbp)
    cmovge  -8(%rbp), %eax
    leave
    ret

max2cmovgeใช้รหัสมากน้อยเนื่องจากการใช้งานการเรียนการสอน แต่กำไรที่แท้จริงคือmax2ไม่ได้เกี่ยวข้องกับการกระโดดข้ามสาขาjmpซึ่งจะมีโทษประสิทธิภาพที่สำคัญหากผลลัพธ์ที่คาดการณ์ไม่ถูกต้อง

แล้วทำไมการเคลื่อนไหวแบบมีเงื่อนไขจึงทำได้ดีกว่า

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

ในกรณีสาขาคำสั่งต่อไปนี้จะถูกกำหนดโดยคำสั่งก่อนหน้าดังนั้นเราจึงไม่สามารถทำการวางท่อได้ เราต้องรอหรือทำนาย

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

หนังสือระบบคอมพิวเตอร์: มุมมองของโปรแกรมเมอร์ฉบับที่สองอธิบายรายละเอียดนี้ คุณสามารถตรวจสอบมาตรา 3.6.6 สำหรับเงื่อนไขย้ายคำแนะนำ , ทั้งบทที่ 4 สำหรับการประมวลผลสถาปัตยกรรมและมาตรา 5.11.2 สำหรับการรักษาพิเศษสำหรับสาขาการทำนายและเบี้ยปรับ misprediction

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


7
@ BlueRaja-DannyPflughoeft นี่เป็นรุ่นที่ไม่ได้รับการปรับปรุง คอมไพเลอร์ไม่ได้เพิ่มประสิทธิภาพผู้ประกอบการ ternary เพียงแค่แปลมัน GCC สามารถปรับให้เหมาะสมถ้าเป็นเช่นนั้นหากมีระดับการเพิ่มประสิทธิภาพที่เพียงพออย่างไรก็ตามสิ่งนี้แสดงให้เห็นถึงพลังของการย้ายตามเงื่อนไขและการปรับให้เหมาะสมด้วยตนเองสร้างความแตกต่าง
WiSaGaN

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

55
@WiSaGaN downvote ของฉันจะกลายเป็น upvote แน่นอนถ้าคุณแก้ไขคำตอบเพื่อลบ-O0ตัวอย่างที่ทำให้เข้าใจผิดและแสดงความแตกต่างในasm ที่ได้รับการปรับปรุงให้ดีที่สุดใน testcase ทั้งสองของคุณ
Justin L.

56
@UpAndAdam ในช่วงเวลาของการทดสอบ VS2010 ไม่สามารถปรับสาขาเดิมให้อยู่ในสภาพที่เคลื่อนไหวได้แม้จะระบุระดับการเพิ่มประสิทธิภาพสูงในขณะที่ gcc สามารถทำได้
WiSaGaN

9
เคล็ดลับผู้ประกอบการที่สามนี้ทำงานได้อย่างสวยงามสำหรับ Java หลังจากอ่านคำตอบของ Mystical ฉันสงสัยว่าสิ่งใดที่ Java สามารถทำได้เพื่อหลีกเลี่ยงการทำนายสาขาที่ผิดเนื่องจาก Java ไม่มีอะไรเทียบเท่ากับ -O3 ผู้ประกอบการที่ประกอบไปด้วย: 2.1943s และเป็นต้นฉบับ: 6.0303 วินาที
Kin Cheung

2271

หากคุณสงสัยเกี่ยวกับการปรับให้เหมาะสมยิ่งขึ้นที่สามารถทำได้กับรหัสนี้ให้พิจารณาสิ่งนี้:

เริ่มต้นด้วยการวนซ้ำดั้งเดิม:

for (unsigned i = 0; i < 100000; ++i)
{
    for (unsigned j = 0; j < arraySize; ++j)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

ด้วยการแลกเปลี่ยนลูปเราสามารถเปลี่ยนลูปนี้เป็น:

for (unsigned j = 0; j < arraySize; ++j)
{
    for (unsigned i = 0; i < 100000; ++i)
    {
        if (data[j] >= 128)
            sum += data[j];
    }
}

จากนั้นคุณจะเห็นว่าifเงื่อนไขนั้นคงที่ตลอดการประมวลผลของiลูปดังนั้นคุณจึงสามารถยกifออก:

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        for (unsigned i = 0; i < 100000; ++i)
        {
            sum += data[j];
        }
    }
}

จากนั้นคุณจะเห็นว่าวงในสามารถยุบลงในนิพจน์เดียวโดยสมมติว่าโมเดลจุดลอยตัวอนุญาตให้มัน ( /fp:fastถูกโยนทิ้งเป็นต้น)

for (unsigned j = 0; j < arraySize; ++j)
{
    if (data[j] >= 128)
    {
        sum += data[j] * 100000;
    }
}

อันนั้นเร็วกว่า 100,000 เท่า


276
หากคุณต้องการโกงคุณอาจใช้การคูณนอกวงและทำผลรวม * = 100000 หลังจากวนรอบ
Jyaif

78
@Michael - ผมเชื่อว่าตัวอย่างนี้เป็นจริงตัวอย่างหนึ่งของการยกวงคงที่ (LIH) การเพิ่มประสิทธิภาพและไม่แลกเปลี่ยนห่วง ในกรณีนี้ทั้งวงภายในเป็นอิสระจากวงด้านนอกและสามารถยกออกมาจากวงด้านนอกดังนั้นผลลัพธ์จะถูกคูณด้วยผลรวมiของหนึ่งหน่วย = 1e5 มันไม่ได้ทำให้ความแตกต่างกับผลลัพธ์สุดท้าย แต่ฉันแค่อยากจะบันทึกตรงเพราะนี่เป็นหน้าบ่อย
Yair Altman

54
แม้ว่าจะไม่ได้อยู่ในจิตวิญญาณที่เรียบง่ายของการสลับลูป แต่ภายในifจุดนี้สามารถเปลี่ยนเป็น: sum += (data[j] >= 128) ? data[j] * 100000 : 0;ซึ่งคอมไพเลอร์อาจลดcmovgeหรือเทียบเท่า
Alex North-Keys

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

34
@saurabheights: คำถามที่ไม่ถูกต้อง: ทำไมคอมไพเลอร์จะไม่วนการแลกเปลี่ยน Microbenchmarks นั้นยาก;)
Matthieu M.

1884

ไม่ต้องสงสัยเลยว่าพวกเราบางคนอาจสนใจวิธีการระบุรหัสที่เป็นปัญหาสำหรับตัวทำนายสาขาของ CPU เครื่องมือ Valgrind cachegrindมีเครื่องมือจำลองการคาดการณ์สาขาซึ่งเปิดใช้งานโดยใช้--branch-sim=yesแฟล็ก เรียกใช้ผ่านตัวอย่างในคำถามนี้ด้วยจำนวนลูปด้านนอกลดลงเหลือ 10,000 แล้วคอมไพล์ด้วยg++ให้ผลลัพธ์เหล่านี้:

เรียง:

==32551== Branches:        656,645,130  (  656,609,208 cond +    35,922 ind)
==32551== Mispredicts:         169,556  (      169,095 cond +       461 ind)
==32551== Mispred rate:            0.0% (          0.0%     +       1.2%   )

ไม่ได้เรียงลำดับ:

==32555== Branches:        655,996,082  (  655,960,160 cond +  35,922 ind)
==32555== Mispredicts:     164,073,152  (  164,072,692 cond +     460 ind)
==32555== Mispred rate:           25.0% (         25.0%     +     1.2%   )

เจาะลึกลงในเอาต์พุตแบบบรรทัดต่อบรรทัดที่ผลิตโดยcg_annotateเราเห็นลูปในคำถาม:

เรียง:

          Bc    Bcm Bi Bim
      10,001      4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .      .  .   .      {
           .      .  .   .          // primary loop
 327,690,000 10,016  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .      .  .   .          {
 327,680,000 10,006  0   0              if (data[c] >= 128)
           0      0  0   0                  sum += data[c];
           .      .  .   .          }
           .      .  .   .      }

ไม่ได้เรียงลำดับ:

          Bc         Bcm Bi Bim
      10,001           4  0   0      for (unsigned i = 0; i < 10000; ++i)
           .           .  .   .      {
           .           .  .   .          // primary loop
 327,690,000      10,038  0   0          for (unsigned c = 0; c < arraySize; ++c)
           .           .  .   .          {
 327,680,000 164,050,007  0   0              if (data[c] >= 128)
           0           0  0   0                  sum += data[c];
           .           .  .   .          }
           .           .  .   .      }

สิ่งนี้ช่วยให้คุณระบุบรรทัดที่มีปัญหาได้อย่างง่ายดาย - ในเวอร์ชันที่ไม่เรียงลำดับif (data[c] >= 128)บรรทัดจะทำให้สาขาที่มีเงื่อนไขผิดเพี้ยนจำนวน 164,050,007 รายการ ( Bcm) ภายใต้โมเดลตัวทำนายสาขาของ cachegrind ในขณะที่มันทำให้ 10,006 ในเวอร์ชันที่เรียงลำดับเท่านั้น


อีกวิธีหนึ่งบน Linux คุณสามารถใช้ระบบย่อยเคาน์เตอร์วัดประสิทธิภาพเพื่อทำงานเดียวกันให้สำเร็จ แต่ด้วยประสิทธิภาพดั้งเดิมโดยใช้เคาน์เตอร์ CPU

perf stat ./sumtest_sorted

เรียง:

 Performance counter stats for './sumtest_sorted':

  11808.095776 task-clock                #    0.998 CPUs utilized          
         1,062 context-switches          #    0.090 K/sec                  
            14 CPU-migrations            #    0.001 K/sec                  
           337 page-faults               #    0.029 K/sec                  
26,487,882,764 cycles                    #    2.243 GHz                    
41,025,654,322 instructions              #    1.55  insns per cycle        
 6,558,871,379 branches                  #  555.455 M/sec                  
       567,204 branch-misses             #    0.01% of all branches        

  11.827228330 seconds time elapsed

ไม่ได้เรียงลำดับ:

 Performance counter stats for './sumtest_unsorted':

  28877.954344 task-clock                #    0.998 CPUs utilized          
         2,584 context-switches          #    0.089 K/sec                  
            18 CPU-migrations            #    0.001 K/sec                  
           335 page-faults               #    0.012 K/sec                  
65,076,127,595 cycles                    #    2.253 GHz                    
41,032,528,741 instructions              #    0.63  insns per cycle        
 6,560,579,013 branches                  #  227.183 M/sec                  
 1,646,394,749 branch-misses             #   25.10% of all branches        

  28.935500947 seconds time elapsed

นอกจากนี้ยังสามารถทำบันทึกย่อซอร์สโค้ดด้วยการแยกส่วน

perf record -e branch-misses ./sumtest_unsorted
perf annotate -d sumtest_unsorted
 Percent |      Source code & Disassembly of sumtest_unsorted
------------------------------------------------
...
         :                      sum += data[c];
    0.00 :        400a1a:       mov    -0x14(%rbp),%eax
   39.97 :        400a1d:       mov    %eax,%eax
    5.31 :        400a1f:       mov    -0x20040(%rbp,%rax,4),%eax
    4.60 :        400a26:       cltq   
    0.00 :        400a28:       add    %rax,-0x30(%rbp)
...

ดูการสอนเกี่ยวกับประสิทธิภาพสำหรับรายละเอียดเพิ่มเติม


74
นี่น่ากลัวในรายการที่ไม่เรียงลำดับน่าจะมีโอกาส 50% ที่จะกดปุ่มเพิ่ม อย่างไรก็ตามการคาดการณ์ของสาขามีอัตราพลาด 25% เท่านั้นมันจะดีกว่า 50% ได้อย่างไร
TallBrian

128
@ tall.b.lo: 25% เป็นของทุกสาขา - มีสองสาขาในลูปหนึ่งสำหรับdata[c] >= 128(ซึ่งมีอัตราพลาด 50% ตามที่คุณแนะนำ) และอีกหนึ่งสำหรับสภาพลูปc < arraySizeที่มีอัตราพลาด ~ 0% .
caf

1340

ฉันเพิ่งอ่านคำถามนี้และคำตอบของมันและฉันรู้สึกว่าคำตอบนั้นหายไป

วิธีทั่วไปในการกำจัดการคาดคะเนสาขาที่ฉันพบว่าทำงานได้ดีโดยเฉพาะในภาษาที่มีการจัดการคือการค้นหาตารางแทนที่จะใช้สาขา (แม้ว่าฉันจะไม่ได้ทดสอบในกรณีนี้)

วิธีนี้ใช้ได้ผลโดยทั่วไปหาก:

  1. มันเป็นตารางเล็ก ๆ และน่าจะถูกแคชในโปรเซสเซอร์และ
  2. คุณกำลังรันบางสิ่งในลูปที่ค่อนข้างแน่นและ / หรือโปรเซสเซอร์สามารถโหลดข้อมูลล่วงหน้าได้

ความเป็นมาและเหตุผล

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

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

โชคดีสำหรับเราถ้ารูปแบบการเข้าถึงหน่วยความจำสามารถคาดเดาได้โปรเซสเซอร์จะโหลดในแคชอย่างรวดเร็วและทั้งหมดเป็นอย่างดี

สิ่งแรกที่เราต้องรู้คืออะไรเล็ก ? ในขณะที่ขนาดเล็กโดยทั่วไปจะดีกว่ากฎของหัวแม่มือคือติดกับตารางการค้นหาที่มีขนาด <= 4096 ไบต์ ในฐานะที่เป็นขีด จำกัด สูงสุด: หากตารางการค้นหาของคุณมีขนาดใหญ่กว่า 64K อาจเป็นเพราะการพิจารณาใหม่

การสร้างตาราง

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

ในกรณีนี้:> = 128 หมายถึงเราสามารถเก็บค่าไว้ได้ <128 หมายถึงเรากำจัดมันทิ้ง วิธีที่ง่ายที่สุดในการทำเช่นนั้นคือใช้ 'และ' ถ้าเราเก็บไว้เราและมันด้วย 7FFFFFFF; ถ้าเราต้องการกำจัดมันเราและมันด้วย 0 ขอให้สังเกตว่า 128 คือพลังของ 2 - เพื่อให้เราสามารถไปข้างหน้าและสร้างตารางจำนวนเต็ม 32768/128 และเติมด้วยหนึ่งศูนย์และจำนวนมาก 7FFFFFFFF ของ

ภาษาที่มีการจัดการ

คุณอาจสงสัยว่าทำไมสิ่งนี้ถึงทำงานได้ดีในภาษาที่มีการจัดการ ท้ายที่สุดภาษาที่มีการจัดการตรวจสอบขอบเขตของอาร์เรย์ด้วยสาขาเพื่อให้แน่ใจว่าคุณจะไม่เลอะ ...

ก็ไม่แน่ ... :-)

มีงานบางส่วนในการกำจัดสาขานี้สำหรับภาษาที่มีการจัดการ ตัวอย่างเช่น:

for (int i = 0; i < array.Length; ++i)
{
   // Use array[i]
}

ในกรณีนี้เห็นได้ชัดว่าคอมไพเลอร์ว่าเงื่อนไขขอบเขตจะไม่ถูกตี อย่างน้อยคอมไพเลอร์ Microsoft JIT (แต่ฉันคาดว่า Java ทำสิ่งที่คล้ายกัน) จะสังเกตเห็นสิ่งนี้และลบเช็คทั้งหมด ว้าวนั่นหมายความว่าไม่มีสาขา ในทำนองเดียวกันมันจะจัดการกับกรณีที่ชัดเจนอื่น ๆ

หากคุณพบปัญหากับการค้นหาในภาษาที่มีการจัดการ - กุญแจสำคัญคือการเพิ่ม& 0x[something]FFFฟังก์ชั่นการค้นหาของคุณเพื่อให้การตรวจสอบขอบเขตสามารถคาดเดาได้และดูได้เร็วขึ้น

ผลของคดีนี้

// Generate data
int arraySize = 32768;
int[] data = new int[arraySize];

Random random = new Random(0);
for (int c = 0; c < arraySize; ++c)
{
    data[c] = random.Next(256);
}

/*To keep the spirit of the code intact, I'll make a separate lookup table
(I assume we cannot modify 'data' or the number of loops)*/

int[] lookup = new int[256];

for (int c = 0; c < 256; ++c)
{
    lookup[c] = (c >= 128) ? c : 0;
}

// Test
DateTime startTime = System.DateTime.Now;
long sum = 0;

for (int i = 0; i < 100000; ++i)
{
    // Primary loop
    for (int j = 0; j < arraySize; ++j)
    {
        /* Here you basically want to use simple operations - so no
        random branches, but things like &, |, *, -, +, etc. are fine. */
        sum += lookup[data[j]];
    }
}

DateTime endTime = System.DateTime.Now;
Console.WriteLine(endTime - startTime);
Console.WriteLine("sum = " + sum);
Console.ReadLine();

57
คุณต้องการข้ามตัวพยากรณ์สาขาทำไม มันเป็นการเพิ่มประสิทธิภาพ
Dustin Oprea

108
เนื่องจากไม่มีสาขาใดจะดีไปกว่าสาขา :-) ในหลาย ๆ สถานการณ์นี่จะเร็วกว่ามาก ... ถ้าคุณเพิ่มประสิทธิภาพมันคุ้มค่าที่จะลอง พวกเขายังใช้มันค่อนข้างน้อยใน f.ex graphics.stanford.edu/~seander/bithacks.html
atlaste

36
โดยทั่วไปตารางการค้นหาสามารถเร็ว แต่คุณได้ทำการทดสอบสำหรับเงื่อนไขนี้หรือไม่? คุณจะยังคงมีเงื่อนไขสาขาในรหัสของคุณตอนนี้มันจะถูกย้ายไปยังส่วนการสร้างตารางค้นหา คุณยังคงไม่ได้รับการสนับสนุนที่สมบูรณ์แบบ
Zain Rizvi

38
@ Zain ถ้าคุณต้องการที่จะรู้ว่า ... ใช่: 15 วินาทีกับสาขาและ 10 กับรุ่นของฉัน ไม่ว่าจะเป็นเทคนิคที่มีประโยชน์ที่จะรู้วิธีใด
atlaste

42
ทำไมไม่sum += lookup[data[j]]ที่lookupเป็นอาร์เรย์ 256 รายการที่คนแรกเป็นศูนย์และคนสุดท้ายที่ถูกเท่ากับดัชนี?
Kris Vandermotten

1200

เนื่องจากข้อมูลถูกกระจายระหว่าง 0 และ 255 เมื่ออาร์เรย์ถูกเรียงลำดับรอบครึ่งแรกของการวนซ้ำจะไม่ป้อนif-statement ( ifคำสั่งถูกแชร์ด้านล่าง)

if (data[c] >= 128)
    sum += data[c];

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

ลองทำเครื่องหมายบนม้านั่งเพื่อทำความเข้าใจให้ดีขึ้น

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

ลองวัดประสิทธิภาพของลูปนี้ด้วยเงื่อนไขที่ต่างกัน:

for (int i = 0; i < max; i++)
    if (condition)
        sum++;

นี่คือการกำหนดเวลาของการวนรอบที่มีรูปแบบที่เป็นเท็จที่แตกต่างกัน:

Condition                Pattern             Time (ms)
-------------------------------------------------------
(i & 0×80000000) == 0    T repeated          322

(i & 0xffffffff) == 0    F repeated          276

(i & 1) == 0             TF alternating      760

(i & 3) == 0             TFFFTFFF           513

(i & 2) == 0             TTFFTTFF           1675

(i & 4) == 0             TTTTFFFFTTTTFFFF   1275

(i & 8) == 0             8T 8F 8T 8F        752

(i & 16) == 0            16T 16F 16T 16F    490

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

ดังนั้นจึงไม่มีข้อสงสัยเกี่ยวกับผลกระทบของการทำนายสาขาที่มีต่อประสิทธิภาพ!


23
@MooingDuck 'เพราะมันจะไม่สร้างความแตกต่าง - ค่านั้นอาจเป็นอะไรก็ได้ แต่มันจะยังคงอยู่ในขอบเขตของเกณฑ์เหล่านี้ เหตุใดจึงต้องแสดงค่าแบบสุ่มเมื่อคุณทราบถึงขีด จำกัด แล้ว แม้ว่าฉันจะยอมรับว่าคุณสามารถแสดงได้เพื่อความสมบูรณ์และ 'เพียงเพื่อให้ได้'
cst1992

24
@ cst1992: ตอนนี้เวลาที่ช้าที่สุดของเขาคือ TTFFTTFFTTFF ซึ่งดูเหมือนกับตามนุษย์ของฉัน การสุ่มไม่สามารถคาดเดาได้โดยเนื้อแท้ดังนั้นจึงเป็นไปได้โดยสิ้นเชิงว่าจะช้าลงและอยู่นอกขีด จำกัด ที่แสดงไว้ที่นี่ OTOH อาจเป็นได้ว่า TTFFTTFF กระทบกรณีทางพยาธิวิทยาได้อย่างสมบูรณ์แบบ ไม่สามารถบอกได้เนื่องจากเขาไม่ได้แสดงเวลาแบบสุ่ม
Mooing Duck

21
@MooingDuck ต่อสายตามนุษย์ "TTFFTTFFTTFF" เป็นลำดับที่คาดเดาได้ แต่สิ่งที่เรากำลังพูดถึงที่นี่คือพฤติกรรมของตัวทำนายสาขาที่สร้างขึ้นในซีพียู ตัวทำนายสาขาไม่ใช่การจดจำรูปแบบ AI ระดับ มันง่ายมาก เมื่อคุณเลือกสาขามันไม่ดี ในรหัสส่วนใหญ่สาขาไปในลักษณะเดียวกันเกือบตลอดเวลา พิจารณาการวนรอบที่ดำเนินการพันครั้ง สาขาที่จุดสิ้นสุดของการวนซ้ำกลับไปที่จุดเริ่มต้นของการวนซ้ำ 999 ครั้งจากนั้นเวลาที่พันทำสิ่งที่แตกต่างกัน ตัวพยากรณ์สาขาแบบง่าย ๆ มักใช้งานได้ดี
steveha

18
@steveha: ฉันคิดว่าคุณกำลังตั้งสมมติฐานเกี่ยวกับการทำงานของตัวพยากรณ์สาขาของ CPU และฉันไม่เห็นด้วยกับวิธีการดังกล่าว ฉันไม่รู้ว่าตัวทำนายสาขาขั้นสูงเป็นอย่างไร แต่ฉันคิดว่ามันล้ำหน้ากว่าคุณมาก คุณอาจจะถูกต้อง แต่การวัดจะดีแน่นอน
Mooing Duck

5
@steveha: ตัวทำนายแบบปรับสองระดับสามารถล็อคเข้ากับรูปแบบ TTFFTTFF โดยไม่มีปัญหาใด ๆ "ตัวแปรของวิธีการทำนายนี้ใช้ในไมโครโปรเซสเซอร์ที่ทันสมัยที่สุด" การคาดคะเนสาขาท้องถิ่นและการพยากรณ์สาขาทั่วโลกขึ้นอยู่กับการทำนายแบบปรับตัวสองระดับซึ่งสามารถทำได้เช่นกัน "การคาดคะเนสาขาทั่วโลกนั้นใช้ในโปรเซสเซอร์ AMD และในโปรเซสเซอร์ Intel Pentium M, Core, Core 2 และ Silvermont-based Atom" นอกจากนี้ยังเพิ่มตัวพยากรณ์ที่ตกลงกัน, ตัวทำนายแบบผสม, การทำนายการกระโดดทางอ้อมในรายการนั้น การคาดการณ์แบบวนซ้ำจะไม่ล็อค แต่ทำคะแนนได้ 75% เหลือเพียง 2 ที่ไม่สามารถล็อคได้
Mooing Duck

1126

วิธีหนึ่งในการหลีกเลี่ยงข้อผิดพลาดในการทำนายสาขาคือการสร้างตารางการค้นหาและจัดทำดัชนีโดยใช้ข้อมูล Stefan de Bruijn พูดถึงเรื่องนี้ในคำตอบของเขา

แต่ในกรณีนี้เรารู้ว่าค่าอยู่ในช่วง [0, 255] และเราสนใจเฉพาะค่า> = 128 นั่นหมายความว่าเราสามารถแยกบิตเดียวที่จะบอกเราว่าเราต้องการค่าหรือไม่: โดยการเลื่อน ข้อมูลทางขวา 7 บิตเราเหลือ 0 บิตหรือ 1 บิตและเราต้องการเพิ่มค่าเมื่อเรามี 1 บิตเท่านั้น เรียกบิตนี้ว่า "บิตการตัดสินใจ"

ด้วยการใช้ค่า 0/1 ของบิตการตัดสินใจเป็นดัชนีลงในอาร์เรย์เราสามารถสร้างโค้ดที่จะเร็วพอ ๆ กันไม่ว่าจะเรียงลำดับข้อมูลหรือไม่เรียงลำดับ รหัสของเราจะเพิ่มมูลค่าเสมอ แต่เมื่อบิตการตัดสินใจเป็น 0 เราจะเพิ่มมูลค่าที่อื่นที่เราไม่สนใจ นี่คือรหัส:

// Test
clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

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

แต่ในการทดสอบของฉันตารางการค้นหาที่ชัดเจนนั้นเร็วกว่านี้เล็กน้อยอาจเป็นเพราะการจัดทำดัชนีในตารางการค้นหานั้นเร็วกว่าการเลื่อนบิตเล็กน้อย สิ่งนี้แสดงให้เห็นว่าโค้ดของฉันตั้งค่าและใช้ตารางการค้นหาอย่างไร (เรียกว่าlut"LookUp Table" ในโค้ด) อย่างไม่คาดคิด นี่คือรหัส C ++:

// Declare and then fill in the lookup table
int lut[256];
for (unsigned c = 0; c < 256; ++c)
    lut[c] = (c >= 128) ? c : 0;

// Use the lookup table after it is built
for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        sum += lut[data[c]];
    }
}

ในกรณีนี้ตารางการค้นหามีขนาดเพียง 256 ไบต์ดังนั้นมันจึงเหมาะกับแคชและทุกอย่างรวดเร็ว เทคนิคนี้ใช้ไม่ได้ผลถ้าข้อมูลมีค่า 24 บิตและเราต้องการเพียงครึ่งเดียว ... ตารางการค้นหาจะใหญ่เกินไปที่จะใช้งานได้จริง ในอีกทางหนึ่งเราสามารถรวมสองเทคนิคที่แสดงด้านบน: แรกเลื่อนบิตไปแล้วทำดัชนีตารางการค้นหา สำหรับค่า 24 บิตที่เราต้องการเพียงค่าครึ่งบนเราสามารถเปลี่ยนข้อมูลได้ 12 บิตและเหลือค่า 12 บิตสำหรับดัชนีตาราง ดัชนีตารางแบบ 12 บิตหมายถึงตารางค่า 4096 ซึ่งอาจเป็นประโยชน์

เทคนิคการจัดทำดัชนีเป็นอาร์เรย์แทนที่จะใช้ifคำสั่งสามารถใช้สำหรับการตัดสินใจว่าจะใช้ตัวชี้ใด ผมเห็นว่าการดำเนินการห้องสมุดต้นไม้ไบนารีและแทนที่จะมีสองชื่อชี้ ( pLeftและpRightหรืออะไรก็ตาม) มีความยาว 2 อาร์เรย์ของตัวชี้และใช้เทคนิค "การตัดสินใจบิต" ตัดสินใจที่หนึ่งที่จะปฏิบัติตาม ตัวอย่างเช่นแทนที่จะเป็น:

if (x < node->value)
    node = node->pLeft;
else
    node = node->pRight;

ห้องสมุดนี้จะทำสิ่งที่ชอบ:

i = (x < node->value);
node = node->link[i];

นี่คือลิงค์ไปยังรหัสนี้: ต้นไม้สีดำสีแดง , สับสนตลอดกาล


29
ใช่คุณสามารถใช้บิตโดยตรงและเพิ่มทวีคูณ ( data[c]>>7ซึ่งถูกกล่าวถึงที่นี่ด้วย) ฉันตั้งใจจะออกโซลูชันนี้ แต่คุณมั่นใจว่าถูกต้อง เพียงข้อสังเกตเล็ก ๆ : กฎง่ายๆสำหรับตารางการค้นหาคือถ้ามันเหมาะกับ 4KB (เนื่องจากการแคช) มันจะทำงานได้ - ควรทำให้โต๊ะเล็กที่สุดเท่าที่จะทำได้ สำหรับภาษาที่มีการจัดการฉันจะส่งไปที่ 64KB สำหรับภาษาระดับต่ำเช่น C ++ และ C ฉันอาจต้องพิจารณาอีกครั้ง (นั่นเป็นเพียงประสบการณ์ของฉัน) ตั้งแต่typeof(int) = 4ฉันพยายามติดสูงสุด 10 บิต
atlaste

17
ฉันคิดว่าการจัดทำดัชนีด้วยค่า 0/1 อาจจะเร็วกว่าจำนวนเต็มคูณ แต่ฉันเดาว่าถ้าประสิทธิภาพสำคัญมากคุณควรทำโปรไฟล์ ฉันยอมรับว่าตารางการค้นหาขนาดเล็กมีความสำคัญต่อการหลีกเลี่ยงความกดดันของแคช แต่ชัดเจนว่าถ้าคุณมีแคชที่ใหญ่กว่าคุณสามารถใช้ตารางการค้นหาที่ใหญ่กว่าได้ดังนั้น 4KB จึงเป็นกฎของหัวแม่มือมากกว่ากฎที่ยากกว่า ฉันคิดว่าคุณหมายถึงsizeof(int) == 4อะไร นั่นจะเป็นจริงสำหรับ 32 บิต โทรศัพท์มือถืออายุสองปีของฉันมีแคช 32KB L1 ดังนั้นแม้แต่ตารางการค้นหา 4K อาจใช้งานได้โดยเฉพาะอย่างยิ่งหากค่าการค้นหาเป็นไบต์แทนที่จะเป็น int
steveha

12
อาจเป็นไปได้ฉันหายไปบางสิ่งบางอย่าง แต่ในของคุณjเท่ากับ 0 หรือ 1 วิธีทำไมคุณไม่เพียงแค่คูณค่าของคุณโดยjก่อนที่จะเพิ่มมากกว่าการใช้การจัดทำดัชนีอาร์เรย์ (อาจจะควรจะคูณด้วย1-jมากกว่าj)
ริชาร์ดซ่า

6
การทวีคูณของ @steveha น่าจะเร็วกว่านี้ฉันลองค้นหามันในหนังสือ Intel แต่หาไม่เจอ ... ทั้งสองวิธีการเปรียบเทียบก็ให้ผลลัพธ์ที่นี่ด้วย
atlaste

10
@steveha PS: คำตอบที่เป็นไปได้อีกข้อหนึ่งคือint c = data[j]; sum += c & -(c >> 7);ไม่ต้องมีการคูณซ้ำ
atlaste

1021

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

แท้จริงอาร์เรย์แบ่งพาร์ติชันในโซนที่อยู่ติดกันด้วยและอีกด้วยdata < 128 data >= 128ดังนั้นคุณควรหาจุดพาร์ติชันที่มีการค้นหาแบบแบ่งขั้ว (โดยใช้Lg(arraySize) = 15การเปรียบเทียบ) จากนั้นทำการสะสมตรงจากจุดนั้น

สิ่งที่ชอบ (ไม่ถูกตรวจสอบ)

int i= 0, j, k= arraySize;
while (i < k)
{
  j= (i + k) >> 1;
  if (data[j] >= 128)
    k= j;
  else
    i= j;
}
sum= 0;
for (; i < arraySize; i++)
  sum+= data[i];

หรือสับสนเล็กน้อยมากขึ้น

int i, k, j= (i + k) >> 1;
for (i= 0, k= arraySize; i < k; (data[j] >= 128 ? k : i)= j)
  j= (i + k) >> 1;
for (sum= 0; i < arraySize; i++)
  sum+= data[i];

วิธีที่เร็วกว่าซึ่งให้วิธีแก้ปัญหาโดยประมาณสำหรับทั้งแบบเรียงลำดับและไม่เรียงลำดับคือ: sum= 3137536;(สมมติว่ามีการแจกแจงที่เหมือนกันอย่างแท้จริง 16384 ตัวอย่างที่มีค่าที่คาดหวัง 191.5) :-)


23
sum= 3137536- ฉลาด เห็นได้ชัดว่าไม่ใช่ประเด็นของคำถาม คำถามชัดเจนเกี่ยวกับการอธิบายลักษณะของประสิทธิภาพที่น่าแปลกใจ ฉันอยากจะบอกว่าการเพิ่มstd::partitionแทนที่จะทำstd::sortมีค่า แม้ว่าคำถามที่เกิดขึ้นจริงจะขยายไปถึงมากกว่าเกณฑ์มาตรฐานสังเคราะห์ที่ให้
sehe

12
@DeadMG: นี่ไม่ใช่การค้นหาแบบแบ่งขั้วมาตรฐานสำหรับคีย์ที่กำหนด แต่เป็นการค้นหาดัชนีการแบ่งพาร์ติชัน มันต้องเปรียบเทียบเดียวต่อการทำซ้ำ แต่อย่าพึ่งรหัสนี้ฉันยังไม่ได้ตรวจสอบ หากคุณมีความสนใจในการดำเนินการที่ถูกต้องแจ้งให้เราทราบ
Yves Daoust

831

พฤติกรรมข้างต้นเกิดขึ้นเนื่องจากการพยากรณ์สาขา

เพื่อให้เข้าใจคำพยากรณ์สาขาต้องเข้าใจก่อน คำสั่งไปป์ไลน์ก่อน :

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

โดยทั่วไปโปรเซสเซอร์ที่ทันสมัยมีท่อค่อนข้างยาว แต่เพื่อความสะดวกลองพิจารณา 4 ขั้นตอนเหล่านี้เท่านั้น

  1. IF - ดึงคำสั่งจากหน่วยความจำ
  2. ID - ถอดรหัสคำสั่ง
  3. EX - ดำเนินการคำสั่ง
  4. WB - เขียนกลับไปที่ CPU register

ขั้นตอนโดยทั่วไปสำหรับ 4 ขั้นตอนสำหรับ 2 ขั้นตอน ท่อส่ง 4 ขั้นตอนโดยทั่วไป

ย้ายกลับไปที่คำถามข้างต้นลองพิจารณาคำแนะนำต่อไปนี้:

                        A) if (data[c] >= 128)
                                /\
                               /  \
                              /    \
                        true /      \ false
                            /        \
                           /          \
                          /            \
                         /              \
              B) sum += data[c];          C) for loop or print().

หากไม่มีการพยากรณ์สาขาต่อไปนี้จะเกิดขึ้น:

ในการดำเนินการคำสั่ง B หรือคำสั่ง C โปรเซสเซอร์จะต้องรอจนกว่าคำสั่ง A ไม่ถึงจนถึงขั้นตอน EX ในท่อเนื่องจากการตัดสินใจที่จะไปใช้คำสั่ง B หรือคำสั่ง C ขึ้นอยู่กับผลของคำสั่ง A ดังนั้นท่อ จะมีลักษณะเช่นนี้

เมื่อหากเงื่อนไขส่งคืนจริง: ป้อนคำอธิบายรูปภาพที่นี่

เมื่อหากเงื่อนไขคืนค่าเท็จ ป้อนคำอธิบายรูปภาพที่นี่

จากการรอผลการเรียนการสอน A รอบ CPU ทั้งหมดที่ใช้ในกรณีข้างต้น (โดยไม่มีการทำนายสาขา; ทั้งจริงและเท็จ) คือ 7

ดังนั้นการทำนายสาขาคืออะไร?

ตัวทำนายสาขาจะพยายามเดาว่าสาขาใด (โครงสร้าง if-then-else) จะไปก่อนหน้านี้ซึ่งเป็นที่รู้จักกันอย่างแน่นอน มันจะไม่รอให้คำสั่ง A ไปถึงขั้นตอน EX ของไปป์ไลน์ แต่มันจะเดาการตัดสินใจและไปที่คำสั่งนั้น (B หรือ C ในกรณีของตัวอย่างของเรา)

ในกรณีที่คาดเดาถูกต้องไปป์ไลน์จะมีลักษณะดังนี้: ป้อนคำอธิบายรูปภาพที่นี่

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

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

  1. องค์ประกอบทั้งหมดน้อยกว่า 128
  2. องค์ประกอบทั้งหมดมีค่ามากกว่า 128
  3. องค์ประกอบใหม่ที่เริ่มต้นบางรายการมีค่าน้อยกว่า 128 และต่อมาจะมีค่ามากกว่า 128

ให้เราสมมติว่าตัวทำนายจะถือว่าสาขาที่แท้จริงในการรันครั้งแรกเสมอ

ดังนั้นในกรณีแรกมันจะยึดเอาสาขาที่แท้จริงอยู่เสมอเนื่องจากในอดีตการทำนายทั้งหมดนั้นถูกต้อง ในกรณีที่ 2 เริ่มแรกมันจะทำนายผิด แต่หลังจากการทำซ้ำสองสามครั้งมันจะทำนายได้อย่างถูกต้อง ในกรณีที่ 3 มันจะทำนายอย่างถูกต้องในตอนแรกจนกระทั่งองค์ประกอบมีค่าน้อยกว่า 128 หลังจากนั้นมันจะล้มเหลวบางครั้งและถูกต้องเมื่อมันเห็นความล้มเหลวในการทำนายสาขาในประวัติศาสตร์

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

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


1
คำสั่งสองคำสั่งดำเนินการร่วมกันอย่างไร ทำได้ด้วยการแยกแกน cpu หรือคำสั่งไพพ์ไลน์รวมอยู่ในแกนซีพียูเดี่ยวหรือไม่?
M.kazem Akhgary

1
@ M.kazemAkhgary ทุกอย่างอยู่ในแกนตรรกะเดียว หากคุณสนใจนี่เป็นตัวอย่างที่อธิบายไว้ในคู่มือนักพัฒนาซอฟต์แวร์ของ Intel
Sergey.quixoticaxis.Ivanov

727

คำตอบอย่างเป็นทางการจะมาจาก

  1. Intel - หลีกเลี่ยงค่าใช้จ่ายในการวินิจฉัยผิดสาขา
  2. Intel - การปรับโครงสร้างสาขาและวนซ้ำเพื่อป้องกันการคาดการณ์ที่ผิดพลาด
  3. เอกสารวิทยาศาสตร์ - สถาปัตยกรรมคอมพิวเตอร์ทำนายสาขา
  4. หนังสือ: JL Hennessy, DA Patterson: สถาปัตยกรรมคอมพิวเตอร์: แนวทางเชิงปริมาณ
  5. บทความในสิ่งพิมพ์ทางวิทยาศาสตร์: TY Yeh, YN Patt ทำสิ่งเหล่านี้มากมายในการทำนายสาขา

คุณสามารถเห็นได้จากแผนภาพที่น่ารักนี้ว่าทำไมตัวพยากรณ์สาขาถึงสับสน

แผนภาพสถานะ 2 บิต

องค์ประกอบในรหัสต้นฉบับแต่ละค่าสุ่ม

data[c] = std::rand() % 256;

ดังนั้นตัวทำนายจะเปลี่ยนด้านเมื่อเกิดการstd::rand()ระเบิด

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



696

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

if (likely( everything_is_ok ))
{
    /* Do something */
}

หรือในทำนองเดียวกัน:

if (unlikely(very_improbable_condition))
{
    /* Do something */    
}

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

โดยทั่วไปแล้วการเพิ่มประสิทธิภาพประเภทนี้ส่วนใหญ่จะพบในแอปพลิเคชันแบบเรียลไทม์หรือระบบฝังตัวที่เวลาการดำเนินการมีความสำคัญและเป็นสิ่งสำคัญ ตัวอย่างเช่นหากคุณกำลังตรวจสอบเงื่อนไขข้อผิดพลาดบางอย่างที่เกิดขึ้น 1/10000 ครั้งเท่านั้นแล้วทำไมไม่แจ้งคอมไพเลอร์เกี่ยวกับเรื่องนี้? วิธีนี้ตามค่าเริ่มต้นการคาดคะเนสาขาจะถือว่าเงื่อนไขเป็นเท็จ


678

การใช้งานบูลีนที่ใช้บ่อยใน C ++ จะสร้างสาขาจำนวนมากในโปรแกรมที่คอมไพล์ หากสาขาเหล่านี้อยู่ในลูปและยากต่อการคาดการณ์พวกเขาสามารถชะลอการดำเนินการได้อย่างมีนัยสำคัญ ตัวแปรบูลีนจะถูกเก็บเป็นจำนวนเต็ม 8 บิตที่มีค่า0สำหรับfalseและสำหรับ1true

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

bool a, b, c, d;
c = a && b;
d = a || b;

โดยทั่วไปจะมีการใช้งานคอมไพเลอร์ด้วยวิธีดังต่อไปนี้:

bool a, b, c, d;
if (a != 0) {
    if (b != 0) {
        c = 1;
    }
    else {
        goto CFALSE;
    }
}
else {
    CFALSE:
    c = 0;
}
if (a == 0) {
    if (b == 0) {
        d = 0;
    }
    else {
        goto DTRUE;
    }
}
else {
    DTRUE:
    d = 1;
}

รหัสนี้อยู่ไกลจากที่ดีที่สุด สาขาอาจใช้เวลานานในกรณีที่มีการตัดสินผิด การดำเนินงานแบบบูลสามารถทำได้มีประสิทธิภาพมากขึ้นถ้ามันเป็นที่รู้จักกันด้วยความมั่นใจว่าตัวถูกดำเนินการมีค่าไม่มีอื่น ๆ กว่าและ0 1เหตุผลที่คอมไพเลอร์ไม่ได้ตั้งสมมติฐานเช่นนี้ว่าตัวแปรอาจมีค่าอื่น ๆ หากไม่มีการกำหนดค่าเริ่มต้นหรือมาจากแหล่งที่ไม่รู้จัก รหัสข้างต้นสามารถปรับให้เหมาะสมหากaและbได้รับการเริ่มต้นเป็นค่าที่ถูกต้องหรือถ้าพวกเขามาจากผู้ประกอบการที่ผลิตส่งออกบูลีน รหัสที่ได้รับการปรับปรุงจะมีลักษณะดังนี้:

char a = 0, b = 1, c, d;
c = a & b;
d = a | b;

charถูกใช้แทนboolเพื่อให้สามารถใช้ตัวดำเนินการ bitwise ( &และ|) แทนตัวดำเนินการบูลีน ( &&และ||) ตัวดำเนินการระดับบิตเป็นคำสั่งเดียวที่ใช้เวลาหนึ่งรอบนาฬิกา ผู้ประกอบการหรือ ( |) ทำงานแม้ว่าaและbมีค่าอื่น ๆ กว่าหรือ0 1และผู้ประกอบการ ( &) และ EXCLUSIVE หรือผู้ประกอบการ ( ^) อาจให้ผลลัพธ์ที่สอดคล้องกันถ้าตัวถูกดำเนินการมีค่าอื่น ๆ กว่าและ01

~ไม่สามารถใช้เพื่อไม่ได้ แต่คุณสามารถสร้าง Boolean NOT บนตัวแปรที่ทราบกันว่าเป็น0หรือ1โดย XOR โดยใช้1:

bool a, b;
b = !a;

สามารถปรับให้เหมาะกับ:

char a = 0, b;
b = a ^ 1;

a && bไม่สามารถถูกแทนที่ด้วยa & bถ้าbเป็นการแสดงออกที่ไม่ควรได้รับการประเมินถ้าaเป็นfalse( &&จะไม่ประเมินb, &จะ) ในทำนองเดียวกันa || bไม่สามารถถูกแทนที่ด้วยa | bถ้าbเป็นการแสดงออกที่ไม่ควรได้รับการประเมินถ้ามีatrue

การใช้ตัวดำเนินการระดับบิตมีประโยชน์มากขึ้นถ้าตัวถูกดำเนินการเป็นตัวแปรมากกว่าตัวถูกดำเนินการเปรียบเทียบ

bool a; double x, y, z;
a = x > y && z < 5.0;

เหมาะสมที่สุดในกรณีส่วนใหญ่ (เว้นแต่ว่าคุณคาดหวังว่า&&นิพจน์จะสร้างข้อผิดพลาดหลายสาขา)


341

แน่นอน! ...

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

หากมีการจัดเรียงอาร์เรย์เงื่อนไขของคุณจะเป็นเท็จในขั้นตอนแรก: data[c] >= 128จากนั้นจะกลายเป็นมูลค่าที่แท้จริงตลอดทางจนถึงจุดสิ้นสุดของถนน นั่นเป็นวิธีที่คุณจะไปถึงจุดสิ้นสุดของตรรกะได้เร็วขึ้น ในทางกลับกันการใช้อาเรย์ที่ไม่ได้เรียงลำดับคุณต้องมีการพลิกและการประมวลผลจำนวนมากซึ่งทำให้โค้ดของคุณทำงานช้าลงอย่างแน่นอน ...

ดูภาพที่ฉันสร้างให้คุณด้านล่าง ถนนสายไหนจะเสร็จเร็วขึ้น?

การทำนายสาขา

ดังนั้นทางโปรแกรมทำนายสาขาทำให้กระบวนการช้าลง ...

นอกจากนี้ในตอนท้ายเราควรทราบว่าเรามีการคาดการณ์ของสาขาสองประเภทที่แต่ละคนจะมีผลต่อรหัสของคุณแตกต่างกัน:

1. คงที่

2. แบบไดนามิก

การทำนายสาขา

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

ในการเขียนโค้ดของคุณอย่างมีประสิทธิภาพเพื่อใช้ประโยชน์จากกฎเหล่านี้เมื่อเขียนif-elseหรือswitch statement ให้ตรวจสอบกรณีที่พบบ่อยที่สุดก่อน ลูปไม่จำเป็นต้องมีการสั่งรหัสพิเศษใด ๆ สำหรับการคาดคะเนสาขาแบบคงที่เนื่องจากโดยปกติจะใช้เงื่อนไขของลูปตัววนซ้ำเท่านั้น


304

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

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

ลิงก์อยู่ที่นี่: http://www.geoffchappell.com/studies/windows/km/ntoskrnl/api/ex/profile/demo.htm


3
นั่นเป็นบทความที่น่าสนใจมาก (อันที่จริงฉันเพิ่งอ่านมาทั้งหมด) แต่มันจะตอบคำถามได้อย่างไร
Peter Mortensen

2
@PeterMortensen ฉันเป็นบิต flummoxed ตามคำถามของคุณ ตัวอย่างเช่นที่นี่เป็นหนึ่งบรรทัดที่เกี่ยวข้องจากชิ้นส่วนนั้น: When the input is unsorted, all the rest of the loop takes substantial time. But with sorted input, the processor is somehow able to spend not just less time in the body of the loop, meaning the buckets at offsets 0x18 and 0x1C, but vanishingly little time on the mechanism of looping. ผู้เขียนพยายามพูดคุยเกี่ยวกับการทำโปรไฟล์ในบริบทของโค้ดที่โพสต์ที่นี่และในกระบวนการพยายามอธิบายว่าทำไมกรณีที่เรียงลำดับจึงเร็วกว่ามาก
ForeverLearning

260

ในฐานะที่เป็นสิ่งที่ได้ถูกกล่าวถึงโดยคนอื่น ๆ สิ่งที่อยู่เบื้องหลังความลึกลับเป็นสาขาทำนาย

ฉันไม่ได้พยายามเพิ่มอะไร แต่อธิบายแนวคิดในอีกทางหนึ่ง มีคำแนะนำสั้น ๆ เกี่ยวกับวิกิซึ่งประกอบด้วยข้อความและแผนภาพ ฉันชอบคำอธิบายด้านล่างซึ่งใช้ไดอะแกรมเพื่ออธิบายรายละเอียดของ Branch Predictor โดยสังเขป

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

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

รูปที่ 1

จากสถานการณ์ที่อธิบายไว้ฉันได้เขียนตัวอย่างการเคลื่อนไหวเพื่อแสดงวิธีการใช้งานคำสั่งในขั้นตอนต่างๆในสถานการณ์ที่แตกต่างกัน

  1. ไม่มีตัวทำนายสาขา

ตัวประมวลผลจะต้องรอจนกว่าคำสั่งการกระโดดแบบมีเงื่อนไขผ่านขั้นตอนการดำเนินการก่อนที่คำสั่งถัดไปจะสามารถเข้าสู่ขั้นตอนการดึงข้อมูลในไปป์ไลน์

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

ไม่มีตัวทำนายสาขา

จะใช้เวลา 9 รอบนาฬิกาเพื่อให้ 3 คำแนะนำเสร็จสมบูรณ์

  1. ใช้ Branch Predictor และอย่ากระโดดตามเงื่อนไข สมมติว่าการทำนายไม่ได้เป็นการกระโดดตามเงื่อนไข

ป้อนคำอธิบายรูปภาพที่นี่

จะใช้เวลา 7 รอบนาฬิกาเพื่อให้ 3 คำแนะนำเสร็จสมบูรณ์

  1. ใช้ Branch Predictor และกระโดดตามเงื่อนไข สมมติว่าการทำนายไม่ได้เป็นการกระโดดตามเงื่อนไข

ป้อนคำอธิบายรูปภาพที่นี่

จะใช้เวลา 9 รอบนาฬิกาเพื่อให้ 3 คำแนะนำเสร็จสมบูรณ์

เวลาที่สูญเสียไปในกรณีที่การคาดคะเนความผิดพลาดของสาขาเท่ากับจำนวนของขั้นตอนต่างๆในไปป์ไลน์จากขั้นตอนการดึงข้อมูลไปยังระยะดำเนินการ ไมโครโปรเซสเซอร์ที่ทันสมัยมักจะมีท่อค่อนข้างยาวดังนั้นความล่าช้าในการคาดคะเนผิดระหว่าง 10 ถึง 20 รอบนาฬิกา เป็นผลให้การส่งไปป์ไลน์เพิ่มความต้องการตัวพยากรณ์สาขาขั้นสูงขึ้นอีกต่อไป

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

มันเป็นตัวอย่างง่ายๆที่อธิบายส่วนพื้นฐานของ Branch Predictor อย่างชัดเจน หาก GIF เหล่านั้นน่ารำคาญโปรดลบออกจากคำตอบและผู้เยี่ยมชมยังสามารถรับซอร์สโค้ดสาธิตสดจากBranchPredictorDemo


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

@mckenzm: ผู้บริหารเก็งกำไรนอกสั่งทำให้การคาดการณ์ของสาขามีค่ายิ่งกว่า เช่นเดียวกับการซ่อนฟองเรียก / ถอดรหัสการคาดการณ์ของสาขา + การเก็งกำไร exec จะลบการพึ่งพาการควบคุมจากเวลาแฝงเส้นทางที่สำคัญ โค้ดภายในหรือหลังif()บล็อกสามารถดำเนินการได้ก่อนที่จะทราบเงื่อนไขของสาขา หรือสำหรับลูปการค้นหาเช่นstrlenหรืออินเทอmemchrร์เนชันสามารถทับซ้อน หากคุณต้องรอผลการจับคู่หรือไม่ทราบก่อนที่จะเรียกใช้การทำซ้ำครั้งถัดไปใด ๆ คุณต้องมีปัญหาคอขวดในการโหลดแคช + เวลาแฝง ALU แทนปริมาณงาน
Peter Cordes

209

กำไรจากการทำนายสาขา!

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

if (expression)
{
    // Run 1
} else {
    // Run 2
}

เมื่อใดก็ตามที่มีคำสั่งif-else\ switchการแสดงออกจะต้องมีการประเมินเพื่อกำหนดบล็อกที่ควรจะดำเนินการ ในรหัสแอสเซมบลีที่สร้างโดยคอมไพเลอร์คำสั่งสาขาที่มีเงื่อนไขจะถูกแทรก

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

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

การแสดง:

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

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

 O      Route 1  /-------------------------------
/|\             /
 |  ---------##/
/ \            \
                \
        Route 2  \--------------------------------

ในขณะที่การล้างท่อส่งเป็นไปอย่างรวดเร็ว ไม่จริง มันเร็วเมื่อเทียบกับแคชที่พลาดไปจนถึง DRAM แต่ใน x86 ประสิทธิภาพสูงที่ทันสมัย ​​(เช่นตระกูล Intel Sandybridge) มีประมาณหนึ่งโหล แม้ว่าการกู้คืนที่รวดเร็วจะช่วยให้สามารถหลีกเลี่ยงการรอคำแนะนำอิสระเก่ากว่าทั้งหมดเพื่อให้ถึงวัยเกษียณก่อนที่จะเริ่มการกู้คืน จะเกิดอะไรขึ้นเมื่อซีพียู skylake ทำนายสาขาผิด . (และแต่ละรอบสามารถทำงานได้ประมาณ 4 คำแนะนำ) ไม่ดีสำหรับรหัสความเร็วสูง
Peter Cordes

153

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

ลูปด้านในสำหรับอัลกอริทึมนี้จะมีลักษณะดังนี้ในภาษาแอสเซมบลี ARM:

MOV R0, #0     // R0 = sum = 0
MOV R1, #0     // R1 = c = 0
ADR R2, data   // R2 = addr of data array (put this instruction outside outer loop)
.inner_loop    // Inner loop branch label
    LDRB R3, [R2, R1]     // R3 = data[c]
    CMP R3, #128          // compare R3 to 128
    ADDGE R0, R0, R3      // if R3 >= 128, then sum += data[c] -- no branch needed!
    ADD R1, R1, #1        // c++
    CMP R1, #arraySize    // compare c to arraySize
    BLT inner_loop        // Branch to inner_loop if c < arraySize

แต่นี่เป็นส่วนหนึ่งของภาพใหญ่:

CMPopcodes อัปเดตบิตสถานะในตัวประมวลผลสถานะการลงทะเบียน (PSR) เสมอเพราะนั่นเป็นจุดประสงค์ของพวกเขา แต่คำแนะนำอื่น ๆ ส่วนใหญ่จะไม่แตะ PSR เว้นแต่คุณจะเพิ่มSคำต่อท้ายที่ไม่จำเป็นให้กับคำสั่งระบุว่าควรปรับปรุง PSR ตาม ผลการเรียนการสอน เช่นเดียวกับคำต่อท้ายเงื่อนไข 4 บิตความสามารถในการดำเนินการคำสั่งโดยไม่ส่งผลกระทบต่อ PSR เป็นกลไกที่ช่วยลดความต้องการกิ่งไม้บน ARM และยังอำนวยความสะดวกในการส่งคำสั่งซื้อที่ระดับฮาร์ดแวร์เพราะหลังจากดำเนินการ X บิตสถานะต่อมา (หรือขนาน) คุณสามารถทำงานอื่น ๆ ที่ชัดเจนไม่ควรส่งผลกระทบต่อสถานะบิตจากนั้นคุณสามารถทดสอบสถานะของบิตสถานะที่ตั้งค่าไว้ก่อนหน้าโดย X

ฟิลด์การทดสอบเงื่อนไขและฟิลด์ "set status bit" ซึ่งเป็นทางเลือกสามารถรวมกันได้เช่น:

  • ADD R1, R2, R3ดำเนินการR1 = R2 + R3โดยไม่อัปเดตบิตสถานะใด ๆ
  • ADDGE R1, R2, R3 ทำการดำเนินการเดียวกันหากคำสั่งก่อนหน้านี้ที่มีผลต่อบิตสถานะส่งผลให้เกิดเงื่อนไขที่มากกว่าหรือเท่ากับ
  • ADDS R1, R2, R3ดำเนินการนอกจากนี้แล้วการปรับปรุงN, Z, CและVธงในสถานะประมวลผลลงทะเบียนขึ้นอยู่กับว่าผลที่ได้ลบศูนย์ดำเนินการ (สำหรับการเพิ่มไม่ได้ลงนาม) หรือล้น (สำหรับการเพิ่มลงนาม)
  • ADDSGE R1, R2, R3ทำการเพิ่มต่อเมื่อการGEทดสอบเป็นจริงจากนั้นอัปเดตบิตสถานะตามผลลัพธ์ของการเพิ่ม

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

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


1
นวัตกรรมอื่น ๆ ใน ARM คือการเพิ่มคำต่อท้ายคำสั่ง S ซึ่งเป็นทางเลือกใน (เกือบ) คำสั่งทั้งหมดซึ่งหากไม่มีให้ป้องกันคำแนะนำจากการเปลี่ยนสถานะบิต (ยกเว้นคำสั่ง CMP ซึ่งมีหน้าที่ตั้งบิตสถานะ ดังนั้นจึงไม่ต้องการคำต่อท้าย S) สิ่งนี้ช่วยให้คุณหลีกเลี่ยงคำสั่ง CMP ในหลาย ๆ กรณีตราบใดที่การเปรียบเทียบมีค่าเป็นศูนย์หรือคล้ายกัน (เช่น SUBS R0, R0, # 1 จะตั้งค่าบิต Z (ศูนย์) เมื่อ R0 ถึงศูนย์) เงื่อนไขและส่วนต่อท้าย S มีค่าใช้จ่ายเป็นศูนย์ มันค่อนข้าง ISA ที่สวยงาม
ลุคฮัทชิสัน

2
การไม่เพิ่มคำต่อท้าย S อนุญาตให้คุณมีคำแนะนำแบบมีเงื่อนไขหลายข้อในแถวโดยไม่ต้องกังวลว่าหนึ่งในนั้นอาจเปลี่ยนบิตสถานะซึ่งอาจมีผลข้างเคียงของการข้ามคำแนะนำแบบมีเงื่อนไขที่เหลือ
ลุคฮัทชิสัน

โปรดทราบว่า OP ไม่รวมเวลาในการจัดเรียงการวัด อาจเป็นความสูญเสียโดยรวมในการเรียงลำดับก่อนที่จะเรียกใช้ x86 ลูปแบรนช์ด้วยเช่นกันแม้ว่าเคสที่ไม่เรียงลำดับจะทำให้ลูปรันช้าลงมาก แต่การเรียงลำดับอาร์เรย์ขนาดใหญ่ต้องใช้จำนวนมากของการทำงาน
Peter Cordes

BTW คุณสามารถบันทึกคำสั่งในลูปโดยการจัดทำดัชนีสัมพันธ์กับจุดสิ้นสุดของอาร์เรย์ ก่อนที่วงที่ตั้งขึ้นมาแล้วเริ่มต้นด้วยR2 = data + arraySize R1 = -arraySizeด้านล่างของวงกลายเป็น/adds r1, r1, #1 bnz inner_loopคอมไพเลอร์ไม่ได้ใช้การเพิ่มประสิทธิภาพนี้ด้วยเหตุผลบางอย่าง: / แต่อย่างไรก็ตามการดำเนินการบอกกล่าวของ add ไม่ได้เป็นพื้นฐานที่แตกต่างในกรณีนี้จากสิ่งที่คุณสามารถทำได้ด้วยรหัสสาขาในอกหักอื่น ๆ เช่น cmovx86 แม้ว่าจะไม่ดีเท่าไร: การเพิ่มประสิทธิภาพ gcc -O3 ทำให้โค้ดช้ากว่า -O2
Peter Cordes

1
(ARM สั่งการประมวลผลล่วงหน้าโดยเฉพาะอย่างยิ่ง NOP ดังนั้นคุณสามารถใช้กับโหลดหรือร้านค้าที่จะเกิดข้อผิดพลาดซึ่งแตกต่างจาก x86 cmovกับตัวถูกดำเนินการแหล่งหน่วยความจำ ISAs ส่วนใหญ่รวมถึง AArch64 มีการเลือก ALU เท่านั้น และใช้งานได้อย่างมีประสิทธิภาพมากกว่ารหัสไร้สาขาใน ISAs ส่วนใหญ่)
Peter Cordes

146

มันเกี่ยวกับการทำนายสาขา มันคืออะไร?

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

  • ในทางกลับกันการคาดการณ์ของสาขาที่ซับซ้อน - ทั้งจากระบบประสาทหรือตัวแปรของการทำนายสาขาสองระดับ - ให้ความแม่นยำในการทำนายที่ดีกว่า แต่พวกมันใช้พลังงานและความซับซ้อนมากขึ้นเรื่อย ๆ

  • นอกจากนี้ในเทคนิคการคาดการณ์ที่ซับซ้อนเวลาที่ใช้ในการทำนายกิ่งไม้นั้นสูงมากตั้งแต่ 2 ถึง 5 รอบ - ซึ่งเทียบเท่ากับเวลาดำเนินการของสาขาจริง

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

จริงๆแล้วมีสามสาขาที่แตกต่างกัน:

ส่งต่อสาขาตามเงื่อนไข - ตามเงื่อนไขรันไทม์ PC (โปรแกรมตัวนับ) จะเปลี่ยนเป็นชี้ไปยังที่อยู่ข้างหน้าในสตรีมคำแนะนำ

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

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

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

อ้างอิง:


145

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

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

 // sort backwards (higher values first), may be in some other part of the code
 std::sort(data, data + arraySize, std::greater<int>());

 for (unsigned c = 0; c < arraySize; ++c) {
       if (data[c] < 128) {
              break;
       }
       sum += data[c];               
 }

1
ถูกต้อง แต่ค่าใช้จ่ายในการตั้งค่าการเรียงลำดับอาเรย์คือ O (N log N) ดังนั้นการแตกเร็วไม่ได้ช่วยคุณได้ถ้าเหตุผลเดียวที่คุณเรียงลำดับอาเรย์คือการแตกเร็ว อย่างไรก็ตามหากคุณมีเหตุผลอื่นในการจัดเรียงอาร์เรย์ล่วงหน้าใช่แล้วนี่เป็นสิ่งที่มีค่า
ลุคฮัทชิสัน

ขึ้นอยู่กับจำนวนครั้งที่คุณเรียงลำดับข้อมูลเปรียบเทียบกับจำนวนครั้งที่คุณวนรอบข้อมูล การเรียงลำดับในตัวอย่างนี้เป็นเพียงตัวอย่างเท่านั้นไม่จำเป็นต้องอยู่ตรงหน้าลูป
Yochai Timmer

2
ใช่นั่นคือจุดที่ฉันทำในความคิดเห็นแรกของฉัน :-) คุณพูดว่า "การคาดคะเนสาขาจะพลาดเพียงครั้งเดียวเท่านั้น" แต่คุณจะไม่นับการคาดคะเนสาขา O (N log N) ที่หายไปภายในอัลกอริทึมการเรียงซึ่งจริงๆแล้วสูงกว่าการคาดคะเนสาขา O (N) ที่หายไปในกรณีที่ไม่ได้เรียงลำดับ ดังนั้นคุณจะต้องใช้ข้อมูลทั้งหมดที่เรียงลำดับ O (log N) ครั้งเพื่อแยกให้ได้ (อาจใกล้เคียงกับ O (10 log N) ขึ้นอยู่กับอัลกอริทึมการเรียงลำดับเช่นสำหรับ quicksort เนื่องจากการคิดถึงแคช - การรวมกัน แคชที่สอดคล้องกันมากขึ้นดังนั้นคุณจะต้องใกล้ชิดกับ O (2 บันทึก N) ประเพณีที่จะทำลายแม้กระทั่ง.)
Luke Hutchison

การปรับให้เหมาะสมที่สำคัญอย่างหนึ่งแม้ว่าจะทำเพียง "ครึ่งทางลัด" โดยเรียงลำดับเฉพาะรายการที่น้อยกว่าค่า pivot เป้าหมายที่ 127 (สมมติว่าทุกอย่างน้อยกว่าหรือเท่ากับ pivot จะถูกจัดเรียงหลัง pivot) เมื่อคุณไปถึงเดือยให้รวมองค์ประกอบก่อนหน้าเดือย สิ่งนี้จะทำงานในเวลาเริ่มต้นของ O (N) มากกว่า O (N log N) ถึงแม้ว่าจะยังมีการคาดคะเนสาขาจำนวนมากที่ขาดหายไปอาจเป็นไปได้ว่าคำสั่งของ O (5 N) ขึ้นอยู่กับตัวเลขที่ฉันให้ไว้ก่อนหน้า มันเป็นครึ่งทางสั้น
ลุคฮัทชิสัน

132

อาร์เรย์ที่เรียงลำดับจะถูกประมวลผลเร็วกว่าอาเรย์ที่ไม่ได้เรียงเนื่องจากปรากฏการณ์ที่เรียกว่าการทำนายสาขา

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

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

คำตอบสำหรับคำถามของคุณนั้นง่ายมาก

ในอาร์เรย์ที่ไม่เรียงลำดับคอมพิวเตอร์จะทำการคาดการณ์หลายครั้งทำให้มีโอกาสเกิดข้อผิดพลาดเพิ่มขึ้น ในขณะที่เรียงลำดับคอมพิวเตอร์ทำให้การคาดการณ์น้อยลงลดโอกาสของข้อผิดพลาด การคาดการณ์เพิ่มเติมต้องใช้เวลามากขึ้น

เรียงลำดับ: ถนนตรง ________________________________________________________________________________TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

Array ที่ไม่เรียงกัน: ถนนโค้ง

______   ________
|     |__|

การคาดคะเนสาขา: การคาดเดา / คาดการณ์ว่าถนนเส้นใดตรงและตามโดยไม่ตรวจสอบ

___________________________________________ Straight road
 |_________________________________________|Longer road

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


นอกจากนี้ฉันต้องการอ้างถึง@Simon_Weaverจากความคิดเห็น:

มันไม่ได้ทำให้การคาดการณ์น้อยลง - มันทำให้การคาดการณ์ที่ไม่ถูกต้องน้อยลง มันยังคงมีการทำนายแต่ละครั้งผ่านการวนซ้ำ ...


123

ฉันลองรหัสเดียวกันกับ MATLAB 2011b กับ MacBook Pro ของฉัน (Intel i7, 64 บิต, 2.4 GHz) สำหรับรหัส MATLAB ต่อไปนี้:

% Processing time with Sorted data vs unsorted data
%==========================================================================
% Generate data
arraySize = 32768
sum = 0;
% Generate random integer data from range 0 to 255
data = randi(256, arraySize, 1);


%Sort the data
data1= sort(data); % data1= data  when no sorting done


%Start a stopwatch timer to measure the execution time
tic;

for i=1:100000

    for j=1:arraySize

        if data1(j)>=128
            sum=sum + data1(j);
        end
    end
end

toc;

ExeTimeWithSorting = toc - tic;

ผลลัพธ์สำหรับรหัส MATLAB ด้านบนมีดังนี้:

  a: Elapsed time (without sorting) = 3479.880861 seconds.
  b: Elapsed time (with sorting ) = 2377.873098 seconds.

ผลลัพธ์ของรหัส C ใน @GManNickG ฉันได้รับ:

  a: Elapsed time (without sorting) = 19.8761 sec.
  b: Elapsed time (with sorting ) = 7.37778 sec.

จากนี้ดูเหมือนว่า MATLAB จะช้ากว่าการใช้ C เกือบ175 เท่าโดยไม่ต้องเรียงลำดับและช้ากว่า350 เท่าเมื่อทำการเรียงลำดับ กล่าวอีกนัยหนึ่งผลกระทบ (จากการคาดคะเนสาขา) คือ1.46xสำหรับการใช้งาน MATLAB และ2.7xสำหรับการใช้งาน C


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

1
Matlab ทำการปรับเทียบ / เวกเตอร์อัตโนมัติในหลาย ๆ สถานการณ์ แต่ปัญหาที่นี่คือการตรวจสอบผลของการทำนายสาขา Matlab ไม่ได้รับการยกเว้น
Shan

1
matlab ใช้ตัวเลขดั้งเดิมหรือการใช้งานเฉพาะของแล็บ mat (จำนวนไม่ จำกัด จำนวนหรือดังนั้น?)
Thorbjørn Ravn Andersen

54

ข้อสันนิษฐานโดยคำตอบอื่น ๆ ที่ต้องการเรียงลำดับข้อมูลไม่ถูกต้อง

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

การเรียงลำดับเฉพาะส่วนองค์ประกอบ k เท่านั้นที่ทำให้การประมวลผลล่วงหน้าเป็นแบบเส้นตรงO(n)แทนที่จะใช้O(n.log(n))เวลาในการเรียงลำดับอาร์เรย์ทั้งหมด

#include <algorithm>
#include <ctime>
#include <iostream>

int main() {
    int data[32768]; const int l = sizeof data / sizeof data[0];

    for (unsigned c = 0; c < l; ++c)
        data[c] = std::rand() % 256;

    // sort 200-element segments, not the whole array
    for (unsigned c = 0; c + 200 <= l; c += 200)
        std::sort(&data[c], &data[c + 200]);

    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i) {
        for (unsigned c = 0; c < sizeof data / sizeof(int); ++c) {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    std::cout << static_cast<double>(clock() - start) / CLOCKS_PER_SEC << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

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


4
ฉันไม่เห็นจริง ๆ ว่าสิ่งนี้พิสูจน์ได้อย่างไร สิ่งเดียวที่คุณแสดงให้เห็นคือ "การไม่เรียงลำดับอาร์เรย์ทั้งหมดใช้เวลาน้อยกว่าการเรียงลำดับอาร์เรย์ทั้งหมด" การอ้างสิทธิ์ของคุณว่า "การทำงานเร็วที่สุด" นั้นขึ้นอยู่กับสถาปัตยกรรมเป็นอย่างมาก ดูคำตอบของฉันเกี่ยวกับการทำงานของ ARM ป.ล. คุณสามารถทำให้โค้ดของคุณเร็วขึ้นในสถาปัตยกรรมที่ไม่ใช่แขนโดยใส่ผลรวมเข้าไปในลูปบล็อก 200 องค์ประกอบเรียงลำดับย้อนกลับแล้วใช้คำแนะนำของ Yochai Timmer ในการแตกหักเมื่อคุณได้รับค่านอกช่วง ด้วยวิธีนี้การสรุปบล็อก 200 องค์ประกอบแต่ละรายการสามารถยกเลิกได้ในช่วงต้น
ลุคฮัทชิสัน

หากคุณต้องการใช้อัลกอริทึมอย่างมีประสิทธิภาพเหนือข้อมูลที่ไม่ได้เรียงลำดับคุณจะทำการดำเนินการโดยไม่มีสาขา (และด้วย SIMD เช่นกับ x86 pcmpgtbเพื่อค้นหาองค์ประกอบที่มีการตั้งค่าบิตสูงแล้วและเป็นศูนย์ขนาดเล็ก) การใช้เวลาในการเรียงลำดับชิ้นงานจริง ๆ จะช้าลง เวอร์ชันไร้สาขาจะมีประสิทธิภาพที่ไม่ขึ้นกับข้อมูลและพิสูจน์ว่าค่าใช้จ่ายนั้นมาจากการคาดคะเนความผิดพลาดของสาขา หรือเพียงใช้เคาน์เตอร์วัดประสิทธิภาพเพื่อสังเกตว่าโดยตรงเช่น Skylake int_misc.clear_resteer_cyclesหรือint_misc.recovery_cyclesนับรอบหน้าว่างที่ไม่ได้ใช้งานจากการ
Peter Cordes

ความคิดเห็นทั้งสองข้างต้นดูเหมือนว่าจะไม่สนใจปัญหาอัลกอริทึมทั่วไปและความซับซ้อนเพื่อสนับสนุนฮาร์ดแวร์พิเศษพร้อมคำแนะนำเครื่องพิเศษ ฉันพบว่าคนแรกโดยเฉพาะอย่างยิ่งเล็ก ๆ น้อย ๆ ในการที่มันกำจัดความเข้าใจทั่วไปที่สำคัญในคำตอบนี้ในความโปรดปรานของคำแนะนำเครื่องพิเศษ
2297550

36

คำตอบของ Bjarne Stroustrupสำหรับคำถามนี้:

ฟังดูเหมือนคำถามสัมภาษณ์ จริงป้ะ? คุณจะรู้ได้อย่างไร เป็นความคิดที่ดีที่จะตอบคำถามเกี่ยวกับประสิทธิภาพโดยไม่ทำการวัดบางครั้งดังนั้นจึงเป็นสิ่งสำคัญที่จะต้องรู้วิธีการวัด

ดังนั้นฉันจึงลองด้วยเวกเตอร์จำนวนเต็มหนึ่งล้านและได้รับ:

Already sorted    32995 milliseconds
Shuffled          125944 milliseconds

Already sorted    18610 milliseconds
Shuffled          133304 milliseconds

Already sorted    17942 milliseconds
Shuffled          107858 milliseconds

ฉันวิ่งไปสองสามครั้งเพื่อให้แน่ใจ ใช่ปรากฏการณ์เป็นของจริง รหัสสำคัญของฉันคือ:

void run(vector<int>& v, const string& label)
{
    auto t0 = system_clock::now();
    sort(v.begin(), v.end());
    auto t1 = system_clock::now();
    cout << label 
         << duration_cast<microseconds>(t1  t0).count() 
         << " milliseconds\n";
}

void tst()
{
    vector<int> v(1'000'000);
    iota(v.begin(), v.end(), 0);
    run(v, "already sorted ");
    std::shuffle(v.begin(), v.end(), std::mt19937{ std::random_device{}() });
    run(v, "shuffled    ");
}

อย่างน้อยปรากฏการณ์นี้เป็นจริงด้วยคอมไพเลอร์ไลบรารีมาตรฐานและการตั้งค่าเครื่องมือเพิ่มประสิทธิภาพ การใช้งานที่แตกต่างกันและสามารถให้คำตอบที่แตกต่างกัน ในความเป็นจริงมีคนทำการศึกษาอย่างเป็นระบบมากกว่า (การค้นหาเว็บอย่างรวดเร็วจะพบมัน) และการใช้งานส่วนใหญ่แสดงให้เห็นถึงผลกระทบนั้น

เหตุผลหนึ่งคือการทำนายสาขา: การดำเนินการหลักในอัลกอริทึมการเรียงลำดับนั้น“if(v[i] < pivot]) …”หรือเทียบเท่า สำหรับการเรียงลำดับที่การทดสอบจะเป็นจริงเสมอในขณะที่สำหรับการสุ่มลำดับสาขาที่เลือกแตกต่างกันไปแบบสุ่ม

อีกเหตุผลหนึ่งคือเมื่อเวกเตอร์ถูกจัดเรียงแล้วเราไม่จำเป็นต้องย้ายองค์ประกอบไปยังตำแหน่งที่ถูกต้อง ผลของรายละเอียดเล็กน้อยเหล่านี้คือปัจจัยห้าหรือหกที่เราเห็น

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

ถ้าคุณต้องการเขียนโค้ดที่มีประสิทธิภาพคุณจำเป็นต้องรู้สถาปัตยกรรมของเครื่องสักหน่อย


28

คำถามนี้ถูกฝังในโมเดลการทำนายสาขาบนซีพียู ฉันขอแนะนำให้อ่านกระดาษนี้:

การเพิ่มอัตราการเรียกคำสั่งผ่านการคาดการณ์หลายสาขาและที่อยู่แคชสาขา

เมื่อคุณจัดเรียงองค์ประกอบแล้ว IR จะไม่สามารถดึงข้อมูลคำสั่ง CPU ทั้งหมดได้อีกครั้งและอีกครั้งซึ่งจะดึงข้อมูลจากแคช


คำแนะนำยังคงร้อนอยู่ในแคชคำสั่ง L1 ของ CPU โดยไม่คำนึงถึงความผิดพลาด ปัญหากำลังดึงข้อมูลเหล่านั้นไปยังไปป์ไลน์ตามลำดับที่ถูกต้องก่อนที่คำสั่งก่อนหน้านี้ในทันทีจะถอดรหัสและดำเนินการเสร็จสิ้น
Peter Cordes

15

วิธีหนึ่งในการหลีกเลี่ยงข้อผิดพลาดในการทำนายสาขาคือการสร้างตารางการค้นหาและจัดทำดัชนีโดยใช้ข้อมูล Stefan de Bruijn พูดถึงเรื่องนี้ในคำตอบของเขา

แต่ในกรณีนี้เรารู้ว่าค่าอยู่ในช่วง [0, 255] และเราสนใจเฉพาะค่า> = 128 นั่นหมายความว่าเราสามารถแยกบิตเดียวที่จะบอกเราว่าเราต้องการค่าหรือไม่: โดยการเลื่อน ข้อมูลทางขวา 7 บิตเราเหลือ 0 บิตหรือ 1 บิตและเราต้องการเพิ่มค่าเมื่อเรามี 1 บิตเท่านั้น เรียกบิตนี้ว่า "บิตการตัดสินใจ"

ด้วยการใช้ค่า 0/1 ของบิตการตัดสินใจเป็นดัชนีลงในอาร์เรย์เราสามารถสร้างโค้ดที่จะเร็วพอ ๆ กันไม่ว่าจะเรียงลำดับข้อมูลหรือไม่เรียงลำดับ รหัสของเราจะเพิ่มมูลค่าเสมอ แต่เมื่อบิตการตัดสินใจเป็น 0 เราจะเพิ่มมูลค่าที่อื่นที่เราไม่สนใจ นี่คือรหัส:

// ทดสอบ

clock_t start = clock();
long long a[] = {0, 0};
long long sum;

for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        int j = (data[c] >> 7);
        a[j] += data[c];
    }
}

double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;
sum = a[1];

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

แต่ในการทดสอบของฉันตารางการค้นหาที่ชัดเจนนั้นเร็วกว่านี้เล็กน้อยอาจเป็นเพราะการจัดทำดัชนีในตารางการค้นหานั้นเร็วกว่าการเลื่อนบิตเล็กน้อย สิ่งนี้แสดงให้เห็นว่าโค้ดของฉันตั้งค่าและใช้ตารางการค้นหาอย่างไร (เรียกว่า lut สำหรับ "LookUp Table" ในโค้ด) อย่างไม่คาดคิด นี่คือรหัส C ++:

// ประกาศและกรอกข้อมูลลงในตารางการค้นหา

int lut[256];
for (unsigned c = 0; c < 256; ++c)
    lut[c] = (c >= 128) ? c : 0;

// Use the lookup table after it is built
for (unsigned i = 0; i < 100000; ++i)
{
    // Primary loop
    for (unsigned c = 0; c < arraySize; ++c)
    {
        sum += lut[data[c]];
    }
}

ในกรณีนี้ตารางการค้นหามีขนาดเพียง 256 ไบต์ดังนั้นมันจึงเหมาะกับแคชและทุกอย่างรวดเร็ว เทคนิคนี้ใช้ไม่ได้ผลถ้าข้อมูลมีค่า 24 บิตและเราต้องการเพียงครึ่งเดียว ... ตารางการค้นหาจะใหญ่เกินไปที่จะใช้งานได้จริง ในอีกทางหนึ่งเราสามารถรวมสองเทคนิคที่แสดงด้านบน: แรกเลื่อนบิตไปแล้วทำดัชนีตารางการค้นหา สำหรับค่า 24 บิตที่เราต้องการเพียงค่าครึ่งบนเราสามารถเปลี่ยนข้อมูลได้ 12 บิตและเหลือค่า 12 บิตสำหรับดัชนีตาราง ดัชนีตารางแบบ 12 บิตหมายถึงตารางค่า 4096 ซึ่งอาจเป็นประโยชน์

เทคนิคการจัดทำดัชนีในอาร์เรย์แทนที่จะใช้คำสั่ง if สามารถใช้สำหรับการตัดสินใจว่าจะใช้ตัวชี้ใด ฉันเห็นห้องสมุดที่ใช้ต้นไม้ไบนารีและแทนที่จะมีพอยน์เตอร์ชื่อสองชื่อ (pLeft และ pRight หรืออะไรก็ตาม) มีพอยน์เตอร์ที่มีความยาว 2 แถวและใช้เทคนิค "บิตการตัดสินใจ" เพื่อตัดสินใจว่าจะเลือกอันไหน ตัวอย่างเช่นแทนที่จะเป็น:

if (x < node->value)
    node = node->pLeft;
else
    node = node->pRight;
this library would do something like:

i = (x < node->value);
node = node->link[i];

มันเป็นทางออกที่ดีบางทีมันจะทำงาน


คอมไพเลอร์ / ฮาร์ดแวร์ C ++ ใดที่คุณทดสอบด้วยและด้วยตัวเลือกคอมไพเลอร์ตัวใด ฉันแปลกใจที่เวอร์ชันดั้งเดิมไม่ได้ปรับเวกเตอร์อัตโนมัติเป็นรหัส SIMD ที่ไม่มีสาขาที่ดี คุณเปิดใช้งานการเพิ่มประสิทธิภาพเต็มรูปแบบหรือไม่
Peter Cordes

ตารางการค้นหารายการ 4096 ฟังดูบ้า ถ้าคุณเปลี่ยนออกใด ๆบิตคุณจะต้องไม่สามารถเพียงใช้ผล LUT ถ้าคุณต้องการที่จะเพิ่มจำนวนเดิม เสียงเหล่านี้ดูเหมือนกลอุบายโง่ ๆ ในการหลีกเลี่ยงคอมไพเลอร์ของคุณไม่ใช่เรื่องง่ายโดยใช้เทคนิคไร้สาขา ตรงไปตรงมามากขึ้นจะเป็นmask = tmp < 128 : 0 : -1UL;/total += tmp & mask;
Peter Cordes
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.