2010-05-21 38 views
4

這並不意味着主觀,我正在尋找基於資源利用,編譯器性能,GC性能等原因而不是優雅。哦,括號的位置不計算,所以沒有文體評論。以下哪個Java編碼片段比較好?

採取以下循環;

Integer total = new Integer(0); 
Integer i; 
for (String str : string_list) 
{ 
    i = Integer.parse(str); 
    total += i; 
} 

與...

Integer total = 0; 
for (String str : string_list) 
{ 
    Integer i = Integer.parse(str); 
    total += i; 
} 

在第一個,i是函數作用域而在第二它在迴路中的作用範圍。我一直認爲(相信)第一個會更高效,因爲它只是引用已經分配到堆棧上的現有變量,而第二個會在循環的每次迭代中推送和彈出。

還有很多其他的情況,我傾向於將範圍變量的範圍比可能的範圍更廣泛,所以我想我會在這裏要求澄清我的知識缺口。還要注意,初始化時變量的賦值要麼涉及新操作符,要麼不涉及。做任何這種半風格的半優化都會有所作爲嗎?

+0

我假設你的實際循環比你的示例更復雜,實際上使得我不僅僅是一個臨時變量或其他東西... – BoltClock 2010-05-21 21:18:12

+2

爲什麼你首先使用昂貴的Integer而不是int? – bmargulies 2010-05-21 21:23:50

+2

我建議你重新提出問題的方式,重點放在你想要發現的問題上。在Integer中裝箱,取消裝箱和「新」的例子中,效率非常低,它遠遠超過了關於變量範圍的任何理論微觀優化。 – Yishai 2010-05-21 21:35:41

回答

7

第二個是我更喜歡的。除了範圍界定之外,沒有功能上的差異。

在每次迭代中設置相同的變量沒有區別,因爲Integer是不可變的類。現在,如果你是修改一個對象,而不是每次創建一個新對象,那麼就會有所不同。

作爲一個附註,在這個代碼中,您應該使用intInteger.parseInt()而不是IntegerInteger.parse()。你引入了很多不必要的裝箱和拆箱。


編輯:這已經有一段時間,因爲我在字節碼打亂身邊,所以我想我會再次讓我的手髒。

下面是測試類我編譯:

class ScopeTest { 
    public void outside(String[] args) { 
     Integer total = 0; 
     Integer i; 
     for (String str : args) 
     { 
      i = Integer.valueOf(str); 
      total += i; 
     } 
    } 
    public void inside(String[] args) { 
     Integer total = 0; 
     for (String str : args) 
     { 
      Integer i = Integer.valueOf(str); 
      total += i; 
     } 
    } 
} 

字節碼輸出(編譯後javap -c ScopeTest檢索):

Compiled from "ScopeTest.java" 
class ScopeTest extends java.lang.Object{ 
ScopeTest(); 
    Code: 
    0: aload_0 
    1: invokespecial #1; //Method java/lang/Object."<init>":()V 
    4: return 

public void outside(java.lang.String[]); 
    Code: 
    0: iconst_0 
    1: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 
    4: astore_2 
    5: aload_1 
    6: astore 4 
    8: aload 4 
    10: arraylength 
    11: istore 5 
    13: iconst_0 
    14: istore 6 
    16: iload 6 
    18: iload 5 
    20: if_icmpge  55 
    23: aload 4 
    25: iload 6 
    27: aaload 
    28: astore 7 
    30: aload 7 
    32: invokestatic #3; //Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer; 
    35: astore_3 
    36: aload_2 
    37: invokevirtual #4; //Method java/lang/Integer.intValue:()I 
    40: aload_3 
    41: invokevirtual #4; //Method java/lang/Integer.intValue:()I 
    44: iadd 
    45: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 
    48: astore_2 
    49: iinc 6, 1 
    52: goto 16 
    55: return 

public void inside(java.lang.String[]); 
    Code: 
    0: iconst_0 
    1: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 
    4: astore_2 
    5: aload_1 
    6: astore_3 
    7: aload_3 
    8: arraylength 
    9: istore 4 
    11: iconst_0 
    12: istore 5 
    14: iload 5 
    16: iload 4 
    18: if_icmpge  54 
    21: aload_3 
    22: iload 5 
    24: aaload 
    25: astore 6 
    27: aload 6 
    29: invokestatic #3; //Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer; 
    32: astore 7 
    34: aload_2 
    35: invokevirtual #4; //Method java/lang/Integer.intValue:()I 
    38: aload 7 
    40: invokevirtual #4; //Method java/lang/Integer.intValue:()I 
    43: iadd 
    44: invokestatic #2; //Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 
    47: astore_2 
    48: iinc 5, 1 
    51: goto 14 
    54: return 

} 

出乎我的意料,有兩個之間的一個區別:outside() ,變量i仍然佔用了一個寄存器,即使它從實際代碼中被省略(請注意,所有的iloadistore指令都指向一個更高的寄存器)。

JIT編譯器應該縮短這種差異的工作量,但您仍然可以看到限制範圍是一種很好的做法。 (關於我前面的注意事項,你可以看到要添加兩個Integer對象,Java必須用intValue取代兩個整數對象,然後添加它們,然後用valueOf創建一個新的Integer,不要這樣做,除非絕對因爲這是沒有意義的較慢。)

+0

很好的答案,謝謝。 – Simon 2010-05-21 21:57:00

+0

以及JIT完成後的樣子? – stacker 2010-05-21 22:01:58

+0

很好的回答;如果可以的話,會給你+10。 ESP。注意到拳擊/拆箱的浪費 - 這是Java程序設計的一個新陷阱。 – 2010-05-21 22:02:05

