Sự khác biệt về hiệu suất tương đối của câu lệnh if / else so với câu lệnh switch trong Java là gì?


123

Lo lắng về hiệu suất của ứng dụng web của tôi, tôi đang tự hỏi câu lệnh "if / else" hoặc câu lệnh switch nào tốt hơn về hiệu suất?


6
Bạn có bất kỳ lý do gì để nghĩ rằng cùng một mã bytecode không được tạo cho hai cấu trúc?
Pascal Cuoq

2
@Pascal: có thể có tối ưu hóa được thực hiện bằng cách sử dụng tra cứu bảng thay vì danh sách, ifv.v.
jldupont

18
"Sớm tối ưu hóa là gốc rễ của mọi tội lỗi" - Donald Knuth
missingfaktor

104
Mặc dù đây chắc chắn là sự tối ưu hóa quá sớm, nhưng "Việc không tuân thủ một câu trích dẫn nào được đưa ra ngoài ngữ cảnh là lý do chúng ta cần một máy tính đa lõi cao cấp chỉ để hiển thị GUI đáp ứng hợp lý ngày nay" - Tôi.
Lawrence Dol,

2
Knuth có một đầu óc chính xác. Xin lưu ý rằng vòng loại "quá sớm". Tối ưu hóa là một mối quan tâm hoàn toàn hợp lệ. Điều đó nói lên rằng, một máy chủ bị ràng buộc IO và các nút thắt cổ chai của mạng và I / O đĩa là những thứ có mức độ quan trọng hơn bất kỳ thứ gì khác mà bạn đang diễn ra trong máy chủ của mình.
alphazero

Câu trả lời:


108

Đó là tối ưu hóa vi mô và tối ưu hóa quá sớm, đó là những điều xấu xa. Thay vì lo lắng về tính ổn định và khả năng bảo trì của mã được đề cập. Nếu có nhiều hơn hai if/elsekhối được dán lại với nhau hoặc kích thước của nó là không thể đoán trước, thì bạn có thể cân nhắc một switchtuyên bố.

Ngoài ra, bạn cũng có thể lấy Đa hình . Đầu tiên, hãy tạo một số giao diện:

public interface Action { 
    void execute(String input);
}

Và nắm được tất cả các triển khai trong một số Map. Bạn có thể làm điều này tĩnh hoặc động:

Map<String, Action> actions = new HashMap<String, Action>();

Cuối cùng thay thế if/elsehoặc switchbằng một cái gì đó như thế này (bỏ qua các kiểm tra tầm thường như nullpointers):

actions.get(name).execute(input);

thể nhỏ hơn if/elsehoặc nhỏ hơn switch, nhưng mã ít nhất có thể bảo trì tốt hơn nhiều.

Khi bạn đang nói về các ứng dụng web, bạn có thể sử dụng HttpServletRequest#getPathInfo()làm khóa hành động (cuối cùng viết thêm một số mã để tách phần cuối cùng của pathinfo đi trong một vòng lặp cho đến khi một hành động được tìm thấy). Bạn có thể tìm thấy câu trả lời tương tự ở đây:

Nếu bạn đang lo lắng về hiệu suất ứng dụng web Java EE nói chung, thì bạn cũng có thể thấy bài viết này hữu ích. Có những khu vực khác mà đưa ra một nhiều hơn nữa đạt được hiệu suất hơn chỉ (vi) tối ưu hóa mã Java thô.


1
hoặc xem xét đa hình thay thế
jk.

Điều đó thực sự được khuyến khích hơn trong trường hợp số lượng khối if / else "không thể đoán trước".
BalusC

73
Tôi không nhanh chóng loại bỏ tất cả tối ưu hóa ban đầu là "xấu xa". Quá hung hăng là điều ngu ngốc, nhưng khi đối mặt với các cấu trúc có thể đọc được tương đương, việc chọn một thiết bị được biết là hoạt động tốt hơn là một quyết định thích hợp.
Brian Knoblauch

8
Phiên bản tra cứu HashMap có thể dễ dàng chậm hơn 10 lần so với lệnh tìm kiếm bảng. Tôi sẽ không gọi đây là "microlower"!
x4u

