程序函數條件與返回的區別?

用lua代碼說明

1.

function dum(t)
if t then return 1 end
return 0
end

2.

function dum(t)
if t then return 1 else return 0 end
end

這兩個函數在功能上是一樣的,實現上有一些差別

我的問題是

1.在語義上是否有差別

2.在編譯器中的實現方式是否相同

3.在邏輯上,這兩個函數是否完全一致


1、語義上沒有差別。但是當有嵌套if...else邏輯並且代碼使用縮進來表示代碼塊的嵌套結構時,第一種寫法(可以稱之為「guard」寫法)可以使用較少的縮進,因為else是隱含的。

if (condition) {
// then do something
return ...; // early return
}
// otherwise

這種寫法在if的then塊的末尾是個return。由於它位於函數中間而不是末尾,所以也叫做「early return」。這種early return使這個then塊可以被看作控制流的終結塊,而在這個if之後的部分就會隱含著else(或者說if (!condition))的語義。

2、在需要關心的層面上實現相同;有差異的僅是細微細節,可忽略。

Lua給它生成的位元組碼幾乎一樣:

Lua 5.2.3:

第一種寫法:

main & (3 instructions at 0x125e3c0)
0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function
1 [4] CLOSURE 0 0 ; 0x125e660
2 [1] SETTABUP 0 -1 0 ; _ENV "dum"
3 [4] RETURN 0 1

function & (7 instructions at 0x125e660)
1 param, 2 slots, 0 upvalues, 1 local, 2 constants, 0 functions
1 [2] TEST 0 0
2 [2] JMP 0 2 ; to 5
3 [2] LOADK 1 -1 ; 1
4 [2] RETURN 1 2
5 [3] LOADK 1 -2 ; 0
6 [3] RETURN 1 2
7 [4] RETURN 0 1

第二種寫法:

main & (3 instructions at 0x16153c0)
0+ params, 2 slots, 1 upvalue, 0 locals, 1 constant, 1 function
1 [3] CLOSURE 0 0 ; 0x1615660
2 [1] SETTABUP 0 -1 0 ; _ENV "dum"
3 [3] RETURN 0 1

function & (8 instructions at 0x1615660)
1 param, 2 slots, 0 upvalues, 1 local, 2 constants, 0 functions
1 [2] TEST 0 0
2 [2] JMP 0 3 ; to 6
3 [2] LOADK 1 -1 ; 1
4 [2] RETURN 1 2
5 [2] JMP 0 2 ; to 8
6 [2] LOADK 1 -2 ; 0
7 [2] RETURN 1 2
8 [3] RETURN 0 1

(這裡位元組碼層面的差別純粹是因為Lua的前端編譯器沒做好優化…不過這裡就多了一個JMP而已,而且是dead code不會被執行到,放那兒無傷大雅。這符合Lua的設計理念,至於好不好就見仁見智了)

3、完全一致。

==========================================================

如果用Java來寫題主的例子,會是:

public class xx {
public static int dum1(boolean t) {
if (t) return 1;
return 0;
}

public static int dum2(boolean t) {
if (t) return 1;
else return 0;
}
}

然後藉助HotSpot Client Compiler(C1)來分析一個「合理的編譯器」的做法。dum1和dum2經過C1的前端解析(parse)後生成的IR分別如下:

dum1:

static jint xx.dum1(jboolean)
After Generation of HIR

B3 -&> B0 [0, 0]
Locals size 1 [static jint xx.dum1(jboolean)]
0 i3 [method parameter]
_p__bci__use__tid__instruction__________________________________________________ (HIR)
. 0 0 v11 std entry B0

B0 &<- B3 -&> B2,B1 [0, 1] std
Locals size 1 [static jint xx.dum1(jboolean)]
0 i3
_p__bci__use__tid__instruction__________________________________________________ (HIR)
1 0 i4 0
. 1 0 v5 if i3 == i4 then B2 else B1