0

這都沒有顯著差異除了在最後一次迭代,當參考被清除在第二個例子快(這將是我的偏好 - 與其說是出於這個原因,但清晰度)

保持範圍儘可能小。熱點虛擬機確實逃避分析以確定引用何時不再可訪問,並基於此分配棧上的某些對象而不是堆。儘可能縮小範圍有助於此過程。

我會問你爲什麼使用整數而不是簡單的int ...或者它可能只是作爲例子嗎?

0

第二個好得多。儘可能窄的範圍變量使得代碼更容易到讀取保持,其總體上比這些示例之間的性能差異更重要,這些示例是微不足道的並且容易優化。

+0

風格,而不是資源相關 – Simon 2010-05-21 21:47:37

0

都沒有。 Integer.valueOf(0);將使用對緩存0的引用。:)

4

第二個更好,因爲第一個樣式應該只用於C代碼中作爲它的必需。 Java允許內聯聲明來最小化變量的範圍,你應該利用它。但是你的代碼可以進一步改進:

int total = 0; 
for (String str: stringList) { 
    try { 
     total += Integer.valueOf(str); 
    } catch(NumberFormationException nfe) { 
     // more code to deal with the error 
    } 
} 

遵循Java代碼風格約定。閱讀完整的指導: http://java.sun.com/docs/codeconv/html/CodeConvTOC.doc.html

0

那麼,在這種情況下,你實例化你說i = Integer.parseInt(str)一個Integer原始的每一次(i是Integer),所以(除非Java的知道如何優化它)兩種情況幾乎都是低效的。考慮使用int代替:

int total = 0; 
for (String str : string_list) 
{ 
    int i = Integer.parseInt(str); 
    total += i; 
} 

現在我們又回到了是否把int類型聲明的內部或外部的問題。假設Java編譯器有一個體面的優化舔,我會說這沒關係。不考慮效率,認爲變量儘可能接近它們的使用是一種很好的做法。

+0

謝謝,但問題是*的* Java編譯器有一個體面的優化舔? – Simon 2010-05-21 21:42:37

-1

第二個,因爲你想保持變量的範圍儘可能「內在」。較小範圍的優勢是較少的碰撞機會。在你的例子中,只有幾行,所以優勢可能不那麼明顯。但是如果它更大,那麼小範圍的變量肯定會更有益處。如果其他人以後必須查看代碼,則他們必須一直掃描到方法定義的外部,以瞭解什麼是i。這個論點與我們爲什麼要避免全局變量的觀點並沒有太大的不同。

+0

這是一個文體評論,我一直在尋找基於語言和資源的原因而不是可維護性等。 – Simon 2010-05-21 21:46:46

0

第二個是可讀性,可維護性和效率兩者中較好的一個。

所有這些目標都實現了,因爲您可以簡潔地解釋您正在做什麼以及如何使用變量。你正在向開發人員和編譯器解釋清楚。當在for塊中定義變量i時,大家都知道在塊之外忽略它是安全的,並且該值僅對塊的此迭代有效。這將導致垃圾收集器能夠輕鬆地標記這個內存被釋放。

我建議不要使用Integer作爲中間值。累積總數爲int,並在循環創建對象或取決於自動裝箱。

+0

可讀性和可維護性是風格的,你在其餘答案中沒有證明效率。 – Simon 2010-05-21 21:59:39

0

假設你的列表中有正數,你是認真的與

我基於 資源利用率,編譯器 性能,GC性能等 而不是優雅尋找原因。

你應該自己實現它想:

import java.util.ArrayList; 
import java.util.List; 

public class Int { 
    public static void main(String[] args) { 
     List<String> list = new ArrayList<String>(); 
     list.add("10"); 
     list.add("20"); 
     int total = 0; 
     for (String str : list) { 
      int val = 0; 
      for (char c : str.toCharArray()) { 
       val = val * 10 + (int) c - 48; 
      } 
      total += val; 
     } 
     System.out.print(total); 
    } 
} 

唯一GC相關的事情是toCharArray()這可以通過使用charAt()

0

什麼變量範圍使用的問題,另一個循環替換是一個可讀性問題比任何其他更多。當每個變量被限制在實際使用的範圍內時,代碼會更好理解。

現在,如果我們檢查使用寬範圍/窄範圍的技術後果,我認爲在狹窄範圍內存在性能/優勢。考慮下面的方法,在這裏我們有3個局部變量,屬於一個全局範圍:

private static Random rnd = new Random(); 

public static void test() { 
    int x = rnd.nextInt(); 
    System.out.println(x); 

    int y = rnd.nextInt(); 
    System.out.println(y); 

    int z = rnd.nextInt(); 
    System.out.println(z);  
} 

如果diassemble此代碼(使用-c的javap -verbose {類名}爲例),你會看到,編譯器儲備3個插槽爲test()方法的棧幀結構中的局部變量。

現在,假設我們添加一些人工範圍:

public static void test() { 
    { 
     int x = rnd.nextInt(); 
     System.out.println(x); 
    } 

    { 
     int y = rnd.nextInt(); 
     System.out.println(y); 
    } 

    { 
     int z = rnd.nextInt(); 
     System.out.println(z); 
    } 
} 

如果你現在diassemble的代碼,你會發現,編譯器儲備局部變量只有1插槽。由於示波器是完全獨立的,每次使用x,y或z時,都使用相同的槽#0。

這是什麼意思?

1)窄範圍節省堆棧空間

2)如果我們處理的對象變量,這意味着該對象可以更快地變得不可,因此有資格獲得GC比其他更快。

再一次指出,這2個「優點」確實很小,可讀性問題應該是迄今爲止最重要的問題。