7
Tôi quan tâm đến việc thực sự biết hoạt động bên trong của Java trong trường hợp chung với các câu lệnh switch - Tôi không quan tâm đến việc có ai đó nghĩ rằng điều này có liên quan đến việc ưu tiên quá mức tối ưu hóa sớm hay không. Nói như vậy, tôi hoàn toàn không biết tại sao câu trả lời này lại được ủng hộ nhiều như vậy và tại sao nó lại là câu trả lời được chấp nhận ... điều này không có gì để trả lời câu hỏi ban đầu.
searchhengine27

125

Tôi hoàn toàn đồng ý với ý kiến ​​rằng việc tối ưu hóa quá sớm là điều nên tránh.

Nhưng đúng là máy ảo Java có các mã bytecodes đặc biệt có thể được sử dụng cho switch ().

Xem WM Spec ( lookupswitchtableswitch )

Vì vậy, có thể có một số mức tăng hiệu suất, nếu mã là một phần của biểu đồ CPU hiệu suất.


60
Tôi tự hỏi tại sao bình luận này không được đánh giá cao hơn: nó là bình luận có nhiều thông tin nhất trong số chúng. Ý tôi là: tất cả chúng ta đều đã biết về việc tối ưu hóa quá sớm là không tốt và như vậy, không cần phải giải thích điều đó lần thứ 1000.
Folkert van Heusden,

5
+1 Kể từ stackoverflow.com/a/15621602/89818 , có vẻ như hiệu suất tăng thực sự ở đó và bạn sẽ thấy lợi thế nếu sử dụng trường hợp 18+.
caw

52

Rất khó xảy ra trường hợp if / else hoặc một công tắc sẽ là nguồn gốc gây ra tai họa về hiệu suất của bạn. Nếu bạn đang gặp sự cố về hiệu suất, trước tiên bạn nên thực hiện phân tích hồ sơ hiệu suất để xác định đâu là điểm chậm. Tối ưu hóa sớm là gốc rễ của mọi điều ác!

Tuy nhiên, có thể nói về hiệu suất tương đối của switch so với if / else với các tối ưu hóa trình biên dịch Java. Đầu tiên lưu ý rằng trong Java, các câu lệnh switch hoạt động trên một miền rất hạn chế - số nguyên. Nói chung, bạn có thể xem một câu lệnh chuyển đổi như sau:

switch (<condition>) {
   case c_0: ...
   case c_1: ...
   ...
   case c_n: ...
   default: ...
}

where c_0, c_1..., và c_Nlà các số tích phân là mục tiêu của câu lệnh switch và <condition>phải chuyển thành một biểu thức số nguyên.

  • Nếu tập hợp này là "dày đặc" - nghĩa là (max (c i ) + 1 - min (c i )) / n> α, trong đó 0 <k <α <1, trong đó klớn hơn một số giá trị thực nghiệm, a bảng nhảy có thể được tạo, có hiệu quả cao.

  • Nếu tập hợp này không quá dày đặc, nhưng n> = β, cây tìm kiếm nhị phân có thể tìm thấy mục tiêu trong O (2 * log (n)) mà vẫn hiệu quả.

Đối với tất cả các trường hợp khác, một câu lệnh switch chính xác hiệu quả như một chuỗi các câu lệnh if / else tương đương. Các giá trị chính xác của α và β phụ thuộc vào một số yếu tố và được xác định bởi mô-đun tối ưu hóa mã của trình biên dịch.

Cuối cùng, tất nhiên, nếu miền của <condition>không phải là các số nguyên, một câu lệnh switch hoàn toàn vô dụng.


+1. Rất có thể thời gian dành cho I / O mạng dễ dàng làm lu mờ vấn đề cụ thể này.
Adam Paynter

3
Cần lưu ý rằng công tắc hoạt động với nhiều thứ hơn là chỉ int. Từ Hướng dẫn Java: "Một công tắc hoạt động với các kiểu dữ liệu nguyên thủy byte, short, char và int. Nó cũng hoạt động với các kiểu liệt kê (được thảo luận trong Các kiểu Enum), lớp Chuỗi và một số lớp đặc biệt bao bọc một số kiểu nguyên thủy nhất định : Ký tự, Byte, Ngắn gọn và Số nguyên (được thảo luận trong Số và Chuỗi). " Hỗ trợ cho Chuỗi là bổ sung gần đây hơn; được thêm vào trong Java 7. docs.oracle.com/javase/tutorial/java/nutsandbolts/switch.html
atraudes

1
@jhonFeminella Bạn có thể vui lòng so sánh các hiệu ứng khái niệm BIG O cho Chuỗi Java7 trong Swtich so với Chuỗi trong if / else if ..?
Kanagavelu Sugumar

Chính xác hơn, javac 8 đưa ra một trọng lượng từ 3 đến phức tạp thời gian qua phức tạp không gian: stackoverflow.com/a/31032054/895245
Ciro Santilli郝海东冠状病六四事件法轮功

11

Sử dụng công tắc!

Tôi ghét duy trì if-else-blocks! Có bài kiểm tra:

public class SpeedTestSwitch
{
    private static void do1(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            switch (r)
            {
                case 0:
                    temp = 9;
                    break;
                case 1:
                    temp = 8;
                    break;
                case 2:
                    temp = 7;
                    break;
                case 3:
                    temp = 6;
                    break;
                case 4:
                    temp = 5;
                    break;
                case 5:
                    temp = 4;
                    break;
                case 6:
                    temp = 3;
                    break;
                case 7:
                    temp = 2;
                    break;
                case 8:
                    temp = 1;
                    break;
                case 9:
                    temp = 0;
                    break;
            }
        }
        System.out.println("ignore: " + temp);
    }

    private static void do2(int loop)
    {
        int temp = 0;
        for (; loop > 0; --loop)
        {
            int r = (int) (Math.random() * 10);
            if (r == 0)
                temp = 9;
            else
                if (r == 1)
                    temp = 8;
                else
                    if (r == 2)
                        temp = 7;
                    else
                        if (r == 3)
                            temp = 6;
                        else
                            if (r == 4)
                                temp = 5;
                            else
                                if (r == 5)
                                    temp = 4;
                                else
                                    if (r == 6)
                                        temp = 3;
                                    else
                                        if (r == 7)
                                            temp = 2;
                                        else
                                            if (r == 8)
                                                temp = 1;
                                            else
                                                if (r == 9)
                                                    temp = 0;
        }
        System.out.println("ignore: " + temp);
    }

    public static void main(String[] args)
    {
        long time;
        int loop = 1 * 100 * 1000 * 1000;
        System.out.println("warming up...");
        do1(loop / 100);
        do2(loop / 100);

        System.out.println("start");

        // run 1
        System.out.println("switch:");
        time = System.currentTimeMillis();
        do1(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));

        // run 2
        System.out.println("if/else:");
        time = System.currentTimeMillis();
        do2(loop);
        System.out.println(" -> time needed: " + (System.currentTimeMillis() - time));
    }
}

Mã chuẩn C # của tôi để đo điểm chuẩn


Bạn có thể vui lòng (đôi khi) giải thích một chút về cách bạn đã làm tiêu chuẩn này không?
DerMike

Cảm ơn bạn rất nhiều cho bản cập nhật của bạn. Ý tôi là, chúng khác nhau theo một bậc của độ lớn - tất nhiên là có thể. Bạn có chắc rằng trình biên dịch không chỉ tối ưu hóa các switches đi?
DerMike

@DerMike Tôi không nhớ mình nhận được kết quả cũ bằng cách nào. Tôi đã rất khác ngày hôm nay. Nhưng hãy tự mình thử và cho tôi biết nó diễn ra như thế nào.
Bitterblue

1
khi tôi chạy nó trên máy tính xách tay của mình; thời gian chuyển đổi cần thiết: 3585, nếu / thời gian khác cần thiết: 3458 vì vậy nếu / khác là tốt hơn :) hay không tồi tệ hơn
Halil

1
Chi phí chủ đạo trong thử nghiệm là tạo số ngẫu nhiên. Tôi đã sửa đổi bài kiểm tra để tạo ra số ngẫu nhiên trước vòng lặp và sử dụng giá trị tạm thời để cấp lại vào r. Sau đó, quá trình chuyển đổi nhanh hơn gần như gấp đôi so với chuỗi if-else.
boneill

8

