2014年11月11日 星期二

Jonathan Blow's Ideas About Programming Language雜記(1)





知名獨立遊戲開發者Jonathan Blow近日在twitch.tv上發表了一些有關"為了遊戲製作而生的程式語言"的想法,從9月中開始一共有3個talk,在第三個talk中還給出了他的prototype demo。而後來他還實況live coding了一些prototype feature,令我相當期待日後的發展。
而目前的3個talk,個人認為相當具有可看性,茲以此系列文筆記之。


Talk #1

Motivation

很多人將Jonathan Blow視為Game Designer,但實際上呢,他是Programmer出身,而就算他現在成立了一間遊戲公司,需要做許多商務工作,程式設計依然是他每天主要活動。
而他目前跟許多人一樣,每天最常使用的程式語言是C++,而C++依然是遊戲業界最普遍的語言之一。現在雖然有許多新的程式語言,但是大都缺乏C++的低階與高效能特性,例如指標與記憶體的直接操作,且C++的新規格C++0x/11/14等為C++加入了許多高階語言才有的功能,這些都讓C++在業界中保持良好的動能。

然而,Jon認為C++目前的複雜度已經高到難以接受的程度了,而且以他使用C++0X/14新特性的經驗,發現這些新特性要不是效能有問題,就是使用方式上跟過去的C++ feature有許多不一致的地方,這些都造成Game Programmer日常生活巨大的痛苦,現在是跳離C++這條大船的時候了。

因此,Jon開始了這一系列的Talk,他的目的不是要宣布一個新程式語言開發計畫,而是要開啟遊戲業界對這個問題的關注,並討論可能的改善方法。

Not a Big Agenda Language

Jon認為如果我們要轉換到另一種語言的話,它不能是一個Big Agenda Language。
什麼是Big Agenda?

