Tại sao chuyển đổi nhanh hơn nếu


116

Rất nhiều sách Java mô tả switchcâu lệnh này nhanh hơn if elsecâu lệnh. Nhưng tôi không tìm thấy bất cứ nơi nào tại sao chuyển đổi nhanh hơn nếu .

Thí dụ

Tôi có một tình huống tôi phải chọn bất kỳ một trong hai mục. Tôi có thể sử dụng một trong hai cách sử dụng

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

hoặc là

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

coi item và BREAD là một giá trị int không đổi.

Trong ví dụ trên, cái nào hoạt động nhanh hơn và tại sao?


Có thể đây cũng là câu trả lời cho java: stackoverflow.com/questions/767821/…
Tobias,

19
Nói chung, từ Wikipedia : Nếu phạm vi giá trị đầu vào được xác định là 'nhỏ' và chỉ có một vài khoảng trống, một số trình biên dịch kết hợp trình tối ưu hóa có thể thực sự triển khai câu lệnh switch dưới dạng bảng nhánh hoặc mảng các con trỏ hàm được lập chỉ mục thay vì một loạt các lệnh điều kiện dài dòng. Điều này cho phép câu lệnh switch xác định ngay lập tức nhánh nào sẽ thực thi mà không cần phải thông qua danh sách so sánh.
Felix Kling,

Câu trả lời hàng đầu cho câu hỏi này giải thích nó khá tốt. Bài báo này cũng giải thích mọi thứ khá tốt.
bezmax

Tôi hy vọng rằng trong hầu hết các trường hợp, một trình biên dịch tối ưu hóa sẽ có thể tạo mã có các đặc điểm hiệu suất tương tự. Trong mọi trường hợp, bạn sẽ phải gọi hàng triệu lần để nhận thấy bất kỳ sự khác biệt nào.
Mitch Wheat,

2
Bạn nên cảnh giác với những cuốn sách đưa ra những tuyên bố như thế này mà không có giải thích / chứng minh / lập luận.
matt b

Câu trả lời:


110

Bởi vì có các mã byte đặc biệt cho phép đánh giá tuyên bố chuyển đổi hiệu quả khi có nhiều trường hợp.

Nếu được triển khai với câu lệnh IF, bạn sẽ có một dấu kiểm, một bước nhảy sang mệnh đề tiếp theo, một dấu kiểm, một bước nhảy sang mệnh đề tiếp theo, v.v. Với chuyển đổi JVM tải giá trị để so sánh và lặp lại qua bảng giá trị để tìm một kết quả phù hợp, nhanh hơn trong hầu hết các trường hợp.


6
Không lặp lại dịch thành "kiểm tra, nhảy"?
fivetwentysix

17
@fivetwentysix: Không, hãy tham khảo phần này để biết thông tin: Arma.com/underthehood/flowP.html . Trích dẫn từ bài báo: Khi JVM gặp lệnh chuyển đổi bảng, nó có thể chỉ cần kiểm tra xem khóa có nằm trong phạm vi được xác định bởi thấp và cao hay không. Nếu không, nó sẽ lấy phần bù nhánh mặc định. Nếu vậy, nó chỉ trừ thấp từ khóa để có được một phần bù vào danh sách các phần bù nhánh. Bằng cách này, nó có thể xác định độ lệch nhánh thích hợp mà không cần phải kiểm tra từng giá trị trường hợp.
bezmax

1
(i) a switchcó thể không được dịch thành một tableswitchlệnh bytecode - nó có thể trở thành một lookupswitchlệnh thực hiện tương tự như if / else (ii) thậm chí một tableswitchlệnh bytecode có thể được biên dịch thành một chuỗi if / else bởi JIT, tùy thuộc vào các yếu tố chẳng hạn như số lượng cases.
assylias


34

Một switchtuyên bố không phải lúc nào cũng nhanh hơn một iftuyên bố. Nó mở rộng quy mô tốt hơn một danh sách dài các if-elsecâu lệnh vì switchcó thể thực hiện tra cứu dựa trên tất cả các giá trị. Tuy nhiên, đối với một điều kiện ngắn, nó sẽ không nhanh hơn và có thể chậm hơn.