Tôi nhớ đã đọc rằng có 2 loại câu lệnh Switch trong Java bytecode. (Tôi nghĩ đó là trong 'Điều chỉnh hiệu suất Java'. Một là một triển khai rất nhanh sử dụng các giá trị số nguyên của câu lệnh switch để biết độ lệch của mã sẽ được thực thi. Điều này sẽ yêu cầu tất cả các số nguyên phải liên tiếp và trong một phạm vi được xác định rõ Tôi đoán rằng việc sử dụng tất cả các giá trị của Enum cũng sẽ thuộc loại đó.

Tôi đồng ý với nhiều áp phích khác mặc dù ... có thể còn sớm để lo lắng về điều này, trừ khi đây là mã rất nóng.


4
+1 cho nhận xét mã nóng. Nếu nó trong vòng lặp chính của bạn thì nó không sớm.
KingAndrew

Có, javac triển khai switchmột số cách khác nhau, một số cách hiệu quả hơn những cách khác. Nói chung, hiệu quả sẽ không kém hơn một " ifbậc thang" thẳng tiến , nhưng có đủ các biến thể (đặc biệt là với JITC) mà khó có thể chính xác hơn nhiều.
Hot Licks vào

8

Theo Cliff Click trong bài nói chuyện Java One 2009 A Crash Course in Modern Hardware :

Ngày nay, hiệu suất bị chi phối bởi các kiểu truy cập bộ nhớ. Bộ nhớ cache không chiếm ưu thế - bộ nhớ là đĩa mới. [Trang trình bày 65]

Bạn có thể lấy các slide đầy đủ của anh ấy ở đây .

Cliff đưa ra một ví dụ (kết thúc trên Slide 30) cho thấy rằng ngay cả khi CPU thực hiện đổi tên thanh ghi, dự đoán nhánh và thực thi suy đoán, nó chỉ có thể bắt đầu 7 hoạt động trong 4 chu kỳ đồng hồ trước khi phải chặn do hai lần bỏ lỡ bộ nhớ cache. 300 chu kỳ đồng hồ để quay trở lại.

Vì vậy, anh ấy nói để tăng tốc chương trình của bạn, bạn không nên xem xét loại vấn đề nhỏ này, nhưng trên những vấn đề lớn hơn, chẳng hạn như liệu bạn có đang thực hiện các chuyển đổi định dạng dữ liệu không cần thiết hay không, chẳng hạn như chuyển đổi "SOAP → XML → DOM → SQL →… "mà" chuyển tất cả dữ liệu qua bộ nhớ đệm ".


4

Trong thử nghiệm của tôi, hiệu suất tốt hơn là ENUM> MAP> SWITCH> IF / ELSE IF trong Windows7.

import java.util.HashMap;
import java.util.Map;

public class StringsInSwitch {
public static void main(String[] args) {
    String doSomething = null;


    //METHOD_1 : SWITCH
    long start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        switch (input) {
        case "Hello World0":
            doSomething = "Hello World0";
            break;
        case "Hello World1":
            doSomething = "Hello World0";
            break;
        case "Hello World2":
            doSomething = "Hello World0";
            break;
        case "Hello World3":
            doSomething = "Hello World0";
            break;
        case "Hello World4":
            doSomething = "Hello World0";
            break;
        case "Hello World5":
            doSomething = "Hello World0";
            break;
        case "Hello World6":
            doSomething = "Hello World0";
            break;
        case "Hello World7":
            doSomething = "Hello World0";
            break;
        case "Hello World8":
            doSomething = "Hello World0";
            break;
        case "Hello World9":
            doSomething = "Hello World0";
            break;
        case "Hello World10":
            doSomething = "Hello World0";
            break;
        case "Hello World11":
            doSomething = "Hello World0";
            break;
        case "Hello World12":
            doSomething = "Hello World0";
            break;
        case "Hello World13":
            doSomething = "Hello World0";
            break;
        case "Hello World14":
            doSomething = "Hello World0";
            break;
        case "Hello World15":
            doSomething = "Hello World0";
            break;
        }
    }

    System.out.println("Time taken for String in Switch :"+ (System.currentTimeMillis() - start));




    //METHOD_2 : IF/ELSE IF
    start = System.currentTimeMillis();

    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);

        if(input.equals("Hello World0")){
            doSomething = "Hello World0";
        } else if(input.equals("Hello World1")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World2")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World3")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World4")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World5")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World6")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World7")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World8")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World9")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World10")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World11")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World12")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World13")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World14")){
            doSomething = "Hello World0";

        } else if(input.equals("Hello World15")){
            doSomething = "Hello World0";

        }
    }
    System.out.println("Time taken for String in if/else if :"+ (System.currentTimeMillis() - start));









    //METHOD_3 : MAP
    //Create and build Map
    Map<String, ExecutableClass> map = new HashMap<String, ExecutableClass>();
    for (int i = 0; i <= 15; i++) {
        String input = "Hello World" + (i & 0xF);
        map.put(input, new ExecutableClass(){
                            public void execute(String doSomething){
                                doSomething = "Hello World0";
                            }
                        });
    }


    //Start test map
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "Hello World" + (i & 0xF);
        map.get(input).execute(doSomething);
    }
    System.out.println("Time taken for String in Map :"+ (System.currentTimeMillis() - start));






    //METHOD_4 : ENUM (This doesn't use muliple string with space.)
    start = System.currentTimeMillis();
    for (int i = 0; i < 99999999; i++) {
        String input = "HW" + (i & 0xF);
        HelloWorld.valueOf(input).execute(doSomething);
    }
    System.out.println("Time taken for String in ENUM :"+ (System.currentTimeMillis() - start));


    }

}

