最近業界有很多技術和產品都被認為屬於內存計算的範疇,並且大家都覺得內存計算是未來大數據方面的核心技術,特別是類似Spark和HANA這樣的產品和技術湧現,使得內存計算已經在大數據技術方面成為主流。但身為一個在內存計算方面研究3年左右的筆者,感覺很多人對於內存計算的理解僅停留在把數據緩存在內存中,或者使用最新的SIMD指令集,但筆者在這三年的研發過程中的最大發現並不是這樣,我個人覺得內存計算引擎相對於傳統數據處理引擎,最大的革新是基於LLVM編譯器的動態代碼生成技術,所以本文將給大家介紹現在的產品和技術是如何使用LLVM編譯器來動態生成執行代碼的,從而實現真正意義上的內存計算,那麼在深入這個技術之前,想給大家稍微介紹一下LLVM的技術本身。
LLVM-移動時代的幕後英雄
LLVM是Low Level Virtual Machine的簡稱,首先,大家不要被這個名字所誤導,認為它是類似VMware和KVM這樣的虛擬機項目,其實它是一個類似GCC的大型編譯器框架。而LLVM這個名字本身隨著這個項目不斷發展,已經無法完全代表這個項目了,只是作為這種叫法一直延續下來。
作為編譯器,LLVM本身和GCC一樣,是一個開源的項目,協議是BSD,而不是Linux和GCC所用的GPL協議。它最早的時候是伊利諾伊大學厄巴納香檳分校的一個研究項目,主要負責人是大牛Chris Lattner,並且項目本身也很榮幸地獲得2012年ACM軟體系統獎。
LLVM可以作為多種語言的後端,它可以提供可程式語言無關的優化和針對很多種CPU的代碼生成功能,與GCC相比,它的整體架構更清晰,速度更快,此外,LLVM已經不僅僅是個編程框架,它目前還包含了很多子項目,比如最具盛名的用於C編譯的Clang,除此之外,還有用於連結GCC前端解析器的Dragonegg等項目。
雖然LLVM對於很多程式設計師而言是一個陌生的名詞,但是在整個移動時代起到了至關重要的作用,無論是Android還是iOS平臺。
對於iOS平臺而言,Chris Latter現在就職於Apple,而且Apple目前也是LLVM主要贊助者之一。在iOS之前,LLVM的誕生就給OSX本身的開發帶來極大的幫助,因為當時Apple困於GCC無法提供給他們完善的支持,特別是和Object-C相關的編譯部分,而當時LLVM項目的出現幫助他們解決燃眉之急,包括後面Grand Dispatch這樣的核心功能都有LLVM的影子。到了iOS時代,LLVM已經是整個iOS平臺最底層的核心,無論從iOS系統本身的編譯,以及XCode的調試支持,還是最新的語言Swift也都是由LLVM之父Chris Latter本人設計的。
儘管Android平臺是iOS平臺的對手,但Android平臺對LLVM依賴更盛,雖然4.4之前都是用自建的Dalvik VM,使用類似JIT(Just-in-time Compiler)這樣即時編譯的形式來執行,但是在性能和用戶體驗方面被大家所詬病,所以在Android的4.4版本推出了ART模式,ART是直接使用LLVM去做AOT(Ahead of Time)編譯,也就是在安裝的時候直接將程序編譯為機器碼,平時Android用戶使用App的時候能像iOS用戶一樣直接執行本地代碼,這樣使Android的整體性能已經接近iOS的水準,雖然在安裝的時候,可能需要多花點時間用於做預編譯。所以LLVM可謂是移動時代的幕後英雄,那麼它和傳統的數據處理有什麼關係呢?在深入介紹LLVM是如何改變傳統數據處理引擎之前,讓我們先了解一下傳統數據處理引擎的短板。
傳統數據處理引擎的短板
眾所周知,最初傳統數據處理引擎(比如Postgres,Oracle等)的主要瓶頸在於I/O,因為當時底層硬碟的速度實在乏善可陳,多年沒有進步,但是隨著近幾年多節點並行,SSD,大內存等I/O資源的發展,更重要的是列存技術的普及和進化,使得I/O問題已經不再是一個極大的瓶頸,但隨之而來的就是在網絡和CPU方面的瓶頸反而顯現出來,由於網絡瓶頸不是本文關注的重點,所以主要聊聊數據處理引擎CPU方面的瓶頸。
圖1 繁瑣的數據處理引擎代碼
雖然新一代基於內存的計算引擎,使得I/O性能比傳統基於硬碟有10倍左右的性能提升,但與之同時,CPU的瓶頸會更明顯,基於我們之前測試結果,以傳統Postgres的引擎為例,操作數據都被緩存到內存的Page Cache上面,它做最簡單的Count(*)統計都只能勉強達到每秒鐘400萬行左右,而真實所需要的操作其實是很少的,按照2010年的X86 CPU而言,其實處理這些計算是非常簡單的,但實際性能還是很低,為什麼呢?在這裡,先和大家聊聊現在X86 CPU性能特徵,隨著技術本身的發展,X86 CPU本身的處理能力非常強大,但是一切換Context就會出現性能方面的小滑坡。如果等待處理的命令和數據沒有被預緩存在Cache上面,而是需要訪問的內存的話,會有一個更大的性能滑坡。既然大家都了解CPU本身的瓶頸,下面來聊聊傳統資料庫處理引引擎的短板有哪些?主要有以下四點:
其一是條件邏輯冗餘,數據處理引擎代碼非常繁瑣,因為SQL語句本身非常複雜,所以數據引擎為了支持那些複雜的SQL語句,使得數據處理引擎需要複雜的條件邏輯來處理,就像圖1那樣,甚至一個Switch循環裡面會有成百上千的case這樣的選擇邏輯,雖然Switch循環本身會被編譯器進行一定程度的優化,但是最終機器碼中的分支指令會一定程度上阻止指令的管道化(instruction pipelining)和並行執行(instruction-level parallelism)。
其二是虛函數的調用,和第一個問題的原因類似,因為數據處理引擎要支持極為複雜的SQL語句,還有十幾種的數據類型,比如,程序在處理add這個邏輯的時候,此時數據處理引擎需要根據來源數據是INT還是BIGINT來選擇不同的函數來處理,所以在實際處理時,肯定只能用虛函數來轉給具體的執行函數,這個對CPU的影響肯定是非常明顯的,因為很多時候虛函數調用本身的運行成本,比這個函數本身執行成本更高。更因為如此,內聯函數這個最常見的性能優化方式也無法被使用。
其三是需要不斷地從內存中調用數據,而無法一次性將數據從內存加載至Cache上,比如,常見的For循環,雖然知道下一個數據就在下一個偏移地址,但還是要從內存上面讀取,這樣讀取開銷很大而且阻止整個CPU管道化的操作。
其四是因為不同x86 CPU年代不同,所以支持不同擴展指令集,而這些新的指令集對很多操作都能提升100%以上的性能,比如,有些比較新的CPU支持SSE 4.2,而有些卻不支持,為了保證數據引擎能跨不同的硬體平臺,所以數據引擎很少支持一些擴展的指令集,這導致浪費了本來可以提升的性能沒有得到支持。
雖然這些瓶頸很難克服,但Google研發的Tenzing技術裡面提出基於LLVM編譯框架實現動態生成代碼Codegen這個技術,並且通過這個技術基於MapReduce分布式框架下面的類SQL系統的性能也能接近商業收費並行資料庫的水準。Codegen這種方式,就是在SQL執行前才編譯具體的執行代碼,之前的數據引擎本身是一個大而複雜的框架,任何計算包括1+1,都要進入這個龐大的框架裡面做判斷來選擇合適的函數來執行1+1,所以最終的成本就像前面列的那樣,是非常高的。那麼使用Codegen的好處如下:
其一是簡化了條件分支,因為在生成代碼的時候,程序已經獲知運行時的信息,通過展開for循環(因為我們已經知道循環次數)和解析數據類型,所以可以像圖2那樣if/switch這些分支指令這樣的語句就能優化掉,這是非常簡單有效的。
圖2 if/switch分支指令語句
其二是內存加載,我們可以使用代碼生成來替代數據加載,這樣極大減少內存的讀取,增加CPU Cache的利用率,這對CPU性能而言非常有幫助。
圖3 用代碼生成取代虛函數的調用
其三是內聯虛函數的調用,因此當對象實例的類型在運行時可知,我們可以如圖3所示使用代碼生成來取代虛函數的調用,並做內聯化,這樣表達式可以無需函數調用而直接求值。此外,內聯後的函數使編譯器做進一步的優化,例如子表達式消除等。
其四是能利用最新的指令集,在Codegen的時候,由於Codegen本身是在即將執行的那個節點執行,所以它很方便就能感知到其底層CPU到底支持那個版本最新的指令集,比如是SSE 4.2還是SSE4.1,所以Codegen完全會根據具體的指令集支持來編譯具體的執行代碼,使其能儘可能地利用最新的指令集。
圖4 基於TPCH-Q1的基準測試
基於圖4的測試結果(跑TPCH-Q1,數據量2.7GB,單節點,Codegen時間150ms),通過這樣的Codegen方式的確能有效地提升數據處理引擎利用CPU的效率,整體性能提升達到3倍以上,並且Cache Miss率和之前比重大提升。當然Codegen技術就像其他技術一樣,它也有自己的成本和不足,那就是在Codegen本身的代碼生成操作也是需要一定的時間,包括For循環展開和多層次pass優化,雖然LLVM本身的處理性能強,但是這樣操作還是很耗時,一般都要100-200ms左右,但原本一個10秒鐘的查詢卻因次提升到了6秒,我相信大家都能理解。
基於LLVM的動態代碼生成技術
業界已經有越來越多的內存計算產品採用基於LLVM的動態代碼生成技術,已經有成為內存計算標配的趨勢,除了上面提到在Google內部大範圍使用的Tenzing外,還有Cloudera Impala和SAP HANA也在使用Codegen技術,另外內存領域當紅的Spark,也計劃在接下來的版本中實現Codegen。
Cloudera Impala的實現方式
雖然Cloudera Impala在業界毀譽參半,貌似現在在Cloudera內部也準備放棄Impala,改方向往Spark發展,但是Impala在性能方面還是很受到大家的認可,主要原因就是其使用基於LLVM的動態代碼生成技術。為什麼採用Codegen?除了性能好之外,另一個因素就是Impala的首席設計師本身就是Google Tenzing的設計師。
那麼讓我們看看Cloudera Impala的具體實現方式。由於在運行時候將執行邏輯代碼動態生成機器代碼,成本會很高,所以Impala在編譯的時候,會先將具體的一些執行函數先編譯成LLVM的IR(Intermediate Representation)這個中間形態,從而生成一個大的IR文件,裡面包括所有相關函數的IR代碼塊,之後在具體SQL命令執行開始之前,數據節點DataNode會先根據具體的SQL語句,從根節點開始遞歸初始化執行樹(AST),之後開始LLVM會根據執行樹來調用那個IR文件裡面對應函數的IR代碼塊來生成本地可執行代碼,接著會繼續對生成好的代碼進行優化來進一步提升性能。
SAP HANA的設計思路
其實至少十年前就有一波內存計算的風潮,那時企業級產品中,具代表性的主要有用於OLTP事務加速的Timeten和Altibase,而2010年開始推廣的那些內存計算技術產品中,最有代表性的莫過於SAP的HANA。
SAP HANA在處理邏輯上,全面採用向量計算(Vector Processing)的理念,儘可能地使用最新的SSE4.1和SSE4.2等指令集,還有就是在多核NUMA場景下降低運行消耗,使其多線程性能提升比儘可能地接近1。另外,在數據結構方面,為了儘可能地利用CPU緩存(Cache),並儘可能減少訪問內存,所以推出了緩存敏感的CSB(Cache Sensitive B+)樹來代替傳統的B樹。
為了進步提升,並且完善整體架構和提升性能,HANA也使用了LLVM支持動態編譯,無論是SQL查詢還是MDX(Multi-Dimensional Expressions,多維表達式)查詢等,在HANA內部都會都被轉譯一個公共的表示層,名為L語言,並且在執行之前,會使用LLVM編譯為二進位代碼並直接執行,這樣做的好處是避免傳統資料庫引擎裡面繁瑣邏輯,從而提升性能,並且這樣L語言的形式,對於HANA今後添加更多的功能非常有幫助,所以HANA在很短的時間內支持很多SQL分析語句之後,還支持各種數據挖掘的算法,還提供很多定製化腳本,讓用戶根據自己的業務需求來擴展HANA的功能。
Apache Spark的設計思路
大家都知道,現在Apache Spark可以說是最近最火的開源大數據項目,就連EMC旗下專門做大數據Pivotal公司也開始準備拋棄其自研十幾年GreenPlum技術,轉而投入到Spark技術研發當中。並且從整個業界來看,Spark火的程度也只有IaaS界的OpenStack能相提並論。那麼我們接著就直接切入它的核心機制吧。
Spark已經將代碼生成用於SQL和DataFrames裡的表達式求值(expression evaluation),同時他們正準備努力讓代碼生成可以應用到所有的內置表達式上,並且在這個基礎上支持更多新指令集。還有他們不僅通過Codegen內部組件來提升CPU效率,還期望將代碼生成推到更廣泛的地方,比如,數據的序列化,也就是Shuffle數據再分布的過程中,將數據從內存二進位格式轉換到wire-protocol這樣流格式的過程,因為Shuffle通常會因數據系列化而出現性能瓶頸,而通過代碼生成,可以顯著地提升序列化吞吐量,從而反過來作用到shuffle網絡吞吐量的提升。
圖5 shuffle性能分析
圖5對比了單線程對800萬複雜行做shuffle的性能,分別使用的是之前Kryo方式和最新代碼生成,在速度上後者是前者的2倍以上。
最後,筆者覺得這個技術所帶來在內存計算部分的效應只能說是整個蝴蝶效應最開始的那部分,今後對整個雲計算會帶來更多的變革。你能想像中間件和資料庫合二為一嗎?你能想像所有的業務代碼和基礎設施代碼都動態生成嗎?你能想像當整個百萬臺雲計算集群被合併為一個大電腦的時候,數據和命令都合二為一嗎?而這些就是我認為LLVM給我們帶來的巨大變革,而我們中國人在這場浪潮中應該起著領導的角色。
作者:吳朱華
簡介:上海雲人信息科技有限公司聯合創始人兼CEO,國內資深的雲計算和大數據專家,之前曾在IBM中國研究院參與過多款雲計算產品的開發工作。2010年底,他和另兩位創始人組建了一支十多人的團隊,在上海楊浦雲基地辦公。
本文為CSDN原創文章,未經允許不得轉載,如需轉載請聯繫market#csdn.net(#換成@)