知名獨立遊戲開發者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?
什麼是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年後情況改變了也不一定。
Rust:
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==無價。
所謂的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而生的一種配套。
依照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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//C++11 lets me initailize structs nicely. | |
//Imagine we start with C augmented thus. | |
struct Party_Member{ | |
char * character_name = NULL; | |
char * class_name = NULL; | |
int health_max = 4; | |
int member_flags = 0; | |
int experience = 0; | |
int current_level =1; | |
}; |
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表示"釋放已釋放記憶體"這種狀況。
這樣做的問題是:沒有Destructor,其中character_name跟class_name字串可能是allocate在heap上的,沒有destructor如何回收記憶體?
那麼這樣如何?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//C++11 lets me initailize structs nicely. | |
//Imagine we start with C augmented thus. | |
struct Party_Member{ | |
char *! character_name = NULL; | |
char *! class_name = NULL; | |
int health_max = 4; | |
int member_flags = 0; | |
int experience = 0; | |
int current_level =1; | |
}; |
當然,這樣做可能遭遇"釋放已釋放記憶體"的bug。試想若有另一個該struct的copy,那當copy destruct的時候不就造成這種bug了嗎?
是的,的確是這樣,也有可能造成dereference已經釋放的記憶體區塊的問題,但是這些問題都可以藉由在程式語言spec中,規範compiler及Debugger必須在compile time或run time回報錯誤及錯誤發生的位置與行數,讓程序員自行解決。目前是有些3rd party工具在做類似的事,但是這些工具大都效能低落,而且因為compiler不知道memory ownership,無法標出造成錯誤的那段程式碼在哪裡,他們只能告訴你0xfeeefeee表示"釋放已釋放記憶體"這種狀況。
在Mesh class中也可以這樣用:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct Mesh{ | |
Vector3 *! position; | |
int *! indices; | |
}; | |
struct MeshC++{ | |
std::unique_ptr<Vector3> *position; | |
std::unique_ptr<int> *indices; | |
}; |
此時,C++粉絲冒出來了:"嘿嘿,這傢伙果然是個蠢蛋,unique_ptr不就是你要的嗎?"
確實unique_ptr解決這個問題,但是template compile很慢,錯誤訊息很難以理解。且站在程式碼美學的觀點來看,許多人會認為unique_ptr有些冗長,更糟的是程式碼的抽象概念改變了: 當我們宣告Vector3 *!時我們所要表達的重點是"Vector3",unique_ptr *他的type卻是"unique_ptr",Vector3這個"參數"似乎變成可有可無的東西。唯一性應該是type的一種參數,而非type是唯一性的一個參數,後者是一種奇怪的概念。
確實unique_ptr解決這個問題,但是template compile很慢,錯誤訊息很難以理解。且站在程式碼美學的觀點來看,許多人會認為unique_ptr有些冗長,更糟的是程式碼的抽象概念改變了: 當我們宣告Vector3 *!時我們所要表達的重點是"Vector3",unique_ptr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//C-Style | |
struct Mesh{ | |
Vector3 position[]; | |
int indices[]; | |
}; | |
//We propose | |
struct Mesh{ | |
Vector3 [] position; //range-check in debug-time | |
int [] indices; | |
}; | |
struct Mesh{ | |
Vector3 []! position; //range-check and has owenership | |
int []! indices; | |
}; |
接下來這邊講range-checked array,而且在語法上與指標保持一致性,而非像C一樣放在後面。當然這種construct也需支援"!"。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct Mesh { | |
void *memory_block =NULL; | |
Vector3 *positions =NULL; | |
int indices = NULL; | |
Vector2 *uvs = NULL; | |
int num_indices =C; | |
int num_vertices =0; | |
Mesh::~Mesh() {delete [] memory_block;} | |
}; | |
int positions_size = num_vertices * sizeof(positions[0]); | |
int indices_size = num_vertices * sizeof(indices[0]); | |
int uvs_size =num_vertices * sizeof(uvs[0]); | |
mesh->memory_block =new char[positions_size+ indices_size + uvs_size]; | |
mesh->positions =(Vector3 *)mesh->memory_block; | |
mesh->indices =(int *)(mesh->memory_block + positions_size); | |
mesh->uvs =(Vector2 *)(mesh->memory_block + positions_size + indices_size); |
前一個Struct Mesh不太現實,通常game programmer會像上面這樣做。因為Heap allocation很慢很貴,為了避免fragmentation與增加效能通常我們會將一起用到的東西allocate在同一塊連續的記憶體裡。這種寫法太常見了,但是老實說寫起來相當冗長麻煩且易錯。如果語言能提供一種設計讓人標示joint allocation,會非常方便,如下:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct Mesh { | |
Vector3 []! positions; | |
int []! indices; @joint positions | |
Vector2 []! uvs; @joint positions | |
}; | |
mesh.positions.reserve(positions: num_positions, | |
indices: num_indices, | |
uvs:num_uvs); | |
mesh.positions.reserve(num_vertices); | |
mesh.indices.reserve(num_indices); | |
mesh.uvs.reserve(num_vertices); |
上面"@joint"這種markUp就是用來描述這樣的情形,如此一來compiler就能知道positions,indices與uvs必須allocate連在一起。如果compiler夠聰明的話,或許也能幫我們檢查
在保留記憶體空間時是否有一起保留,如最後三行code,如果其中一行沒有執行的話,compile time時就直接報錯。
以上這些Idea是比較主要的幾點,有些可能有點dumb或是trivial,但沒關係,這個talk目的本來就只是開啟後續的討論空間,有些觀點也只是個人經驗,並不是叫大家都相信他。
後面幾點是比較瑣碎的東西,有幾點在Talk #2 /#3有較詳細的討論,這邊我就簡單紀錄如下,詳細討論就留待後續文章。
程式語言的語法設計必須要考慮到編輯的方便性,我們在寫程式的時候是常常copy-n-paste的,如果語法沒有一致性,copy-n-paste時就要改很多東西。如C++裡的->跟'.',當變數是object時你會用後者,此時若copy那段code到另一個function裡若是參數改成傳指標的話,該段code的'.'就必須改成->了,當然C++中我們可以用reference,但是為了兩個本質上相同的東西引進兩種不同概念是不必要的。據說Go在這點上做得很好。
另外C++中function pointer雛形宣告方法太多樣,缺乏一致性,lambda的語法跟一般函數宣告也不相同,這些都造成refactor時的阻力。
這邊在Talk #2中著墨許多,若有興趣請看後面文章。
在保留記憶體空間時是否有一起保留,如最後三行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.)
(To be continued in next article.)
沒有留言:
張貼留言