ทำไมสวิตช์ถึงเร็วกว่าถ้า


116

หนังสือ Java จำนวนมากอธิบายswitchคำสั่งว่าเร็วกว่าif elseคำสั่ง แต่ผมไม่ได้หาได้ทุกเหตุผลที่สวิทช์จะเร็วกว่าถ้า

ตัวอย่าง

ฉันมีสถานการณ์ที่ต้องเลือกหนึ่งรายการจากสองรายการ ฉันสามารถใช้ได้ทั้ง

switch (item) {
    case BREAD:
        //eat Bread
        break;
    default:
        //leave the restaurant
}

หรือ

if (item == BREAD) {
    //eat Bread
} else {
    //leave the restaurant
}

การพิจารณารายการและ BREAD เป็นค่า int คงที่

ในตัวอย่างข้างต้นซึ่งดำเนินการได้เร็วกว่าและทำไม?


บางทีนี่อาจเป็นคำตอบสำหรับ java: stackoverflow.com/questions/767821/…
Tobias

19
โดยทั่วไปจากWikipedia : หากช่วงของค่าอินพุตระบุได้ว่า 'เล็ก' และมีช่องว่างเพียงเล็กน้อยคอมไพเลอร์บางตัวที่รวมตัวเพิ่มประสิทธิภาพอาจใช้คำสั่งสวิตช์เป็นตารางสาขาหรืออาร์เรย์ของตัวชี้ฟังก์ชันที่จัดทำดัชนีแทน a ชุดคำแนะนำตามเงื่อนไขที่มีความยาว สิ่งนี้ช่วยให้คำสั่งสวิตช์สามารถกำหนดสาขาที่จะดำเนินการได้ทันทีโดยไม่ต้องผ่านรายการการเปรียบเทียบ
Felix Kling

คำตอบยอดนิยมสำหรับคำถามนี้อธิบายได้ดี นี้บทความอธิบายทุกอย่างสวยดีเกินไป
bezmax

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

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

คำตอบ:


110

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

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


6
ไม่แปลซ้ำเป็น "ตรวจสอบกระโดด" หรือไม่
fivetwentysix

17
@fivetwentysix: ไม่พูดถึงเรื่องนี้สำหรับข้อมูล: artima.com/underthehood/flowP.html อ้างจากบทความ: เมื่อ JVM พบคำสั่ง tablewitch ก็สามารถตรวจสอบได้ว่าคีย์อยู่ในช่วงที่กำหนดโดยต่ำและสูงหรือไม่ หากไม่เป็นเช่นนั้นจะใช้การชดเชยสาขาเริ่มต้น ถ้าเป็นเช่นนั้นก็จะลบค่าต่ำออกจากคีย์เพื่อให้ได้ค่าชดเชยในรายการออฟเซ็ตสาขา ในลักษณะนี้สามารถกำหนดออฟเซ็ตสาขาที่เหมาะสมได้โดยไม่ต้องตรวจสอบค่าแต่ละกรณี
bezmax

1
(i) a switchไม่สามารถแปลเป็นtableswitchคำสั่ง bytecode ได้ - อาจกลายเป็นlookupswitchคำสั่งที่ทำหน้าที่คล้ายกับ if / else (ii) แม้แต่tableswitchคำสั่ง bytecode ก็อาจรวบรวมเป็นชุดของ if / else โดย JIT ขึ้นอยู่กับปัจจัยต่างๆ เช่นจำนวนcases
assylias


34

switchคำสั่งไม่เสมอเร็วกว่าifคำสั่ง จะปรับขนาดได้ดีกว่ารายการif-elseคำสั่งแบบยาวเนื่องจากswitchสามารถทำการค้นหาตามค่าทั้งหมดได้ อย่างไรก็ตามสำหรับเงื่อนไขสั้น ๆ มันจะไม่เร็วกว่านี้และอาจช้าลง


5
กรุณา จำกัด "ยาว" มากกว่า 5? มากกว่า 10? หรือมากกว่าเช่น 20 - 30?
vanderwyst

11
ฉันสงสัยว่ามันขึ้นอยู่กับ สำหรับฉันคำแนะนำ 3 ข้อขึ้นไปswitchจะชัดเจนกว่าถ้าไม่เร็วกว่านี้
Peter Lawrey

ภายใต้เงื่อนไขใดที่อาจทำให้ช้าลง?
Eric

1
@Eric จะช้ากว่าสำหรับค่าจำนวนเล็กน้อย esp String หรือ int ซึ่งเบาบาง
Peter Lawrey

8

JVM ปัจจุบันมีรหัสไบต์สวิทช์สองชนิด: LookupSwitch และ TableSwitch

แต่ละกรณีในคำสั่งสวิตช์จะมีค่าออฟเซ็ตจำนวนเต็มหากค่าชดเชยเหล่านี้ต่อเนื่องกัน (หรือส่วนใหญ่ติดกันโดยไม่มีช่องว่างขนาดใหญ่) (กรณี 0: กรณีที่ 1: กรณีที่ 2 เป็นต้น) จะใช้ TableSwitch

หากออฟเซ็ตถูกกระจายออกโดยมีช่องว่างขนาดใหญ่ (กรณี 0: กรณี 400: กรณี 93748: ฯลฯ ) ระบบจะใช้ LookupSwitch

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

สวิตช์การค้นหาใช้การค้นหาแบบไบนารีเพื่อค้นหาสาขารหัสที่ถูกต้อง สิ่งนี้ทำงานในเวลา O (log n) ซึ่งก็ยังดี แต่ยังไม่ดีที่สุด

สำหรับข้อมูลเพิ่มเติมโปรดดูที่นี่: ความแตกต่างระหว่าง LookupSwitch ของ JVM และ TableSwitch?

เท่าที่วิธีใดเร็วที่สุดให้ใช้แนวทางนี้: หากคุณมี 3 กรณีขึ้นไปที่มีค่าติดต่อกันหรือเกือบติดต่อกันให้ใช้สวิตช์เสมอ

หากคุณมี 2 กรณีให้ใช้คำสั่ง if

สำหรับสถานการณ์อื่น ๆ สวิทช์มักจะเร็วกว่า แต่ไม่รับประกันเนื่องจากการค้นหาไบนารีใน LookupSwitch อาจประสบกับสถานการณ์ที่ไม่ดี

นอกจากนี้โปรดทราบว่า JVM จะเรียกใช้การเพิ่มประสิทธิภาพ JIT ในคำสั่ง if ที่จะพยายามวางสาขาที่ร้อนแรงที่สุดเป็นอันดับแรกในโค้ด สิ่งนี้เรียกว่า "การทำนายสาขา" สำหรับข้อมูลเพิ่มเติมโปรดดูที่นี่: https://dzone.com/articles/branch-prediction-in-java

ประสบการณ์ของคุณอาจแตกต่างกันไป ฉันไม่รู้ว่า JVM ไม่ได้เรียกใช้การเพิ่มประสิทธิภาพที่คล้ายกันใน LookupSwitch แต่ฉันได้เรียนรู้ที่จะไว้วางใจการปรับแต่ง JIT และอย่าพยายามชิงไหวชิงพริบกับคอมไพเลอร์


1
ตั้งแต่โพสต์สิ่งนี้ฉันสังเกตได้ว่า "การสลับนิพจน์" และ "การจับคู่รูปแบบ" กำลังจะมาถึง Java ซึ่งอาจเป็นไปได้ว่า Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 ยังไม่มีอะไรเป็นรูปธรรม แต่ดูเหมือนว่าสิ่งเหล่านี้จะทำให้switchคุณลักษณะภาษามีประสิทธิภาพยิ่งขึ้น ตัวอย่างเช่นการจับคู่รูปแบบจะช่วยให้การinstanceofค้นหาราบรื่นและมีประสิทธิภาพมากขึ้น อย่างไรก็ตามฉันคิดว่ามันปลอดภัยที่จะสมมติว่าสำหรับสถานการณ์สวิตช์พื้นฐาน / หากกฎที่ฉันกล่าวถึงจะยังคงมีผลบังคับใช้
HesNotTheStig

1

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

เพื่อให้ได้ผลลัพธ์ด้านล่างความต้องการของคุณที่เป็นนามธรรม .. ฉันจะไม่อธิบายวิธีการทำงานหวังว่าคุณจะมีความเข้าใจในเรื่องนั้น

ฉันอัปเดตสิ่งนี้เพื่อตั้งค่าขนาดแพ็คเก็ตเป็น 255 หากคุณต้องการมากกว่านั้นคุณจะต้องทำการตรวจสอบขอบเขต (id <0) || (id> length)

Packets[] packets = new Packets[255];