5
Hãy hạn chế "dài". Lớn hơn 5? Lớn hơn 10? hoặc hơn như 20 - 30?
vanderwyst

11
Tôi nghi ngờ nó phụ thuộc. Đối với tôi, 3 gợi ý trở lên của nó switchsẽ rõ ràng hơn nếu không muốn nói là nhanh hơn.
Peter Lawrey

Trong điều kiện nào nó có thể chậm hơn?
Eric

1
@Eric nó chậm hơn đối với một số lượng nhỏ giá trị, đặc biệt là Chuỗi hoặc int bị thưa thớt.
Peter Lawrey

8

JVM hiện tại có hai loại mã byte chuyển đổi: LookupSwitch và TableSwitch.

Mỗi trường hợp trong một câu lệnh switch có một phần bù số nguyên, nếu các phần bù này liền kề nhau (hoặc hầu hết là liền nhau không có khoảng trống lớn) (trường hợp 0: trường hợp 1: trường hợp 2, v.v.), thì TableSwitch được sử dụng.

Nếu các hiệu số được dàn trải với khoảng trống lớn (trường hợp 0: trường hợp 400: trường hợp 93748:, v.v.), thì LookupSwitch được sử dụng.

Nói tóm lại, sự khác biệt là TableSwitch được thực hiện trong thời gian không đổi vì mỗi giá trị trong phạm vi các giá trị có thể được cung cấp một độ lệch mã byte cụ thể. Do đó, khi bạn cung cấp cho câu lệnh một phần bù là 3, nó sẽ biết nhảy lên trước 3 để tìm nhánh chính xác.

Công tắc tra cứu sử dụng tìm kiếm nhị phân để tìm nhánh mã chính xác. Điều này chạy trong thời gian O (log n), vẫn tốt, nhưng không phải là tốt nhất.

Để biết thêm thông tin về điều này, hãy xem tại đây: Sự khác biệt giữa LookupSwitch của JVM và TableSwitch?

Vì vậy, theo cách nào là nhanh nhất, hãy sử dụng phương pháp này: Nếu bạn có 3 trường hợp trở lên có giá trị liên tiếp hoặc gần như liên tiếp, hãy luôn sử dụng một công tắc.

Nếu bạn có 2 trường hợp, hãy sử dụng câu lệnh if.

Đối với bất kỳ tình huống nào khác, rất có thể chuyển đổi nhanh hơn, nhưng nó không được đảm bảo, vì tìm kiếm nhị phân trong LookupSwitch có thể gặp phải tình huống xấu.

Ngoài ra, hãy nhớ rằng JVM sẽ chạy tối ưu hóa JIT trên các câu lệnh if sẽ cố gắng đặt nhánh nóng nhất đầu tiên trong mã. Đây được gọi là "Dự đoán nhánh". Để biết thêm thông tin về điều này, hãy xem tại đây: https://dzone.com/articles/branch-prediction-in-java

Kinh nghiệm của bạn có thể khác nhau. Tôi không biết rằng JVM không chạy tối ưu hóa tương tự trên LookupSwitch, nhưng tôi đã học được cách tin tưởng vào các tối ưu hóa của JIT và không cố gắng vượt qua trình biên dịch.


1
Kể từ khi đăng bài này, tôi nhận thấy rằng "chuyển đổi biểu thức" và "đối sánh mẫu" sẽ đến với Java, có thể ngay sau Java 12. openjdk.java.net/jeps/325 openjdk.java.net/jeps/305 Vẫn chưa có gì là cụ thể, nhưng có vẻ như những điều này sẽ làm cho switchmột tính năng ngôn ngữ thậm chí còn mạnh mẽ hơn. Ví dụ, đối sánh mẫu sẽ cho phép instanceoftra cứu hiệu quả và mượt mà hơn nhiều . Tuy nhiên, tôi nghĩ rằng sẽ an toàn khi giả định rằng đối với các trường hợp chuyển đổi cơ bản / nếu, quy tắc tôi đã đề cập sẽ vẫn được áp dụng.
HesNotTheStig

