閱讀977 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Clojure筆記:用好type hint

  Clojure的一大優點就是跟Java語言的完美配合,Clojure和Java之間可以相互調用,Clojure可以天然地使用Java平台上的豐富資源。在Clojure裏調用一個類的方法很簡單,利用dot操作符:

user=> (.substring "hello" 3)
"lo"
user=> (.substring "hello" 0 3)
"hel"

    上麵的例子是在clojure裏調用String的substring方法做字符串截取。Clojure雖然是一門弱類型的語言,但是它的Lisp Reader還是能識別大多數常見的類型,比如這裏hello是一個字符串就可以識別出來,3是一個整數也可以,通過這些類型信息可以找到最匹配的substring方法,在生成字節碼的時候避免使用反射,而是直接調用substring方法(INVOKEVIRTUAL指令)。

    但是當你在函數裏調用類方法的時候,情況就變了,例如,定義substr函數:
(defn substr [s begin end] (.substring s begin end))

    我們打開*warn-on-reflection*選項,當有反射的時候告警:

user=> (set! *warn-on-reflection* true)
true
user=> (defn substr [s begin end] (.substring s begin end))
Reflection warning, NO_SOURCE_PATH:22 - call to substring can't be resolved.
#'user/substr
   
    問題出現了,由於函數substr裏沒有任何關於參數s的類型信息,為了調用s的substring方法,必須使用反射來調用,clojure編譯器也警告我們調用substring沒辦法解析,隻能通過反射調用。眾所周知,反射調用是個相對昂貴的操作(對比於普通的方法調用有)。這一切都是因為clojure本身是弱類型的語言,對參數或者返回值你不需要聲明類型而直接使用,Clojure會自動處理類型的轉換和調用。ps.在leiningen裏啟用反射警告很簡單,在project.clj裏設置:

;; Emit warnings on all reflection calls.
  :warn-on-reflection true
   
過多的反射調用會影響效率,有沒有辦法避免這種情況呢?有的,Clojure提供了type hint機製,允許我們幫助編譯器來生成更高效的字節碼。所謂type hint就是給參數或者返回值添加一個提示:hi,clojure編譯器,這是xxx類型,我想調用它的yyy方法,請生成最高效的調用代碼,謝謝合作:
user=> (defn substr [^String s begin end] (.substring s begin end))
#'user/substr
     
    這次沒有警告,^String就是參數s的type hint,提示clojure編譯器說s的類型是字符串,那麼clojure編譯器會從java.lang.String類裏查找名稱為substring並且接收兩個參數的方法,並利用invokevirtual指令直接調用此方法,避免了反射調用。除了target對象(這裏的s)可以添加type hint,方法參數和返回值也可以添加type hint:
user=> (defn ^{:tag String} substr [^String s ^Integer begin ^Integer end] (.substring s begin end))
#'user/substr
    
    返回值添加type hint是利用tag元數據,提示substr的返回類型是String,其他函數在使用substr的時候可以利用這個類型信息來避免反射;而參數的type hint跟target object的type hint一樣以^開頭加上類型,例如這裏begin和end都提示說是Integer類型。

    問題1,什麼時候應該為參數添加type hint呢?我的觀點是,在任何為target object添加type hint的地方,都應該相應地為參數添加type hint,除非你事先不知道參數的類型。為什麼呢?因為clojure查找類方法的順序是這樣:

1.從String類裏查找出所有參數個數為2並且名稱為substring方法
2.遍曆第一步裏查找出來的Method,如果你有設置參數的type hint,則
查找最匹配參數類型的Method;否則,如果第一步查找出來的Method就一個,直接使用這個Method,相反就認為沒有找到對應的Method。
3.如果第二步沒有找到Method,使用反射調用;否則根據該Method元信息生成調用字節碼。

   因此,如果substring方法的兩個參數版本剛好就一個,方法參數有沒有type hint都沒有關係(有了錯誤的type hint反而促使反射的發生),我們都會找到這個唯一的方法;但是如果目標方法的有多個重載方法並且參數相同,而隻是參數類型不同(Java裏是允許方法的參數類型重載的,Clojure隻允許函數的參數個數重載),那麼如果沒有方法參數的type hint,Clojure編譯器仍然無法找到合適的調用方法,而隻能通過反射。
   
   看一個例子,定義get-bytes方法調用String.getBytes:

user=> (defn get-bytes [s charset] (.getBytes s charset))
Reflection warning, NO_SOURCE_PATH:26 - call to getBytes can't be resolved.
#'user/get-bytes
user=> (defn get-bytes [^String s charset] (.getBytes s charset))
Reflection warning, NO_SOURCE_PATH:27 - call to getBytes can't be resolved.
#'user/get-bytes

    第一次定義,s和charset都沒有設置type hint,有反射警告;第二次,s設置了type hint,但是還是有反射警告。原因就在於String.getBytes有兩個重載方法,參數個數都是一個,但是接收不同的參數類型,一個是String的charset名稱,一個Charset對象。如果我們明確地知道這裏charset是字符串,那麼還可以為charset添加type hint:
user=> (defn get-bytes [^String s ^String charset] (.getBytes s charset))
#'user/get-bytes
   
    這次才真正的沒有警告了。總結:在設置type hint的時候,不要隻考慮被調用的target object,也要考慮調用的方法參數。

    問題2:什麼時候應該添加tag元數據呢?理論上,在任何你明確知道返回類型的地方都應該添加tag,但是這不是教條,如果一個偶爾被調用的方法是無需這樣做的。這一點隻對寫庫的童鞋要特別注意。

    Type hint的原理在上文已經大概描述了下,具體到clojure源碼級別,請參考clojure.lang.Compiler.InstanceMethodExpr類的構造函數和emit方法。最後,附送是否使用type hint生成substr函數的字節碼之間的差異對比:
未使用type hint 使用type hint

  // access flags 1

  public invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

   L0

    LINENUMBER 14 L0

   L1

    LINENUMBER 14 L1

    ALOAD 1

    ACONST_NULL

    ASTORE 1

    LDC "substring"

    ICONST_2

    ANEWARRAY java/lang/Object

    DUP

    ICONST_0

    ALOAD 2

    ACONST_NULL

    ASTORE 2

    AASTORE

    DUP

    ICONST_1

    ALOAD 3

    ACONST_NULL

    ASTORE 3

    AASTORE

    INVOKESTATIC clojure/lang/Reflector.invokeInstanceMethod (Ljava/lang/Object;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;

   L2

    LOCALVARIABLE this Ljava/lang/Object; L0 L2 0

    LOCALVARIABLE s Ljava/lang/Object; L0 L2 1

    LOCALVARIABLE begin Ljava/lang/Object; L0 L2 2

    LOCALVARIABLE end Ljava/lang/Object; L0 L2 3

    ARETURN

    MAXSTACK = 0

    MAXLOCALS = 0

public invoke(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

   L0

    LINENUMBER 15 L0

   L1

    LINENUMBER 15 L1

    ALOAD 1

    ACONST_NULL

    ASTORE 1

    CHECKCAST java/lang/String

    ALOAD 2

    ACONST_NULL

    ASTORE 2

    CHECKCAST java/lang/Number

    INVOKESTATIC clojure/lang/RT.intCast (Ljava/lang/Object;)I

    ALOAD 3

    ACONST_NULL

    ASTORE 3

    CHECKCAST java/lang/Number

    INVOKESTATIC clojure/lang/RT.intCast (Ljava/lang/Object;)I

    INVOKEVIRTUAL java/lang/String.substring (II)Ljava/lang/String;

   L2

    LOCALVARIABLE this Ljava/lang/Object; L0 L2 0

    LOCALVARIABLE s Ljava/lang/Object; L0 L2 1

    LOCALVARIABLE begin Ljava/lang/Object; L0 L2 2

    LOCALVARIABLE end Ljava/lang/Object; L0 L2 3

    ARETURN

    MAXSTACK = 0

    MAXLOCALS = 0


    
    對比很明顯,沒有使用type hint,調用clojure.lang.Reflector的invokeInstanceMethod方法,使用反射調用(具體見clojure.lang.Reflector.java),而使用了type hint之後,則直接使用invokevirtual指令(其他方法可能是invokestatic或者invokeinterface等指令)調用該方法,避免了反射。
      

    參考:
文章轉自莊周夢蝶  ,原文發布時間2012-07-10

最後更新:2017-05-18 20:37:07

  上一篇:go  Ring.velocity:render velocity templates for ring in clojure
  下一篇:go  發票識別增值稅發票識別中安未來快人一步