B1 &<- B0 [4, 5] Locals size 1 [static jint xx.dum1(jboolean)] _p__bci__use__tid__instruction__________________________________________________ (HIR) 4 0 i6 1 . 5 0 i7 ireturn i6 B2 &<- B0 [6, 7] Locals size 1 [static jint xx.dum1(jboolean)] _p__bci__use__tid__instruction__________________________________________________ (HIR) 6 0 i8 0 . 7 0 i9 ireturn i8

dum2:

static jint xx.dum2(jboolean)
After Generation of HIR

B3 -&> B0 [0, 0]
Locals size 1 [static jint xx.dum2(jboolean)]
0 i3 [method parameter]
_p__bci__use__tid__instruction__________________________________________________ (HIR)
. 0 0 v11 std entry B0

B0 &<- B3 -&> B2,B1 [0, 1] std
Locals size 1 [static jint xx.dum2(jboolean)]
0 i3
_p__bci__use__tid__instruction__________________________________________________ (HIR)
1 0 i4 0
. 1 0 v5 if i3 == i4 then B2 else B1

B1 &<- B0 [4, 5] Locals size 1 [static jint xx.dum2(jboolean)] _p__bci__use__tid__instruction__________________________________________________ (HIR) 4 0 i6 1 . 5 0 i7 ireturn i6 B2 &<- B0 [6, 7] Locals size 1 [static jint xx.dum2(jboolean)] _p__bci__use__tid__instruction__________________________________________________ (HIR) 6 0 i8 0 . 7 0 i9 ireturn i8

可見兩者完全一樣。

兩者的控制流圖(control flow graph)都是:

其中B3是C1為了簡化分析而人為創建的「方法入口塊」,B0、B1、B2是原本程序包含的邏輯。

而兩者的數據流圖(data flow graph)都是:

從這個意義上說,C1認為dum1和dum2的語義完全一樣,因而可以用完全一樣的IR來表示其邏輯,那麼後面的實現就一模一樣了。

這就是 @空明流轉 孔明大大所說的「構造出來的Data Flow/Control Flow Graph都應該是一致的」的例證。

該例的可視化用的是C1Visualizer,需要的數據文件我放在這裡了:https://gist.github.com/rednaxelafx/1c78677fdfefab483653#file-xx-cfg

我用來跑測試的HotSpot VM是OpenJDK8的正式版之前的開發版。它在Linux/x64上最終編譯出來的dum1和dum2都是:(這裡用Intel語法)

cmp esi, 1 ; if (t)
jeq label_false
mov eax, 1 ; 1
ret
label_false:
xor eax, eax ; 0
ret

顯然也沒優化好。理想情況下,如果省略frame pointer應該只要:

mov eax, esi ; t
ret ; return t

就好。而保留frame pointer的話則是類似:

push rbp
mov rbp, rsp
sub rsp, &
mov eax, esi
mov rsp, rbp
pop rbp
ret

(具體保留frame pointer的代碼看情況,這是一種可能)

要給C1加上這個優化難倒不難但是要做的結構改動不大符合C1的精神。Anyway先放一邊回頭有空給做個patch看看。

HotSpot Server Compiler (C2)則實現了更徹底的優化,對兩者編譯出來的結果都是:

sub rsp, 0x18
mov [rsp+0x10], rbp
mov eax, esi
add rsp, 0x10
pop rbp
test eax, &

; {poll_return}
ret

這就是保留frame pointer的情況下最優化的代碼了。ret前看似多餘的test是HotSpot VM的JIT編譯代碼與GC交互用的safepoint;對HotSpot VM來說它是必要的,不過作為例子講解這裡可以忽略不記。

==========================================================

要展開說的話這個話題有許多可聊的。

首先,第一種寫法(下面就把它叫做「guard寫法」)只在命令式語言里有效。

如果一門語言:

  • 不支持提前return(early return),或者說
  • 是面向表達式(expression-oriented)的——每個語法結構都是一個表達式,必須有值

那顯然guard寫法就派不上用場了。不少函數式語言就是如此。

還有更有趣的情況:if-then-else不是一個內建的語法結構而是個庫函數(或者叫方法/消息)。