1

Vì vậy, nếu bạn có kế hoạch có vô số gói bộ nhớ không thực sự là một chi phí lớn ngày nay và các mảng khá nhanh. Bạn cũng không thể dựa vào câu lệnh switch để tự động tạo bảng nhảy và như vậy, việc tự tạo kịch bản bảng nhảy sẽ dễ dàng hơn. Như bạn có thể thấy trong ví dụ dưới đây, chúng tôi giả định tối đa là 255 gói.

Để có được kết quả dưới đây, bạn cần sự trừu tượng .. Tôi sẽ không giải thích cách hoạt động của nó, vì vậy hy vọng bạn hiểu được điều đó.

Tôi đã cập nhật điều này để đặt kích thước gói thành 255 nếu bạn cần thêm thì bạn sẽ phải kiểm tra giới hạn cho (id <0) || (id> chiều dài).

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

Chỉnh sửa vì tôi sử dụng Jump Table trong C ++ rất nhiều nên bây giờ tôi sẽ hiển thị một ví dụ về bảng nhảy con trỏ hàm. Đây là một ví dụ rất chung chung, nhưng tôi đã chạy nó và nó hoạt động chính xác. Hãy nhớ rằng bạn phải đặt con trỏ thành NULL, C ++ sẽ không tự động làm điều này như trong 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;
}

Ngoài ra, một điểm khác mà tôi muốn đưa ra là Sự chia cắt và Chinh phục nổi tiếng. Vì vậy, ý tưởng mảng 255 ở trên của tôi có thể được giảm xuống không còn 8 câu lệnh if trong trường hợp xấu nhất.

Tức là nhưng hãy nhớ rằng nó sẽ lộn xộn và khó quản lý nhanh và cách tiếp cận khác của tôi nói chung là tốt hơn, nhưng điều này được sử dụng trong trường hợp các mảng sẽ không cắt nó. Bạn phải tìm ra trường hợp sử dụng của mình và khi mỗi tình huống hoạt động tốt nhất. Cũng như bạn sẽ không muốn sử dụng một trong hai phương pháp này nếu bạn chỉ có một vài lần kiểm tra.

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
Tại sao chuyển hướng kép? Vì dù sao cũng phải hạn chế ID, tại sao không chỉ kiểm tra giới hạn của id đến là 0 <= id < packets.lengthvà đảm bảo packets[id]!=nullrồi thực hiện packets[id].execute(data)?
Lawrence Dol,

vâng, xin lỗi vì phản hồi muộn đã xem xét lại điều này .. và tôi không biết tôi đang nghĩ cái quái gì mà tôi đã cập nhật bài đăng lol và giới hạn các gói ở kích thước của một byte không dấu để không cần kiểm tra độ dài.
Jeremy Trifilo

0

Ở mức bytecode, biến chủ đề chỉ được tải một lần vào thanh ghi bộ xử lý từ một địa chỉ bộ nhớ trong tệp .class có cấu trúc được tải bởi Runtime và đây là một câu lệnh switch; trong khi trong câu lệnh if, một lệnh jvm khác được tạo bởi DE biên dịch mã của bạn và điều này yêu cầu mỗi biến được tải vào các thanh ghi mặc dù cùng một biến được sử dụng như trong câu lệnh if tiếp theo. Nếu bạn biết viết mã bằng hợp ngữ thì điều này sẽ trở nên phổ biến; mặc dù các cox được biên dịch trong java không phải là mã bytecode hay mã máy trực tiếp, khái niệm điều kiện ở đây vẫn nhất quán. Vâng, tôi đã cố gắng tránh tính kỹ thuật sâu hơn khi giải thích. Tôi hy vọng tôi đã làm cho khái niệm rõ ràng và phân minh. Cảm ơn bạn.

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