RenderScript: 不只繪圖,還有計算!

上一篇文章一樣,整理自 Android Developer Blog 。這次主要介紹的是「Compute」以及當初設計上的考量,而會使用 RenderScript 來做計算的原因是因為透過 RS 寫成的程式可以直接在多個處理器執行,替 Dalvik 減輕負擔。 讓我們一起來看看:

RenderScript 的三大設計目標:

  1. Portability: 應用程式不能因為硬體的不同就無法執行。光是現今的 ARM 規格就非常多變,例如說:有的有 VFP、有的沒有 NEON,或是不同的暫存器數量。更何況除了 ARM 之外還有許多不同的 CPU 架構(如: x86)、GPU、以及 DSP。
  2. Performance:第二個目標就是在伴隨著 Portability 的限制之下,追求最大的效能。RS 一定要比現存的解決方案(如:NDK) 還要快上許多,才足以說服大家使用。
  3. Usability:第三個就是儘可能地簡化開發複雜度,只要 RS 可以自動化處理的部分,就不能麻煩開發者。

這三大目標當然也會帶來一些 trade-offs,也就是因為這樣,RenderScript、Dalvik、NDK 之間在功能性上就比較不會重疊,而是視各種情境來使用不同的工具解決不同的問題。

設計上的關鍵抉擇

  • **選擇 C99 作為標準:**雖然程式語言這麼多,但是因為 RS 本身的功能偏重於繪圖,所以選擇一個比較接近 Shading style languages 是比較好的,因為在某些圖形應用上透過指標處理資料結構會比較方便。排除了沒有指標的程式語言,所以我們考慮了 C/C++,C++的特色是我們非常想要的,但是也因為功能太強大,導致 Portability 會有些問題。而且某些進階的功能是很難執行在非 CPU 的硬體上的。所以我們最後選擇了 C 語言,既保有效能、對於開發者而言也比較熟悉、更重要的是在絕大多數的硬體上都可以順利執行。

  • **工作流程(Workflow):**在比較舊版的時候(Eclair 2.1 ~ Gingerbread 2.3),RS 的程式碼是在執行中才被 libacc 編譯的,但是我們後來發現這非常不方便,假設你要除錯,你必須先寫好整個程式、編譯整個程式、安裝程式、但卻到執行階段才開始編譯你的 RS 程式,光是找出語法上的錯誤也得經過這個漫長的步驟。而且在比較弱的 CPU 上,會限制你可以做的靜態分析,也會影響最佳化時 scope 的大小。所以我們決定採用 LLVM ,我們先在本機端透過修改過的 Clang 做好編譯以及最佳化動作,這時候會編程 bitcode 格式的檔案,然後在執行時使用 libbcc 把 bitcode 轉換為 machine code 執行 RS 的程式,包含與機器相關的最佳化動作也在此進行。 Usability 是 RS 設計的主要考量,因為大部分的計算與繪圖平台都需要詳細制定 glue logic 以把高效能部分的程式碼銜接回應用部分的程式碼,而這部份是最容易產生錯誤而且難以撰寫的。這時候我們在用來本機端執行靜態分析的編譯器就可以幫助我們解決這個問題。每一個 user script 都會產生一個 Dalvik 「glue」 class。而這些類別的名稱都會取決於你的 script ,大大簡化了從 Dalvik 銜接 RS 的複雜度。

  • **動態執行緒管理(Runtime thread-launch management):**透過現有的計算方案,我們可以針對特地的硬體去做調校,讓程式在該硬體跑到最快,測試與調校當然不是壞事,假設時間沒有限制的話,也的確可以為每個硬體調校到最佳的設定。但假設有某些硬體根本就沒公開,或是開發者沒有的,就不是這回事了,程式可能就會因此沒辦法執行,所以我們決定還是以 Portability 為主,不讓這些調校的事情讓開發者操心,而是在執行的時候動態去做調校,雖然這樣可能會有一些 Overhead ,但是最終的平均效能還是遠高於沒有調校過的。除了 Portability 的好處之外,我們還可以動態的決定到底要在哪邊執行 RS script,例如說,如果有些硬體可以支援指標或是遞迴,而其他的硬體不行。我們當然也可以禁止開發者在低階硬體上面執行高階功能的 API ,並且要求他們使用比較低階但是通用的 API ,但是這產生了浪費:程式沒辦法發揮到硬體的最大效能。所以我們決定在執行的時候做這分析動作,所以開發者就可以專注在程式開發本身,而硬體製造商也可以因為他們硬體上功能的優越而更具有競爭有事(因為所有硬體的功能只要可以用都會被用到),而且就算哪天硬體多了新特色,應用程式不需要修改就可以使用到該項特色。