Smalltalk就是這樣的例子。它的if-then-else是Boolean對象上的ifTrue:ifFalse:消息,True和False分別是Boolean的子類並且覆寫了ifTrue:ifFalse:消息的實現。用法是:

&
ifTrue: [ & ]
ifFalse: [ & ]

這裡ifTrue:ifFalse:是一個完整的消息名,傳參數時必須把then和else都傳進去而不能只傳一個。這樣就沒辦法用guard寫法了。

Smalltalk的ifTrue:ifFalse:不通過任何特殊功能(「special form」)實現,而只是充分利用了Smalltalk虛方法多態與block的語義來實現。也就是說用戶可以自己用純Smalltalk寫出自己的Boolean類並且實現功能一樣的ifTrue:ifFalse:消息。

有些Smalltalk VM為了提高性能,在底層把Boolean類及ifTrue:ifFalse:看作特例硬編碼了,但這只是內部實現細節,不影響上層Smalltalk語義的純潔性。

Common Lisp的if看起來也像是個普通庫「函數」(form)。但它實際被定義為special form,其求值順序與普通的form不一樣。Common Lisp的special form是寫死的,就那麼多,用戶不能自行添加新的special form。當然,Common Lisp的if form可以用macro和lambda來模擬出來。

待續…


。。。不同寫法,計算機語言也是種語言,不要太計較這些。

南方人說:你速度好慢

北方人說:你真墨跡

MD不是一個意思么


@vczh 不要答得太輕率了。邏輯等價不意味著實現相同。編譯器畢竟是 rule-based 機器。證明「等價」對人腦都是個複雜的事情。

@RednaxelaFX 的答案就下功夫多了。不過結論「Lua 的前端編譯器沒做好優化」也不對。Lua author 的論文里說了,Lua 會被用作像 JSON 那樣的 data description 語言,編譯速度很重要,所以複雜的編譯優化不做。


在這個例子里體現不出語義上的差別。

如果你接觸過函數式編程,就知道其中的if大多被定義成了函數而非關鍵詞,即if的每個分支都必須有返回值,而單純的附加分支往往通過類似單子一類的概念實現。

在Common Lisp中,嚴格處理每個分支的情形用if,否則用when。

就通常的實踐而言,尤其涉及到side effect的時候,對比下列兩種情形:

情形一:

if (condition_A) {
set(foo, x);
} else if (condition_B) {
set(foo, y);
}

// foo必然被設定成了x異或y,異或沒有進行設定,三種情況互斥,foo的內容清晰而明確。

// 值得注意的是,由於condition A和B不一定是互斥事件,此處else還體現出了在A和B都成立時,

// A有優先權。這是else帶來的附加語義,在理解和使用時需要細心。

// 使用else顯得結構更加緊湊,它使得控制流完全處於唯一的if-block之中。

情形二:

if (condition_A) {
set(foo, x);
}
if (condition_B) {
set(foo, y);
}

// foo先後被設定了x以及y,也可能沒有設定,三種情況完全相互兼容

// 但是foo最終是何內容?不明確。取決於set的含義。

// 在我看來,當你這麼寫的時候,傾向於告訴他人這種A和B兼容的情形,而非互斥的情形。

// 同時也透露出,函數set可能具有相對複雜的內涵,說不定它包含了一個事件隊列,

// 專門處理x和y的設定順序。

// 既然這裡沒有採用else,也就不涉及A和B的優先權問題,即時程上的前者自然優先。

// 不使用else的話,控制流其實流出了一個if-block,而流入了另一個if-block,結構相對鬆散。

所以,是否使用else,其實很直接的反映了你的程序的內在邏輯結構。在現有的實踐下,我們常說explicit優於implicit,就是要從代碼層面儘可能充分的表達語義。

綜上,我們不應該為了少輸入幾個字母省掉有助語義表達的else。對於不增加無用分支的else,有助於明確互斥情形的else,我的建議是統統寫上。

===========================================================

補充:至於編譯器是否對這種嚴格分支的情形進行優化,我覺得這完全取決於編譯器的設計者,而不是語言應用者應該考慮的問題。若是為了如此細化的編譯誘導而省略else特有的語義,我覺得不划算,這相當於放棄了高級語言的特性,還不如在相關問題上直接選擇彙編語言。在哪一層語言就考慮哪一層的問題。高級語言的優勢就是語義更明確而性能在其次(從而最大化避免細枝末節的錯誤),底層語言的優勢就是操作流非常清晰且充分反映性能要求。

關於高級語言的編碼風格,如Lisp和Python,感興趣的同學可以了解一下Peter Norvig,他的風格很有代表性。

當然,題主的問題是很細節很有針對性,幾個答主能把位元組碼給出也很贊。不過從我的角度而言,充分利用else的表達能力可以幫自己建立更好的代碼結構觀念,也可以習慣性地避免一些可能存在的邏輯含混的陷阱(比如上面的代碼情形二)。


如果是針對這樣的簡單語句,他們構造出來的Data Flow/Control Flow Graph都應該是一致的。

所以

1. 語義上沒有區別

2. 實現上應當一致

3. 完全等價


1、沒有區別

2、實現相同

3、完全一致


以前寫代碼,習慣if...else...都寫完整,然後可能出現這種情況

if(A)
{
return a;
}
else
{
if(B)
{
return b;
}
else
{
if(C)
{
return c;
}
else
{
return x;
}
}
}

後來發現這樣寫應該更好

if(A)
{
return a;
}
if(B)
{
return b;
}
if(C)
{
return c;
}
return x;


邏輯上是一致的,編譯器實現上可能有差異,語義不好評價。

至少對 lua-5.1.5 ,實現有差異:

&>if t then return 1 end; return 0
[1] getglobal 0 0 ; R0 := t
[2] test 0 0 ; if R0 then pc+=1 (goto [4])
[3] jmp 2 ; pc+=2 (goto [6])
[4] loadk 0 1 ; R0 := 1
[5] return 0 2 ; return R0
[6] loadk 0 2 ; R0 := 0
[7] return 0 2 ; return R0
[8] return 0 1 ; return

&>if t then return 1 else return 0 end
[1] getglobal 0 0 ; R0 := t
[2] test 0 0 ; if R0 then pc+=1 (goto [4])
[3] jmp 3 ; pc+=3 (goto [7])
[4] loadk 0 1 ; R0 := 1
[5] return 0 2 ; return R0
[6] jmp 2 ; pc+=2 (goto [9])
[7] loadk 0 2 ; R0 := 0
[8] return 0 2 ; return R0
[9] return 0 1 ; return

因為 lua 的編譯器為了簡單,else 前是強制插入一個 jmp 。而且基本不做高級的優化,這個 jmp 後面也不會被優化掉。


一樣大家都說了,關於選擇,在LLVM的c++規範里看到過關於這個的討論,建議不用else,因為代碼短(對c++),邏輯上更簡單。

----------------

不過這和@Lee Shellay的分析相衝突。我想原因可能在語言方面。c++的編譯器處理這種優化簡直小菜一碟,而高級語言尤其是動態語言就難了。另外LLVM僅僅指單一return,這種情況下省else邏輯反而更清楚(嵌套少)。


如果是c++的這兩種寫法,,在性能是會有區別嗎,,我是問群眾的,,←_←


1.完全相同 因為結果相同

2.不一樣 一個是編譯一個句子 一個是編譯兩個句子 效率也不同

3.一致 得到結果是一樣


語義的話,其實基本上是一致的。實現的功能翻譯過來,描述的結果都是一致的。

至於在彙編等級的代碼實現,就要看編譯器了。不過也就是多一個jump的程度。

實現的效果是一樣的,應該說完全一致。


if(true)
return 1;
return 0;

if(true)
return 1;
else
return 0;

是像這樣么,只是加不加else的區別吧


推薦閱讀:

Python 使用 list 作為函數參數時,參數是否會初始化?
怎麼去實現一個簡單文本編輯器?
使用IDE會不會降低程序員的智商?
怎樣才能做到編程語言的「一通百通」?
學習一門新的編程語言有什麼推薦的輪子可以拿來練手的?

TAG:編程語言 | 編程 | 編譯原理 | Lua |