放棄Python擁抱Mojo?鵝廠工程師真實使用感受
# 關注并星標騰訊云開發者
(資料圖)
# 每周1 | 鵝廠工程師帶你審判技術
#第1期|李志瑞:AI 屆新語言 Mojo 要火??
前段時間 Modular 發布了一個新語言 Mojo,這語言不止官網放了巨大的 emoji ?,而且它的標準文件后綴一個是「.mojo」另一個是「.?」,一副立馬要火的樣子呢。
說實話,這個用 emoji 做后綴名的操作其實挺無聊,也有點敗好感,但如果說這個語言能在完全兼容 Python 的基礎上大幅提高執行效率,并且作者是 LLVM 發起人 Chris Lattner,是不是突然又有興趣繼續了解它了呢?
Mojo 被設計為 Python 語言的超集,并增加了許多特性,包括:
?? Progressive types:能利用類型信息獲得更好性能和靜態檢查,但又不強制要求寫類型。
?? Zero cost abstractions:C++ 的核心設計準則,能夠避免用戶為了性能放棄合理設計的代碼。
?? Ownership + borrow checker:Rust 語言的安全性來源,在編譯期避免許多錯誤的發生。
?? The full power of MLIR:原生支持對 MLIR 的直接訪問,能夠從底層擴展系統。
在 Mojo 這個語言的介紹中反復提到 AI,官網也說它是「a new programming language for all AI developers」。那么為什么 AI 開發需要一個新語言呢?首先,我們知道在 AI 屆具有統治地位的語言就是 Python,Python 是一個語法簡單清晰,容易上手,且靈活度很高的語言,深受廣大程序員喜愛,XKCD 上有就這么一幅漫畫:
當然,受人喜愛的語言有很多,Python 成為 AI 屆的統治語言除了本身易用之外,也有慣性的因素。由于 Python 上機器學習相關的庫多,因此機器學習從業者用的就多,這又反過來令新的機器學習相關庫優先為 Python 提供接口,進一步加強了其統治地位。因此,為了逐步滲透這個用戶群,Mojo 兼容 Python 是很正確的一個選擇。Mojo 不僅承諾語法是 Python 的超集,并且它還能直接調用 Python 的庫,這意味著 Mojo 不需要從零開始構建自己的生態,本身就可以用上繁榮的 Python 生態了。
雖然 Python 很好,但它有一個眾所周知的問題,那就是太慢了。而機器學習本身又需要繁重的計算,因此 Python 生態中大量庫的底層其實都是用高性能的語言(如 C/C++)進行實現,然后再提供一個 Python 接口供用戶調用,典型的如 numpy 這種數學庫。在這種情況下,Python 事實上是被作為一個膠水語言來使用,這造成了開發的碎片化,如果一個用戶只是簡單調一下庫那還好說,但一旦到了工業界,開發過程中不可避免地就要涉及一些底層庫的修改,甚至直接換語言來實現同樣的功能以提高性能,這種割裂不止增加了開發成本和精神負擔,而且考慮到眾多擅長 C/C++ 語言的開發者也并不是 AI 領域專家,這種開發人員能力的不適配也對整個 AI 生態的發展形成了一定阻礙。
因此,Mojo 的目的就是要在 Python 生態的基礎上,讓用戶能用一個語言,從使用易用的接口,到開發復雜的庫,再到實現底層黑科技,統一實驗和生產環境所用的語言。為了實現這個目的,Mojo 擴展了 Python 語法,支持了緊湊的內存布局,并引入了一些現代的語言特性(例如 Rust 的安全性檢查),使得這個語言能夠漸進式地在 AI 界立足。說起來 Chris Lattner 在這方面可以算是經驗豐富了,不管是在 gcc/msvc 的統治下實現 clang,還是在 objective-c 的統治下為蘋果實現 swift,都是一個逐步蠶食對手市場的過程。
說了這么多,該來看看 Mojo 長什么樣了。現在 Mojo 還不能直接下載使用,如果想要嘗鮮,需要在官網申請,然后在 playground 頁面中試用,這是一個基于 Jupyter 的頁面,可以混合筆記和可執行的 Mojo 代碼。
前面提到,Mojo 的語法是 Python 的超集,因此 Mojo 的 Hello World 也跟 Python 一樣簡單:
print("Hello World") #> Hello World與 Python 一樣,Mojo 也使用換行符和縮進來定義代碼塊:
fn foo():var x: Int = 1x += 1let y: Int = 1print(x, y) #> 2 1foo()
上面的代碼中使用 var 來聲明變量 x,使用 let 來聲明了不可變量 y。Mojo 像很多較新近的語言一樣,讓不可變量的聲明變得簡單,以鼓勵開發者使用不可變的量。另外注意到這里定義函數使用了 fn 而非 Python 的 def,這是因為 Mojo 希望在兼容 Python 的基礎上加入編譯期的檢查和優化,而 Python 過于動態的語法很難支持這一目標,因此,Mojo 同時支持使用 fn 和 def 兩個關鍵字來聲明函數,對于調用者來說,這兩種方法聲明出來的函數沒有什么區別,但對于實現者來說,可以將 fn 看作「嚴格模式」下的 def,例如下面的代碼會編譯錯誤(如果改成用 def 則不會出錯):
fn foo(): x = 1 print(x) #error:Expression[12]:6:5:useofunknowndeclaration"x","fn"declarationsrequireexplicitvariabledeclarations#x=1#^
雖然官方承諾 Mojo 的語法是 Python 的超集,但目前 Mojo 還在開發中,很多 Python 語法都還不支持,例如目前連 Python 的 class 都無法被編譯通過:
class MyClass:def foo():pass# error: Expression [15]:17:5: classes are not supported yet# class MyClass:# ^
不過,Mojo 現在先提供了另一個用來組織數據的關鍵字 struct,相比于 class,struct 更加靜態可控,便于優化。一方面,struct 支持類似 Python class 風格的函數聲明和運算符重載。而另一方面,struct 又類似于 C++ 的 struct 和 class,內部的成員在內存中緊湊排布,而且不支持在運行時動態添加成員和方法,便于編譯期進行優化,例如:
struct MyIntPair:var first: Intvar second: Intfn __init__(inout self, first: Int, second: Int):self.first = firstself.second = secondfn __lt__(self, rhs: MyIntPair) -> Bool:return self.first < rhs.first or(self.first == rhs.first andself.second < rhs.second)let p1 = MyIntPair(1, 2)let p2 = MyIntPair(2, 1)if p1 < p2: print("p1 < p2") #> p1 < p2雖然有點不同,但整體上看起來還是非常熟悉的對吧。說到這里,有一點需要提醒各位注意,盡管 Mojo 之后會令語法成為 Python 語法的超集,但其語義則有時會和 Python 不同,這意味著 Python 的代碼直接拷到 Mojo 里可能會出現編譯通過但執行結果不同的情況,這里簡單提一個比較常見的例子:函數傳參。在 Python 中,函數傳參的語義類似于 C++ 的傳指針,在函數內部雖然不能更改調用者指向的對象,但可以改變該對象內部的狀態,例如下面的代碼:
def foo(lst):lst[0] = 5print(lst)x = [1, 2, 3]foo(x)print(x)
在 Python 中,這段代碼打印出來的結果是兩次 [5, 2, 3]。但在 Mojo 中,使用 def 定義的函數默認的傳遞邏輯是復制值,也就是說,盡管在函數中能夠修改參數內部的狀態,但修改對于調用方來說是不可見的,因此上面這段代碼在 Mojo 中打印的結果是 [5, 2, 3](foo 內部)和 [1, 2, 3](foo 外部)。
除了語法像 Python,Mojo 非常務實的一點在于它構建于 Python 的生態之上。因此即便 Mojo 還沒能完整支持 Python 的語法,它還是優先支持了對 Python 庫的調用,以便讓開發者能受益于龐大完善的 Python 的生態。例如下面的代碼就使用了 Python 的 numpy 庫:
fromPythonInterfaceimportPythonlet np = Python.import_module("numpy")ar = np.arange(15).reshape(3, 5)print(ar.shape) #> (3, 5)Mojo 作為一個新語言,廣泛吸收許多現代的程序語言設計思想,例如 Rust 的所有權和借用檢查,以此提升代碼的安全性。在 Mojo 中,使用 fn 定義的函數的參數默認傳的是不可變的引用,即「借用」,調用方仍然擁有其所有權,因此在函數內部不可以對參數進行修改。Mojo 提供了一個 borrow 關鍵字來標注這樣的參數傳遞情況,對于 fn 來說是可以省略的,也就是說下面 foo 函數中兩個參數的傳遞方式相同:
fn foo(borrowed a: SomethingBig, b: SomethingBig):a.use()b.use()
在 Rust 中,傳參的默認行為是移動,如果需要借用則需要在傳入時加上 &,這兩種方式倒是沒有太大的優劣之分,Mojo 的行為可能更接近于 Python 這類高級語言的習慣。如果想要修改傳入的參數,則需要手動注明 inout,例如:
fn swap(inout lhs: Int, inout rhs: Int):let tmp = lhslhs = rhsrhs = tmpfn test_swap():var x = 42var y = 12print(x, y) #> 42, 12swap(x, y)print(x, y) #> 12, 42test_swap()
按道理說,Mojo 應該像 Rust 一樣規避一個變量同時被可變和不可變借用,也應該規避同時被可變借用,但目前 Mojo 編譯器似乎還沒實現這一特性,例如下面的代碼還是能編譯通過的:
var x = 42swap(x,x)
從這也可以看出 Mojo 確實還處在比較早期的發展階段。
另一個重要的內存安全概念是對象的所有權,當一個函數獲取了對象的所有權后,調用方就不應該再去使用這個對象了,例如我們實現了一個只支持移動的類型 UniquePtr:
struct UniquePtr:var ptr: Intfn __init__(inout self, ptr: Int):self.ptr = ptrfn __moveinit__(inout self, owned existing: Self):self.ptr = existing.ptrfn __del__(owned self):self.ptr = 0
同時,我們有兩個函數,其中,use_ptr 使用了前面提到的 borrow 關鍵字,借用了 UniquePtr 對象,而 take_ptr 則使用 owned 關鍵字,指明它需要獲取傳入對象的所有權。那么,在調用 take_ptr 的時候,我們就需要在參數后面加上 ^ 后綴,用來表明我們將所有權轉移給 take_ptr:
fn use_ptr(borrowed p: UniquePtr):print(p.ptr)fn take_ptr(owned p: UniquePtr):print(p.ptr)fn test_ownership():let p = UniquePtr(100)use_ptr(p) #> 100take_ptr(p^) #> 100test_ownership()
因此,如果我們將 use_ptr 和 take_ptr 的調用順序調換一下,就會出現編譯錯誤:
fn test_ownership():let p = UniquePtr(100)take_ptr(p^)use_ptr(p) # ERROR!test_ownership()# error: Expression [13]:23:12: use of uninitialized value "p"# use_ptr(p) # ERROR: p is no longer valid here!# ^
Mojo 的另一個強大之處在于它讓對 MLIR>) 的操作變得更簡單。MLIR 全稱是 Multi-Level Intermediate Representation,是一個編譯器開發框架,它存在的目的是通過定義多種方言來逐級將代碼轉換為機器碼,以降低編譯器的開發成本。在 MLIR 之前,一個廣為人熟知的 IR 是 LLVM IR,一個語言的編譯器作者可以通過將自己的語言編譯為 LLVM IR 來接入 LLVM 的工具鏈,使得編譯器作者不需要關心底層具體硬件的差別,實現了對底層編譯工具鏈的復用:
但 LLVM IR 層級過低,難以進行特定于語言本身的優化,從上面的圖中也能看出,各個語言為了實現語言本身的優化,都在編譯為 LLVM IR 之前加入了自己的 IR。另外 LLVM IR 擴展起來也非常困難,難以適應復雜異構計算的要求,而異構計算在 AI 開發中又非常普遍。MLIR 相比于之前的 IR,更加模塊化,僅保留了一個非常小的內核,方便開發者進行擴展。很多編譯器將代碼編譯為 MLIR,而 Mojo 提供了直接訪問 MLIR 的能力,這使得 Mojo 能夠受益于這些工具。更多關于 MLIR 的內容可以參考這一系列文章:編譯器與中間表示: LLVM IR, SPIR-V, 以及 MLIR,這里就不做過多贅述,我們主要關注在 Mojo 中可以如何操作 MLIR。舉例而言,如果我們希望實現一個新的 boolean 類型 OurBool,我們可以這樣實現:
alias OurTrue: OurBool = __mlir_attr.`true`alias OurFalse: OurBool = __mlir_attr.`false`@register_passable("trivial")struct OurBool:var value: __mlir_type.i1fn __init__() -> Self:return OurFalsefn __init__(value: __mlir_type.i1) -> Self:return Self {value: value}fn __bool__(self) -> Bool:return Bool(self.value)這里定義了一個類型為 OurBool 的類型,里面有一個直接使用 MLIR 內置類型 i1 的成員 value 。在 Mojo 中,我們可以通過 __mlir_type.typename 的形式來訪問 MLIR 類型。接著,我們為這個類型提供了兩個構造函數,默認情況下構造為 OurFalse 也可基于傳入的參數進行構建。最下面的 __bool__ 也和 Python 的 __bool__ 一樣,用于使該類型具有和內置 boolean 類型的性質,此時我們可以這樣使用它:
let t: OurBool = OurTrueif t: print("true") #> true除了使用 MLIR 之外,Mojo 甚至可以允許開發者使用 MLIR 實現邏輯,例如下面的代碼中通過應用 MLIR 的 index.casts 操作來實現類型轉換,然后再通過 index.cmp 對值進行比較:
# ...struct OurBool:# ...fn __eq__(self, rhs: OurBool) -> Self:let lhsIndex = __mlir_op.`index.casts`[_type : __mlir_type.index](self.value)let rhsIndex = __mlir_op.`index.casts`[_type : __mlir_type.index](rhs.value)return Self(__mlir_op.`index.cmp`[pred : __mlir_attr.`#index`](lhsIndex, rhsIndex))
基于封裝好的 __eq__ 方法,我們可以很容易實現 __invert__ 方法:
# ...struct OurBool:# ...fn __invert__(self) -> Self:return OurFalse if self == OurTrue else OurTrue
此時,我們就可以對 OurBool 類型的對象使用 ~ 操作符了:
let f = OurFalseif ~f: print("false") #> false通過這個簡單的例子我們可以看出,在 Mojo 中,開發者可以通過訪問 MLIR 來實現和內置類型同等高效的類型。這使得開發者可以在 Mojo 上為新硬件的數據類型封裝高效簡單的 Mojo 接口而不需要切換語言。雖然大部分開發者并不需要接觸 MLIR,但 Mojo 為更深入和更底層的優化提供了充分的可能性。
雖然 Mojo 反復強調它是為 AI 設計的新語言,但以目前 Mojo 的設計方向來看,它的發展前景并不止于 AI。本質上 Mojo 提供了一個能夠兼容 Python 生態的高性能語言,且這個語言可以讓 Python 開發者幾乎無痛地切換過去,那 Python 開發者何樂而不為呢?對于使用 Mojo 的開發者來說,上層業務可以將 Mojo 當 Python 一樣使用,享受到簡明的語法帶來的高開發效率,當出現性能瓶頸的時候,也不用切換語言去進行優化,直接使用 Mojo 重構模塊即可。雖然現在還沒法在生產環境中驗證這個想法,但這個未來聽起來確實非常美好。關于 Mojo 和 Python 開發性能的對比,您可瀏覽 Mojo 發布會上的 Jeremy Howard demo for Mojo launch 視頻。
目前 Mojo 還在比較早期的階段,不僅許多語言特性都還沒實現,而且連本地開發的套件都沒有提供。不過其發展路線和設計思路都非常務實 ,又有一個足夠專業的領導者和公司作為背景支撐,可以說是未來可期,也非常希望這個語言能在其他領域得到更廣泛的應用。
關鍵詞:
免責聲明:本網站內容主要來自原創、合作媒體供稿和第三方自媒體作者投稿,凡在本網站出現的信息,均僅供參考。本網站將盡力確保所提供信息的準確性及可靠性,但不保證有關資料的準確性及可靠性,讀者在使用前請進一步核實,并對任何自主決定的行為負責。本網站對有關資料所引致的錯誤、不確或遺漏,概不負任何法律責任。任何單位或個人認為本網站中的網頁或鏈接內容可能涉嫌侵犯其知識產權或存在不實內容時,應及時向本網站提出書面權利通知或不實情況說明,并提供身份證明、權屬證明及詳細侵權或不實情況證明。本網站在收到上述法律文件后,將會依法盡快聯系相關文章源頭核實,溝通刪除相關內容或斷開相關鏈接。
網絡快報排行榜
-
2023-08-10 08:05
-
2018-09-28 11:32
-
2018-09-28 11:32
-
2018-09-28 11:32
-
2018-09-28 11:32
網絡快報熱門推薦
-
2023-08-10 08:05
-
2018-09-28 11:32
-
2018-09-28 11:32
-
2018-09-28 11:32
-
2018-09-28 11:32