"Functional EVERYWHERE." (Haskell中槍)
"Buffer overrun should be IMPOSSIBLE." (Rust?)
"EVERYTHING is an object." (Java,C#...)

如果程式語言採用極端且強迫的設計哲學,那有很高的機率會不夠實用,因為現實的世界充滿著各種意外與不一致性,若要強迫使用某一種思維方式去解決所有的問題,必定有極大的困難。

當然這些agenda基本上是好的,但是當我們回顧過去的歷史會發現許多agenda的實用性並未被證明,而在某些領域某些agenda可能永遠不適用。

Alternatives

現在又新又潮的程式語言百百種,我們又何必重新造輪子呢?
Jon認為目前有3個語言有可能是我們的替代方案: Go,D,Rust.但是這三種語言各自都有(對遊戲來說)不適用的理由。

Go:
Jon覺得Go作對了很多事情。但是因為
1.有Garbage Collection
對需要高效能的遊戲或遊戲引擎來說,GC是完全無法容忍的。或許現在有Unity這種東西,很多人也用它做出很多賺錢的遊戲,但是這些遊戲的Asset  loading通常不高,這就不在Jon所想討論的情形之中。
2.Concurrency Model對遊戲程式來說不適用。
Jon認為Go的Concurrency為了safety作了太多限制,而遊戲程式不需要限制那麼多。

D:
1.GC is optional. Good!
2.Buys too many into C++'s idea.
因為它的走向太像C++了,這是Jon所想避免的。
而目前如果要轉換到D的話,Jon認為並不值得,因為必須porting的部分太多,弊大於利。不過或許10年後情況改變了也不一定。

1.No GC.
2.但是它為了Safety作出過多限制,例如unsafe code必須包含在Unsafe block中,遊戲的Code到處充滿了side effect,如果每次寫之前都要考慮哪些safe哪些unsafe,把unsafe block用unsafe{}包起來,那對遊戲程式設計來說實在太痛苦。

所以上述三種程式語言通通出局XD

Feasibility

等等,但是造一個全新的程式語言,不會太工程浩大了嗎?

It's not that hard.

遊戲業界每年都會推出數款AAA級遊戲,這些遊戲程式每個都是不可思議的複雜,
Jon認為這些遊戲可能都比compiler複雜,如果我們把同等精力與資源投入改善工具,可以大大的改善整個業界的生產力。

更何況現在有LLVM這種東西。要知道做一個新的語言難處不是在Parsing或是Code gen,而是Optimization,而這部分LLVM可以幫助我們。

另外這是一個態度問題,想想當幾年前沒人看好電力車的情況下,Elon Musk都可以在美國建好他的電力車充電網,並讓Tesla公司營運上軌道,別說任何事情是不可能的。這個世界總是需要有人去push the envelope.

How Much Could We Gain?

Jon認為以我們現有的科技應該至少可以提升10% ~20%(相對於C的效能或生產力)
看起來好像不多,但是加上The Joy of Programming==無價。
所謂的The Joy of Programming是說:這個語言不會造成Coding上的摩擦力,你不會覺得要做什麼東西都綁手綁腳的,你會很容易進入FLOW狀態(零的領域?),如果這是一個你每天都要用的東西的話,那這點的價值是極大的。

Primary Goal

這個新的程式語言目標是:
Friction reduction
The joy of programming
Performance
Simplicity
Designed for good programmer

前兩項如前一段所述,總之就是這個程式語言必須讓程序員容易揮灑。而且有同等於C等級的機器效能或者人工效能(應該是指類似生產力之類的東西)。
另外這個語言必須要夠簡潔,這邊Jon拋出一個曲線圖來表示生產力隨著程式的複雜程度而下降。
再來,現在很多高階程式語言都是為平均或平均水準以下的programmer設計的,當你有這樣的想法的時候,整個設計哲學就會傾向讓average programmer減少犯錯而設下很多限制,這些限制可能反而造成高手揮灑困難。這就像如果你讓Chuck Yeager來當你的駕駛員你不會希望他去開客機吧?Yeager就是要開實驗戰鬥機阿!

最後則呼應了前面的"not a big agenda language",Jon認為實務上,程式語言只需要提供85% solution而不是100% solution。

RAII

接著,Jon發表了一些對C++裡的RAII特性的"個人意見":

RAII是什麼呢?原文是Resource Acquisition Is Initialization.
是一種由C++發展出來而後被用在許多OO程式語言裡的programming paradigm。
依照RAII原則,物件所持有的資源(resource)跟此物件的life-time同生共死。因此,C++裡有Constructor/Destructor,當物件創建時便在constructor中將其相關資源(例如Mesh file、Texture map等)一併初始化,當物件生命週期結束時便在Destructor中釋放其占據的資源。只要每個物件都遵循RAII原則,那對stack上的物件就不會發生Memory leak,更重要的是,C++中有Exception這種功能,如果物件都遵循RAII原則,那就算Exception發生,也不會發生Memory leak。當初RAII的發展就是為了Exception handling而生的一種配套。

這裡還有個問題:"什麼是資源(Resource)?"
一般來說,我們會認為像Memory,File handle,Texture map...等可以稱之為"資源"。但是仔細想想,這些東西本質上是相當不一樣的,在遊戲中我們對三者的操作方式通常是截然不同的,我們會對memory作new,delete,pooling,對File handle讀取檔案內容,parsing,然後關掉file。對Texture map呼叫OPENGL/D3D去改變render state。而RAII試圖強迫我們以同一種抽象去處理這些不一樣的"資源",帶來的效益並不大。Jon說在整個遊戲的程式碼中處理File或是texture map的code可能只占了1%,那就算沒有RAII,他也不覺得會怎樣。

但是呢,上面提到的三種資源中,有一個跟其他是非常不一樣的:"memory"。
甚至可以說我們程式的本質就是在操縱memory,操縱記憶體是我們90%的時間在做的事。所以,一個好的程式語言最好提供方便我們操縱記憶體的設計,而RAII不但沒有讓記憶體操作更方便,還綁手綁腳的規定我們必須要有constructer/destructor/copy constructer/move constructer/最好還來個"==" operator...etc.

RAII不過是為了解決Exceptionn所造成的問題,這邊Jon的用詞有點重:

Exception is silly.

他認為Exception是C++的feature中最複雜的功能之一。Exception破壞了程式的循序結構,在一段程式碼中,隨時都有可能某個物件拋出某個exception,造成程式skip掉下面整塊程式碼而跳到exception handling的Code,就像舊時代的Goto一樣危險且令程式難以理解。

現在C++社群中認為Exception是有害的並不在少數,
如D語言的發展者之一:
在這個Talk之中說明了大部分C++ Exception所產生的問題,並提出解決方案,雖然Jon認為其解決方案太Overkill。再者,Exception是程式語言發展的包袱,每當你提出一個新Feature,就會有人說:"這樣不行啦,會有Exception。" 程式語言的發展若考慮太多這些並無實質效益的東西,得不償失。而且對Programmer而言,這會造成巨大的阻力,對遊戲開發者來說,我們常常會需要快速實驗一些構想,如果我們連簡單的循序操作都需要把procedure包裝成物件、寫好Constructer,Destructor,copy constructer,考慮exception會如何發生...etc. 人的working memory實在有限,每次都要考慮code在哪種非local情形會跳到哪個block也實在太痛苦。

不是說exception完全不好,而是為了exception我們犧牲了太多。
像Go在這方面就做的不錯,在Go中可以有multiple return value,error code處理比起exception來簡單許多。

Helping with Memory

前面提到了好的程式語言最好提供方便記憶體操作的設計,這段Jon提出了幾種方式,這些方式並未經過驗證,只是Jon覺得可以朝這些方向去思考,大家可以一起來討論激盪出新的火花。

1.Memory ownership notation
首先提出的是以"!"符號代表memory ownership.請看以下幾段Code:

Jon說C++11之後允許這樣的初始方式,不使用Constructor,不需要header file,不會暴露type資訊到其他檔案,也減少了cross referencing,相當簡單好用。
這樣做的問題是:沒有Destructor,其中character_name跟class_name字串可能是allocate在heap上的,沒有destructor如何回收記憶體?

那麼這樣如何?
在char*後面加上"!"(用哪種符號並不重要),用來表示這個變數擁有這塊記憶體,如果這個變數生命週期結束或被呼叫了delete,就回收這塊空間。如此一來,連destructor都不用寫,簡單的syntax construct就能替代destructor的功用。

當然,這樣做可能遭遇"釋放已釋放記憶體"的bug。試想若有另一個該struct的copy,那當copy destruct的時候不就造成這種bug了嗎?
是的,的確是這樣,也有可能造成dereference已經釋放的記憶體區塊的問題,但是這些問題都可以藉由在程式語言spec中,規範compiler及Debugger必須在compile time或run time回報錯誤及錯誤發生的位置與行數,讓程序員自行解決。目前是有些3rd party工具在做類似的事,但是這些工具大都效能低落,而且因為compiler不知道memory ownership,無法標出造成錯誤的那段程式碼在哪裡,他們只能告訴你0xfeeefeee表示"釋放已釋放記憶體"這種狀況。

在Mesh class中也可以這樣用:



此時,C++粉絲冒出來了:"嘿嘿,這傢伙果然是個蠢蛋,unique_ptr不就是你要的嗎?"
確實unique_ptr解決這個問題,但是template compile很慢,錯誤訊息很難以理解。且站在程式碼美學的觀點來看,許多人會認為unique_ptr有些冗長,更糟的是程式碼的抽象概念改變了: 當我們宣告Vector3 *!時我們所要表達的重點是"Vector3",unique_ptr *他的type卻是"unique_ptr",Vector3這個"參數"似乎變成可有可無的東西。唯一性應該是type的一種參數,而非type是唯一性的一個參數,後者是一種奇怪的概念。


接下來這邊講range-checked array,而且在語法上與指標保持一致性,而非像C一樣放在後面。當然這種construct也需支援"!"。

前一個Struct Mesh不太現實,通常game programmer會像上面這樣做。因為Heap allocation很慢很貴,為了避免fragmentation與增加效能通常我們會將一起用到的東西allocate在同一塊連續的記憶體裡。這種寫法太常見了,但是老實說寫起來相當冗長麻煩且易錯。如果語言能提供一種設計讓人標示joint allocation,會非常方便,如下:
上面"@joint"這種markUp就是用來描述這樣的情形,如此一來compiler就能知道positions,indices與uvs必須allocate連在一起。如果compiler夠聰明的話,或許也能幫我們檢查
在保留記憶體空間時是否有一起保留,如最後三行code,如果其中一行沒有執行的話,compile time時就直接報錯。

以上這些Idea是比較主要的幾點,有些可能有點dumb或是trivial,但沒關係,這個talk目的本來就只是開啟後續的討論空間,有些觀點也只是個人經驗,並不是叫大家都相信他。

後面幾點是比較瑣碎的東西,有幾點在Talk #2 /#3有較詳細的討論,這邊我就簡單紀錄如下,詳細討論就留待後續文章。

No God Damn Header Files

Forward declaration,include guard,pragma once...這些東西最好不要出現。


Refactorability

程式語言的語法設計必須要考慮到編輯的方便性,我們在寫程式的時候是常常copy-n-paste的,如果語法沒有一致性,copy-n-paste時就要改很多東西。如C++裡的->跟'.',當變數是object時你會用後者,此時若copy那段code到另一個function裡若是參數改成傳指標的話,該段code的'.'就必須改成->了,當然C++中我們可以用reference,但是為了兩個本質上相同的東西引進兩種不同概念是不必要的。據說Go在這點上做得很好。

另外C++中function pointer雛形宣告方法太多樣,缺乏一致性,lambda的語法跟一般函數宣告也不相同,這些都造成refactor時的阻力。

這邊在Talk #2中著墨許多,若有興趣請看後面文章。

Concurrency

這邊講到C++ lambda的[] capture對concurrency造成的效應。Jon說這邊他還沒有想很多,實際上在Talk #2後段是講到蠻多的,所以就同前段最後一句。

The program specifies how to build it.

這個特色呢則是Jon認為非常重要而且可改進的地方。
一個程式必須說明如何Build自己。語言的spec規定compiler必須做完所有的事(compiling,linking...),如此一來你在不同的OS平台都只需要一個compiler跟你的source code,而不需要像現在在Windows上用VC,在Linux上用gcc/g++/clang,也不需要像CMAKE這種cross-build工具的存在。語言的spec必須解決這些問題。

這點在Talk #3 Demo中有相當有趣且強大的展示,有興趣者可以參考影片。

No implicit type conversions
Named argument passing
Less mark-up
Permissive License
Optional Type
No preprocessor or better prepocessor

(以上及Q&A省略)
(To be continued in next article.)

沒有留言: