淺談「++i」與「i++」
概要
這標題是不是喚醒了很久很久以前初學程式時的記憶?記得當時的老師是怎樣說的嗎?我們直接看程式碼片段,如下:
Code Fragment A:static void printIPlus() {
int i = 0;
System.out.print(i++);
System.out.println(i);
}
以及:
Code Fragment B:static void printPlusI() {
int i = 0;
System.out.print(++i);
System.out.println(i);
}
事實上,不論是「i++」或「++i」,其執行後的結果都是讓「i 值加 1」;所以其差異並不在於結果,而是過程。
仔細觀察上述兩個程式碼片段,其差異僅在「++」運算子擺放的位置;在分別執行後,我們可以得知「Code Fragment A」的打印結果是「01」,而「Code Fragment B」的打印結果則是「11」,兩者不同。
然而稍微有點程式基礎的人都會知道,這是合情合理的。因為「i++」代表著後綴運算,所以「i」會先執行「當前動作」 ,然後再將「i」值進行加 1 的動作,執行結果理所當然是「01」;反觀「++i」,它則是先將「i」值加 1 後再進行「當前動作」。
記得當時老師並沒有講為什麼?反正我也不在意,背起來就是了,當時我的學習是囫圇吞棗的;直到某次,我遇見了這樣的「For Loop」,如下:
for (int i = 0; i <= Byte.MAX_VALUE; ++i) {
// code here...
}
當時的某位職場前輩跟我說,因為「++i」比「i++」來得更有效率,有這種事情?
正文
時戳驗證
想當然耳,身為理科生就該拿出實是求證的態度,於是我撰寫了下列一段程式碼來驗證,如下:
然後,一執行,程式碼就會告訴我們一件事情,就是「兩者執行效率並沒有差別」,囧了。
位元碼
秉持著打破沙鍋的精神,我們換個方式,我們從「位元碼」的角度去探討。
我們先來看一段程式碼:
Code Fragment C:static void testIPlus() {
int i = 0;
i++;
}static void testPlusI() {
int i = 0;
++i;
}
接著我們將「Code Fragment C」進行編譯,然後我們去找到它的「位元碼」,並透過「javap」的指令去解析它,圖如下:
結果如下圖:
是不是有種砂鍋破了的感覺?剛才我們是以「時間差」來做判斷,我們還能推說是因為現在電腦的效能太好,所以導致差異不顯著。
但是現在卻是赤裸裸的說:「沒錯,『i++』與『++i』所編譯出的位元碼是一模樣的。」
難道是前輩說錯了嗎?別急,且聽筆者娓娓道來。
編譯優化
就某種程度來說,前輩的說法是正確的。
理由是「i++」在處理上,會先保存當前「i」值為了稍後作使用,而這樣的行為導致它比「++i」耗費更多的記憶體資源。
但這是在「i++」與「++i」確實有行為上的差異時。
當「i++」跟「++i」是有本質上的差異的;兩者在不同的情況下,會有不同的編譯結果,如下:
Code Fragment D:static void testIPlus2() {
int i = 0;
System.out.println(i++);
}
static void printPlusI2() {
int i = 0;
System.out.println(++i);
}
同樣地,我們藉由「javap」的指令去分析:
仔細注意,兩者的順序不同了;在上方的部分,是先打印,在加一,下方則是相反。
這樣的結果並不令人意外,因為它符合我們的預期。
那為什麼在我們上述的實驗,「Code Fragment C」會編譯出一樣的結果呢?事實上,這一切的原因就藏在編譯器中。
其實編譯器是個「溫暖」的存在,很多時候,我們都以為它只是個「翻譯」的工具,但實際上,除了「翻譯」之外,它一直都在我們的背後默默地做事情,像是惡名昭彰的「編譯器蜜糖」就是其中之一,它讓我們可以更簡潔地去撰寫程式碼;但今天的主角不是它,而是「編譯器」的另外一個重要地功能「編譯優化」。
實際上,「編譯」是門非常高深的學問,甚至它涉及到「JVM」的運作過程,因此,我並沒有打算在這篇文章中深入地去探討它;但關於「JVM」的概念建立,我會建議可以去閱讀下列幾篇文章:
簡單地說,「Java」的編譯可以分為「靜態編譯」和「動態編譯」,前者如「javac」,指的是將「編譯器從『原始碼 (.java)』編譯成『位元碼(.class)』的過程。」,而後者,大名鼎鼎的「JIT Compiler」就是其中之一。
接著回到我們的問題,就如我剛才說過了,是因為「編譯器」的關係才導致「i++」和「++i」在某些時候會產生出相同的編譯結果。
實際上,編譯器會根據原始碼的前後狀況自動「編譯優化」。比較直白的說法是,當我們下達「javac」指令後,編譯器並不是直接將我們的原始碼直接翻譯成位元碼,而是會加以調整後再產生「優化後的位元碼」,以「i++」為例,它就會去判斷該處是否有使用到「後綴」的特性,若非必要,編譯結果就會自動省去了這些部分「命令碼」,因此才會在某些時候使得「i++」和「++i」的編譯結果是相同的。
最後,在我撰寫這篇文章的時候,其實參考了不少相關資料,而其中有一篇文章:「追求真理:++i为何比i++执行效率高?」,它是從 C/C++ 語言的角度去探討,其過程是類似的,而差異在於 C/C++ 的編譯過程不需要先編譯成「ByteCode」,而是直接編譯成系統可讀的執行檔,但也因此,我覺得更能看清楚不同編譯器對於程式碼編譯的影響。
完整程式碼連結:My GitHub