異常高效使用小竅門 — 讀Scala原始碼有感

sorra發表於2019-02-16

另載於 http://www.qingjingjie.com/blogs/11

熟悉Scala的人知道返回值是程式碼塊的最後一句,一般不能提前返回。return關鍵字是用拋異常來實現的,這樣就能提前脫離程式碼塊了。

最近看Scala原始碼,注意到它對return的高效實現,有趣。

Scala咋實現的?

拋的異常類是NonLocalReturnControl,繼承自Throwable,有個欄位用來放置要返回的值。編譯器把return value大致翻譯成throw new NonLocalReturnControl(key/*metadata*/, value)就可以了。

通常Java的異常有三類: Exception, RuntimeException, Error。如果一個物件是Exception但不是RuntimeException,那就是checked exception。而Error通常是JVM丟擲的,例如StackOverflowError, OutOfMemoryError, VerifyError,偶爾也有XML庫拋Error。總之沒有其他直接繼承Throwable的類了。

所以在Scala裡只要注意別無腦catch Throwable,就不會誤攔return了。如果是個伺服器程式,不想let it crash,只要catch Exception和Error就好了。

用拋異常來打斷控制流的做法,在有的Web框架中也出現了,沒啥。但在一門語言的實現中,必須保證高效。

於是我們要知道拋異常為什麼慢!Scala咋解決的?

throw和catch都是很快很快的,畢竟只是幾個地址操作,慢的是new Exception這一步,這裡要讓JVM取得當前的一大串stacktrace填充進去,開銷約為new 200個Object的程度。

Scala的NonLocalReturnControl的竅門是——Override了fillInStackTrace()方法。這個方法本來會呼叫native的fillInStackTrace(int)讓JVM去填stacktrace。覆蓋成空實現,測一下,開銷約為new 6個Object。效果拔群!

還可以提高!去掉synchronized關鍵字,開銷又減半了。最後是new 3個Object的開銷,可以接受!

我想到另一種實現方案是把value塞到一個ThreadLocal裡,異常就不必包含value了,這樣就只需全域性new一次異常物件,每次都throw它。開銷約為new 1個Object。Clojure就喜歡用ThreadLocal來實現一些特性。不過ThreadLocal悠著點用啊,玩不好是要出大事的(下一篇談這個問題)。

相關文章