2012-09-16 57 views
8

有局部變量在基於堆棧的中間語言,如CIL或Java字節碼,爲什麼會有局部變量?只能使用堆棧。手工製作的IL可能不那麼容易,但編譯器肯定可以做到。但我的C#編譯器沒有。爲什麼是基於堆棧的字節碼IL

堆棧和局部變量都是方法專用的,並在方法返回時超出範圍。所以它不可能與方法外部可見的副作用(來自另一個線程)有關。

JIT編譯器將消除加載和存儲都棧槽和生成機器碼的時候,如果我是正確的局部變量,所以JIT編譯器也沒有看到局部變量的需要。

在另一方面,C#編譯器生成的載入和存儲局部變量,啓用優化編譯時也是如此。爲什麼?


採取例如,下面的人爲的例子的代碼:

static int X() 
{ 
    int a = 3; 
    int b = 5; 
    int c = a + b; 
    int d; 
    if (c > 5) 
     d = 13; 
    else 
     d = 14; 
    c += d; 
    return c; 
} 

當在C#編譯,以優化時,其產生:

ldc.i4.3  # Load constant int 3 
    stloc.0   # Store in local var 0 
    ldc.i4.5  # Load constant int 5 
    stloc.1   # Store in local var 1 
    ldloc.0   # Load from local var 0 
    ldloc.1   # Load from local var 1 
    add    # Add 
    stloc.2   # Store in local var 2 
    ldloc.2   # Load from local var 2 
    ldc.i4.5  # Load constant int 5 
    ble.s label1 # If less than, goto label1 
    ldc.i4.s 13  # Load constant int 13 
    stloc.3   # Store in local var 3 
    br.s label2  # Goto label2 
label1: 
    ldc.i4.s 14  # Load constant int 14 
    stloc.3   # Store in local var 3 
label2: 
    ldloc.2   # Load from local var 2 
    ldloc.3   # Load from local var 3 
    add    # Add 
    stloc.2   # Store in local var 2 
    ldloc.2   # Load from local var 2 
    ret    # Return the value 

注意加載和存儲的四個局部變量。我可以在不使用任何局部變量的情況下編寫完全相同的操作(不考慮明顯的常量傳播優化)。

ldc.i4.3  # Load constant int 3 
    ldc.i4.5  # Load constant int 5 
    add    # Add 
    dup    # Duplicate top stack element 
    ldc.i4.5  # Load constant int 5 
    ble.s label1 # If less than, goto label1 
    ldc.i4.s 13  # Load constant int 13 
    br.s label2  # Goto label2 
label1: 
    ldc.i4.s 14  # Load constant int 14 
label2: 
    add    # Add 
    ret    # Return the value 

這對我來說似乎是正確的,而且更短,更高效。那麼,爲什麼基於堆棧的中間語言有局部變量呢?爲什麼優化編譯器如此廣泛地使用它們?

+3

您不能*總是*做出如您在示例中演示的簡單轉換。 –

+0

這個問題問爲什麼「命名的插槽」是*需要*或爲什麼C#「優化」的輸出看起來過於冗長(例如*在這種情況下使用*)? – 2012-09-17 00:02:06

+0

即使在某些情況下(這種情況?)需要或使用「命名空位」,爲什麼優化編譯器不能消除大多數加載和存儲?這似乎很微不足道。我肯定錯過了什麼。 – Virtlink

回答

4

根據不同的情況,特別是當呼叫參與其中的參數進行重新排序相匹配的呼叫,一個純粹的堆棧是不夠的,如果你沒有在您的處置寄存器或變量。如果你只想做這個棧,你需要額外的棧操作abilties,比如交換/交換堆棧的兩個頂層項目的能力。最後,雖然在這種情況下可能會將所有東西都表示爲純粹的基於堆棧的形式,但它可能會給代碼增加很多複雜性,使其膨脹並使其更難以優化(局部變量是理想的候選人被緩存在寄存器中)。

還記得在.NET中,你可以通過引用傳遞參數,你怎麼可以創建IL此方法調用沒有本地變量?

bool TryGet(int key, out string value) {} 
+0

我不明白簽名如何改變問題本身。對於IL來說,可能有一個「writeToOutParam」操作(副作用),否則完全是基於堆棧的。 – 2012-09-17 00:06:49

+1

我認爲Lucero是指呼叫方的來電方。當沒有局部變量時,局部變量的地址作爲第二個參數傳遞給'TryGet'方法?但是,這當然可以成爲堆棧插槽的「指針」。而且,堆棧中的值不是更好的候選緩存在寄存器中嗎? – Virtlink

0

這個答案純粹是推測 - 但我懷疑答案有3個部分。

1:代碼轉變爲喜歡在Dup的局部變量是非常不平凡的,甚至當你忽視的副作用。它爲優化增加了很多複雜性和潛在的大量執行時間。

2:你不能忽視副作用。在一切都只是一個文字的例子中,很容易知道這些值是堆棧還是當地人,因此完全可以控制當前的指令。一旦這些值來自堆,靜態內存或方法調用,就不能再使用Dup來替換當地的東西了。更改順序可能會改變事情的實際工作方式,並可能由於副作用或外部訪問共享內存而導致意想不到的後果。這意味着通常你不能進行這些優化。

3:堆棧值比局部變量不是一個好假設更快的假設 - 對於特定的IL->機器代碼轉換,堆棧值更快,但是沒有理由爲什麼智能JIT不會將堆棧位置放入內存中,而將局部變量放入寄存器中。 JIT的工作是瞭解當前機器的速度和速度,以及解決問題的時間,這是JIT的工作。通過設計,CIL編譯器無法回答當地人或堆棧是否更快;所以這些結果之間的可測量差異僅在代碼大小上。

放在一起,1意味着它很難並且具有不平凡的成本,2意味着現實世界中有價值的情況很少,而3意味着1和2無關。

即使目標是最小化CIL大小,這是CIL編譯器的可測量目標,理由#2將此描述爲對少量情況的小改進。帕累託原理不能告訴我們實現這種優化是一個糟糕的主意,但它會建議有可能更好地使用開發人員時間。

+0

以及我忘記的東西 - 在編譯速度非常重要的真正JIT中,避免Dup操作可能導致最小堆棧大小,從而使JIT的生活更輕鬆。給出相同的結果,最好比結果更快達到結果。 – danwyand

+0

但我仍然看到編譯器放入什麼似乎完全不必要的局部變量(例如'ldfld x; stloc.1; ldarg.0; ldloc.1; ldc.i4.1; add; stfld x;' - 什麼目的'loc.1'服務?),當他們什麼都不加。或者使用'ldarg.0; ldarg.0;'當'ldarg.0;在所有情況下似乎都是安全的? – NetMage