interface ExecutableClass
{
    public void execute(String doSomething);
}



// Enum version
enum HelloWorld {
    HW0("Hello World0"), HW1("Hello World1"), HW2("Hello World2"), HW3(
            "Hello World3"), HW4("Hello World4"), HW5("Hello World5"), HW6(
            "Hello World6"), HW7("Hello World7"), HW8("Hello World8"), HW9(
            "Hello World9"), HW10("Hello World10"), HW11("Hello World11"), HW12(
            "Hello World12"), HW13("Hello World13"), HW14("Hello World4"), HW15(
            "Hello World15");

    private String name = null;

    private HelloWorld(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
        doSomething = "Hello World0";
    }

    public static HelloWorld fromString(String input) {
        for (HelloWorld hw : HelloWorld.values()) {
            if (input.equals(hw.getName())) {
                return hw;
            }
        }
        return null;
    }

}





//Enum version for betterment on coding format compare to interface ExecutableClass
enum HelloWorld1 {
    HW0("Hello World0") {   
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    }, 
    HW1("Hello World1"){    
        public void execute(String doSomething){
            doSomething = "Hello World0";
        }
    };
    private String name = null;

    private HelloWorld1(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void execute(String doSomething){
    //  super call, nothing here
    }
}


/*
 * http://stackoverflow.com/questions/338206/why-cant-i-switch-on-a-string
 * https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-3.html#jvms-3.10
 * http://forums.xkcd.com/viewtopic.php?f=11&t=33524
 */ 

Time taken for String in Switch :3235 Time taken for String in if/else if :3143 Time taken for String in Map :4194 Time taken for String in ENUM :2866
halil

@halil Tôi không chắc mã này hoạt động như thế nào trên các môi trường khác nhau, nhưng bạn đã đề cập if / elseif tốt hơn Switch và Map, điều đó tôi không thể thuyết phục vì if / elseif phải thực hiện nhiều lần so sánh bằng.
Kanagavelu Sugumar

2

Đối với hầu hết switchvà hầu hết if-then-elsecác khối, tôi không thể tưởng tượng rằng có bất kỳ mối quan tâm đáng kể hoặc đáng kể nào liên quan đến hiệu suất.

Nhưng đây là vấn đề: nếu bạn đang sử dụng một switchkhối, cách sử dụng của nó gợi ý rằng bạn đang chuyển sang một giá trị được lấy từ một tập hợp các hằng số đã biết tại thời điểm biên dịch. Trong trường hợp này, bạn thực sự không nên sử dụng các switchcâu lệnh nếu bạn có thể sử dụng một enumvới các phương thức cụ thể.

So với một switchcâu lệnh, một enum cung cấp an toàn kiểu tốt hơn và mã dễ bảo trì hơn. Enums có thể được thiết kế để nếu một hằng số được thêm vào tập hợp hằng số, mã của bạn sẽ không biên dịch mà không cung cấp một phương thức cụ thể cho giá trị mới. Mặt khác, việc quên thêm một khối mới casevào một switchkhối đôi khi chỉ có thể bị bắt gặp trong thời gian chạy nếu bạn đủ may mắn đã thiết lập khối của mình để ném một ngoại lệ.

Hiệu suất giữa switchvà một enumphương pháp cụ thể không đổi không được chênh lệch đáng kể, nhưng phương pháp sau dễ đọc hơn, an toàn hơn và dễ bảo trì hơ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.