static {
     packets[0] = new Login(6);
     packets[2] = new Logout(8);
     packets[4] = new GetMessage(1);
     packets[8] = new AddFriend(0);
     packets[11] = new JoinGroupChat(7); // etc... not going to finish.
}

public void handlePacket(IncomingData data)
{
    int id = data.readByte() & 0xFF; //Secure value to 0-255.

    if (packet[id] == null)
        return; //Leave if packet is unhandled.

    packets[id].execute(data);
}

แก้ไขเนื่องจากฉันใช้ Jump Table ใน C ++ เป็นจำนวนมากตอนนี้ฉันจะแสดงตัวอย่างของตารางกระโดดของตัวชี้ฟังก์ชัน นี่เป็นตัวอย่างทั่วไป แต่ฉันรันแล้วและทำงานได้อย่างถูกต้อง โปรดทราบว่าคุณต้องตั้งค่าตัวชี้เป็น NULL C ++ จะไม่ทำสิ่งนี้โดยอัตโนมัติเหมือนใน Java

#include <iostream>

struct Packet
{
    void(*execute)() = NULL;
};

Packet incoming_packet[255];
uint8_t test_value = 0;

void A() 
{ 
    std::cout << "I'm the 1st test.\n";
}

void B() 
{ 
    std::cout << "I'm the 2nd test.\n";
}

void Empty() 
{ 

}

void Update()
{
    if (incoming_packet[test_value].execute == NULL)
        return;

    incoming_packet[test_value].execute();
}

void InitializePackets()
{
    incoming_packet[0].execute = A;
    incoming_packet[2].execute = B;
    incoming_packet[6].execute = A;
    incoming_packet[9].execute = Empty;
}

int main()
{
    InitializePackets();

    for (int i = 0; i < 512; ++i)
    {
        Update();
        ++test_value;
    }
    system("pause");
    return 0;
}

อีกประเด็นหนึ่งที่ฉันอยากจะพูดถึงคือ Divide and Conquer ที่มีชื่อเสียง ดังนั้นไอเดียอาร์เรย์ 255 รายการข้างต้นของฉันอาจลดลงเหลือไม่เกิน 8 ถ้างบเป็นสถานการณ์กรณีที่เลวร้ายที่สุด

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

If (Value >= 128)
{
   if (Value >= 192)
   {
        if (Value >= 224)
        {
             if (Value >= 240)
             {
                  if (Value >= 248)
                  {
                      if (Value >= 252)
                      {
                          if (Value >= 254)
                          {
                              if (value == 255)
                              {

                              } else {

                              }
                          }
                      }
                  }
             }      
        }
   }
}

2
ทำไมต้องเป็นสองทิศทาง? เนื่องจาก ID ต้องถูก จำกัด อยู่แล้วทำไมไม่ จำกัด ขอบเขต - ตรวจสอบ ID ขาเข้าเป็น0 <= id < packets.lengthและตรวจสอบให้แน่ใจpackets[id]!=nullแล้วทำpackets[id].execute(data)อย่างไร
Lawrence Dol

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

0

ที่ระดับ bytecode ตัวแปร subject จะถูกโหลดเพียงครั้งเดียวในรีจิสเตอร์โปรเซสเซอร์จากแอดเดรสหน่วยความจำในไฟล์. class ที่มีโครงสร้างที่โหลดโดยรันไทม์และนี่อยู่ในคำสั่ง switch ในขณะที่ใน if-statement คำสั่ง jvm ที่แตกต่างกันนั้นถูกสร้างขึ้นโดย DE ที่คอมไพล์โค้ดของคุณและสิ่งนี้ต้องการให้แต่ละตัวแปรถูกโหลดเพื่อลงทะเบียนแม้ว่าจะใช้ตัวแปรเดียวกันกับในคำสั่ง if ถัดไป หากคุณรู้จักการเข้ารหัสในภาษาแอสเซมบลีสิ่งนี้จะเป็นเรื่องธรรมดา แม้ว่า coxes ที่คอมไพล์ java จะไม่ใช่ bytecode หรือรหัสเครื่องโดยตรง แต่แนวคิดเงื่อนไขในที่นี้ก็ยังคงสอดคล้องกัน ฉันพยายามหลีกเลี่ยงความละเอียดลึกซึ้งในการอธิบาย ฉันหวังว่าฉันจะทำให้แนวคิดนี้ชัดเจนและไม่ชัดเจน ขอบคุณ.

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