Java異常的性能分析


詳見:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt276

在Java中拋異常的性能是非常差的。通常來說,拋一個異常大概會消耗100到1000個時鍾節拍。

通常是出現了意想不到的錯誤,我們才會往外拋異常。也就是說,我們肯定不希望一個進程一秒鍾就拋出上千個異常。不過有時候你確實會碰到有些方法把異常當作事件一樣往外拋。我們在這篇文章中已經看到一個這樣的典范):sun.misc.BASE64Decoder之所以性能很差就是因為它通過拋異常來對外請求道,”我還需要更多的數據“:

1
2
3
4
5
6
7
8
9
10
at java.lang.Throwable.fillInStackTrace(Throwable.java:-1)
at java.lang.Throwable.fillInStackTrace(Throwable.java:782)
- locked <0x6c> (a sun.misc.CEStreamExhausted)
at java.lang.Throwable.<init>(Throwable.java:250)
at java.lang.Exception.<init>(Exception.java:54)
at java.io.IOException.<init>(IOException.java:47)
at sun.misc.CEStreamExhausted.<init>(CEStreamExhausted.java:30)
at sun.misc.BASE64Decoder.decodeAtom(BASE64Decoder.java:117)
at sun.misc.CharacterDecoder.decodeBuffer(CharacterDecoder.java:163)
at sun.misc.CharacterDecoder.decodeBuffer(CharacterDecoder.java:194)

如果你用一個數字開頭,字母結尾的字符串來運行下這篇文章里面的pack方法,你也會碰到類似的情況。我們來看下用那個方法打包"12345"和"12345a"需要多長的時間:

1
2
Made 100.000.000 iterations for string '12345' : time = 12.109 sec
Made 1.000.000 iterations for string '12345a' : time = 21.764 sec

可以看到,’12345a'迭代的次數要比‘12345’少100倍。也就是說這個方法處理'12345a'慢了差不多200倍。大多數的處理時間都在填充異常的棧跟蹤信息了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
at java.lang.Throwable.fillInStackTrace(Throwable.java:-1)
         at java.lang.Throwable.fillInStackTrace(Throwable.java:782)
         - locked <0x87> (a java.lang.NumberFormatException)
         at java.lang.Throwable.<init>(Throwable.java:265)
         at java.lang.Exception.<init>(Exception.java:66)
         at java.lang.RuntimeException.<init>(RuntimeException.java:62)
         at java.lang.IllegalArgumentException.<init>(IllegalArgumentException.java:53)
         at java.lang.NumberFormatException.<init>(NumberFormatException.java:55)
         at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
         at java.lang.Long.parseLong(Long.java:441)
         at java.lang.Long.valueOf(Long.java:540)
         at com.mvorontsov.javaperf.StrConvTests.pack(StrConvTests.java:69)
         at com.mvorontsov.javaperf.StrConvTests.test(StrConvTests.java:38)
         at com.mvorontsov.javaperf.StrConvTests.main(StrConvTests.java:29)

通過手動解析數字,我們可以很容易提升pack方法的性能。不過不要忘了——不到萬不得已,不要隨便優化。如果你只是解析幾個輸入參數而已—— keep it simple,就用JDK的方法就好了。如果你要解析大量的消息,又必須調用一個類似pack這樣的方法——那確實得去優化一下了。

新的pack方法和舊的實現差不太多——把一個字符串轉化成一個盡可能小的Character/Integer/Long/Double/String類型,使得result.toString().equals(orginalString)為true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public static Object strToObject( final String str )
{
     if ( str == null || str.length() > 17 )
     //out of Long range
         return str;
     }
     if ( str.equals( "" ) )
         return "" ; //ensure interned string is returned
     if ( str.length() == 1 )
         return str.charAt( 0 ); //return Character
     //if starts with zero - support only "0" and "0.something"
     if ( str.charAt( 0 ) == '0' )
     {
         if ( str.equals( "0" ) )
             return 0;
         if ( !str.startsWith( "0." ) )  //this may be a double
             return str;
     }
  
     long res = 0;
     int sign = 1;
     for ( int i = 0; i < str.length(); ++i )
     {
         final char c = str.charAt( i );
         if ( c <= '9' && c >= '0' )
             res = res * 10 + ( c - '0' );
         else if ( c == '.' )
         {
             //too lazy to write a proper Double parser, use JDK one
             try
             {
                 final Double val = Double.valueOf( str );
                 //check if value converted back to string equals to an original string
                 final String reverted = val.toString();
                 return reverted.equals( str ) ? val : str;
             }
             catch ( NumberFormatException ex )
             {
                 return str;
             }
         }
         else if ( c == '-' )
         {
             if ( i == 0 )
                 sign = -1; //switch sign at first position
             else
                 return str; //otherwise it is not numeric
         }
         else if ( c == '+' )
         {
             if ( i == 0 )
                 sign = 1; //sign at first position
             else
                 return str; //otherwise it is not numeric
         }
         else //non-numeric
             return str;
     }
     //cast to int if value is in int range
     if ( res < Integer.MAX_VALUE )
         return ( int ) res * sign;
     //otherwise return Long
     return res * sign;
}

很驚訝吧,新的方法解析數字比JDK的實現快多了!很大一個原因是因為JDK在解析的最后,調用了一個支持的解析方法,像這樣:

public static int parseInt( String s, int radix ) throws NumberFormatException

新的方法和舊的相比(注意方法調用的次數——對於非數字串pack只調用了1百萬次,而別的情況能調用到千萬級別):

1
2
3
4
Pack: Made 100.000.000 iterations for string '12345' : time = 12.145 sec
Pack: Made 1.000.000 iterations for string '12345a' : time = 23.248 sec
strToObject: Made 100.000.000 iterations for string '12345' : time = 6.311 sec
strToObject: Made 100.000.000 iterations for string '12345a' : time = 5.807 sec

總結

千萬不要把異常當成返回碼一樣用,或者當作可能發生的事件(尤其是和IO無關的方法)往外拋。拋異常的代價太昂貴了,對於一般的方法,至少要慢百倍以上。

如果你每條數據都需要解析,又經常會出現非數值串的時候,盡量不要用Number子類型的parse*/valueOf這些方法。為了性能考慮,你應當手動解析它們。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM