面試官又整新活,居然問我for迴圈用i++和++i哪個效率高?

碼農參上發表於2021-11-24

原創:微信公眾號 碼農參上,歡迎分享,轉載請保留出處。

前幾天,一個小夥伴告訴我,他在面試的時候被面試官問了這麼一個問題:

在for迴圈中,到底應該用 i++ 還是 ++i ?

聽到這,我感覺這面試官確實有點不按套路出牌了,放著好好的八股文不問,淨整些么蛾子的東西。在臨走的時候,小夥伴問面試官這道題的答案是什麼,面試官沒有明確告訴答案,只是說讓從程式執行的效率角度自己思考一下。

好吧,既然這個問題被拋了出來,那我們就見招拆招,也給以後面試的小夥伴們排一下坑。

思路

前面提到,這個搞事情的面試官說要從執行效率的角度思考,那我們就拋開語義上的區別,從執行結果以外的效率來找找線索。回想一下,我們在以前介紹CAS的文章中提到過,後置自增i++和前置自增++i都不是原子操作,那麼實際在執行過程中是什麼樣的呢?下面,我們從位元組碼指令的角度,從底層進行一波分析。

i++ 執行過程

先寫一段簡單的程式碼,核心功能就只有賦值和自增操作:

public static void main(String[] args) {
    int i=3;
    int j=i++;
    System.out.println(j);
}

下面用javap對位元組碼檔案進行反編譯,看一下實際執行的位元組碼指令:

是不是有點難懂?沒關係,接下來我們用圖解的形式來直觀地看看具體執行的過程,也幫大家解釋一下晦澀的位元組碼指令是如何操作棧幀中的資料結構的,為了簡潔起見,在圖中只列出棧幀中比較重要的運算元棧區域性變數表

上面的程式碼中除去列印語句,整體可以拆分成兩步,我們先看第一步 int i=3 是如何執行的 。

上面兩條運算元棧和區域性變數表相關的位元組碼指令還是比較容易理解的,下面再看一下第二步int j=i++的執行過程:

在上圖中需要注意的是,iinc能夠直接更新區域性變數表中的變數值,它不需要把數值壓到運算元棧中就能夠直接進行操作。在上面的過程中,拋去賦值等其他操作,i++實際執行的位元組碼指令是:

2: iload_1
3: iinc    1, 1

如果把它翻譯成我們能看懂的java程式碼,可以理解為:

int temp=i;
i=i+1;

也就是說在這個過程中,除了必須的自增操作以外,又引入了一個新的區域性變數,接下來我們再看看++i的執行過程。

++i 執行過程

我們對上面的程式碼做一點小小的改動,僅把i++換成++i,再來分析一下++i的執行過程是怎樣的。

public static void main(String[] args) {
    int i=3;
    int j=++i;
    System.out.println(j);
}

同樣,用javap反編譯位元組碼檔案:

int i=3對應前兩行位元組碼指令,執行過程和前面i++例子中完全相同,可以忽略不計,重點還是通過圖解的方式看一下int j=++i對應的位元組碼指令的執行過程:

拋去賦值操作,++i實際執行過程只有一行位元組碼指令:

2: iinc    1, 1

轉換成能理解的java程式碼的話,++i實際執行的就在區域性變數中執行的:

i=i+1;

這麼看來,在使用++i時確實比i++少了一步操作,少引入了一個區域性變數,如果在運算結果相同的場景下,使用++i的話的確效率會比i++高那麼一點點。

那麼回到開頭的問題,兩種自增方式應用在for迴圈中執行的時候,那種效率更高呢?剛才得出的結論仍然適用於for迴圈中嗎,別急,讓我們接著往下看。

for迴圈中的自增

下面準備兩段包含了for迴圈的程式碼,分別使用i++後置自增和++i前置自增:

//i++ 後置自增
public class ForIpp {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}
//++i 前置自增
public class ForPpi {
    public static void main(String[] args) {
        for (int i = 0; i < 5; ++i) {
            System.out.println(i);
        }
    }
}

老規矩,還是直接反編譯後的位元組碼檔案,然後對比一下指令的執行過程:

到這裡,有趣的現象出現了,兩段程式執行的位元組碼指令部分居然一模一樣。先不考慮為什麼會有這種現象,我們還是通過圖解來看一下位元組碼指令的執行過程:

可以清晰的看到,在進行自增時,都是直接執行的iinc,在之前並沒有執行iload的過程,也就是說,兩段程式碼執行的都是++i。這一過程的驗證其實還有更簡單的方法,直接使用idea開啟位元組碼檔案,就可以看到最終for迴圈中使用的相同的前置自增方式。

那麼,為什麼會出現這種現象呢?歸根結底,還是java編譯器對於程式碼的優化,在兩種自增方式中,如果沒有賦值操作,那麼都會被優化成一種方式,就像下面的兩個方法的程式碼:

void ipp(){
    int i=3;
    i++;
}
void ppi(){
    int i=3;
    ++i;
}

最終執行時的位元組碼指令都是:

0: iconst_3
1: istore_1
2: iinc    1, 1
5: return

可以看到,在上面的這種特定情況下,程式碼經過編譯器的優化,保持了語義不變,並通過轉換語法的形式提高了程式碼的執行效率。所以再回到我們開頭的問題,就可以得出結論,在for迴圈中,通過jvm進行編譯優化後,不論是i++還是++i,最終執行的方式都是++i,因此執行效率是相同的。

所以,以後再碰到這種半吊子的面試官,和你談for迴圈中i++++i的效率問題,自信點,直接把答案甩在他的臉上,兩種方式效率一樣!

本文程式碼基於Java 1.8.0_261-b12 版本測試

作者簡介,碼農參上,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。個人微信DrHydra9,歡迎新增好友,進一步交流。

相關文章