本文是 VMBC / D# 項目 的 系列文章,html
有關 VMBC / D# , 見 《我發起並創立了一個 VMBC 的 子項目 D#》(如下簡稱 《D#》) http://www.javashuo.com/article/p-zziqptgy-s.html 。java
ILBC 系列文章 收錄在 《ILBC 白皮書》 http://www.javashuo.com/article/p-bsuuysoc-bo.html 。算法
ILBC 規範:編程
加載程序集:數組
ILBC 程序集 有 2 種, 安全
1 Byte Code 程序集, 擴展名 爲 .ilb, 表示 「ILBC Byte Code」 。性能優化
2 Native Code 程序集, 擴展名 遵循 操做系統 定義的 動態連接庫 規範, 好比 Windows 上就是 .dll 文件,閉包
Native Code 程序集 就是 操做系統 定義的 動態連接庫 。架構
假設 操做系統 是 Windows, 程序集 名字 是 A, 加載 A 的 過程 是:併發
在 當前目錄 下 先查找 A.ilb, 若存在 則 JIT 編譯 A.ilb 爲 本地代碼 A.dll, 加載 A.dll,
若找不到 A.ilb, 則找 A.dll, 若存在 則 加載 A.dll 。
加載 本地庫 A.dll 的 方式 遵循 操做系統 定義的 動態連接 規範 。
JIT 編譯 A.ilb 爲 本地代碼 並 加載 的 過程 能夠在 內存中 完成, 不必定要 生成 文件 A.dll (若是 技術上 能夠實現 在 內存 中加載的話)。
高級語言(D#) 編譯 的 過程:
高級語言(D#) 編譯 有 2 種方式,
1 AOT, 高級語言(D#) 編譯器 先根據 高級語言(D#) 源代碼 生成 C 語言 中間代碼, 再由 InnerC (InnerC to Byte Code) 編譯爲表達式樹, 再由 InnerC(Byte Code to Native Code) 把 表達式樹 生成爲 Native Code 。 Native Code 是一個 本地庫, 好比 .dll 。
2 JIT , 高級語言(D#) 編譯器 先根據 高級語言(D#) 源代碼 生成 C 語言 中間代碼, 再由 InnerC (InnerC to Byte Code) 編譯爲表達式樹, 把 表達式樹 序列化 獲得 Byte Code, 將 Byte Code 保存爲 ilb 文件 即 獲得 Byte Code 程序集(.ilb) 。
.ilb 在 運行的時候 由 ILBC 運行時 的 InnerC (Byte Code to Native Code) 把 Byte Code 反序列化 爲 表達式樹, 再把 表達式樹 編譯爲 Native Code 。
把 Native Code 程序集 加載到 應用程序 後, ILBC 運行時 會 調用 程序集 的 ILBC_Load() 函數, ILBC_Load() 會 建立一個 ILBC_Assembly 結構體, 並返回這個 結構體 的 指針, ILBC_Assembly 結構體 包含了 程序集 的 元數據 信息, 相似 .Net / C# 中 的 System.Reflection.Assembly 。
元數據 就是 一堆 結構體(Struct), 這些 Struct 及 ILBC_Load() 函數 的 代碼是由 高級語言(D#)編譯器 生成, 代碼以下:
struct ILBC_Assembly
{
ILBC_ClassLoader classLoaderList [ n ] ; // n 是 程序集 中 Class 的 數量, 由 高級語言(D#) 編譯器 在 編譯時 指定
// classLoader 包含了 加載 Class 的 函數 的 函數指針 (保存在 load 字段 裏)
// 每一個 Class 有一個 classLoader,
// classLoaderList 是 保存 classLoader 的 數組,
// 在 ILBC 運行時 加載 Class 時 會調用 classLoader.load 保存的 函數指針 指向 的 函數, 具體內容見下文
// Class 加載完成獲得的 Type 對象 保存在 type 字段 裏
}
struct ILBC_ClassLoader
{
char * className ; // Class 名字
void * load ; // 加載 Class 的 函數 的 函數指針
ILBC_Type * type = 0 ; // 加載 Class 完成後把 Type 對象 保存在這裏
}
struct ILBC_Type
{
char * name ; // Class 名字
int size ; // Class 佔用的 空間大小(字節數)
ILBC_Field fieldList [ n ] ; // n 是 Class 中 Field 的 數量, 由 高級語言(D#) 編譯器 在 編譯時 指定
int fieldCount ; // C 語言數組 的 長度 須要 本身記錄
ILBC_Method methodList [ n ] ; // n 是 Class 中 Method 的 數量, 由 高級語言(D#) 編譯器 在 編譯時 指定
int methodCount ; // C 語言數組 的 長度 須要 本身記錄
}
struct ILBC_Field
{
char name [ n ] = "字段名" ; // n 應和 字段名字符串 的 字節數 相等, n 由 高級語言(D#) 編譯器 在 編譯時 指定
int size; // 字段 佔用的 字節數
int offset; // 字段 相對於 ILBC_Field 結構體 的 首地址 的 偏移量
// ILBC_Type * type ;
char * type ; // type 不能 聲明爲 ILBC_Type 或者 ILBC_Type * 類型, 由於會形成 Type 和 Field 之間的 循環引用,
// 因此先聲明爲 char * (字符串), 保存 Type 的名字, 經過 GetFieldType() 之類 的 方法 來返回 Type 對象,
// Type 對象 就至關於 這裏的 ILBC_Type 或者 ILBC_Type * 。
}
struct ILBC_Method
{
char name [ n ] = "方法名"; // n 應和 方法名字符串 的 字節數 相等, n 由 高級語言(D#) 編譯器 在 編譯時 指定
ILBC_Argument * argList [ n ] ; // n 是 方法 中 參數 的 數量, 由 高級語言(D#) 編譯器 在 編譯時 指定
Type * returnValue ; // 返回值 類型
void * funcPtr ; // Method 對應的 函數指針
}
struct ILBC_Argument
{
char name [ n ] = "參數名"; // n 應和 參數名字符串 的 字節數 相等, n 由 高級語言(D#) 編譯器 在 編譯時 指定
ILBC_Type * type; // 參數類型
}
看到這裏, 是否是跟 C# 反射裏的 AssemblyInfo, Type, FieldInfo, MethodInfo 很像 ?
是的, ILBC 也要支持 完整的 元數據 架構, 元數據 用於 動態連接 和 反射 。
接下來 是 ILBC_Load() 相關的 代碼:
假設 程序集 名字 是 B, 包含了 Person 類 和 Animal 類 2 個 類, Person 類 有 2 個字段 name, age, 有 2 個方法 Sing(0, Smile() ,
void * ILBC_ClassLoaderList_B [ 2 ] ; // 數組長度 2 表示 B 程序集 包含了 2 個 類
ILBC_Assembly * ILBC_Load()
{
ILBC_Assembly * assembly = ILBC_gcNew( sizeof ( ILBC_Assembly ) ) ;
assembly.classLoaderList [ 0 ].className = "Person" ;
assembly.classLoaderList [ 0 ].load = & ILBC_LoadClass_B_Person ;
assembly.classLoaderList [ 1 ].className = "Animal" ;
assembly.classLoaderList [ 1 ].load = & ILBC_LoadClass_B_Animal ;
return assembly ;
}
ILBC_Type * ILBC_LoadClass_B_Person()
{
ILBC_Type * type = ILBC_gcNew ( sizeof ( ILBC_Type ) );
// ILBC_gcNew( ) 是 ILBC 提供的一個 庫函數, 用於 在 堆 裏申請一塊空間, 這裏是在 堆 裏 建立一個 ILBC_Type 結構體
type.name = "Person";
type.size = 8; // Class 佔用的 空間大小(字節數), name 字段是 char * 類型, 假設 指針 是 32 位 地址, 佔用 4 個 字節, age 是 int 類型, 假設是 32 位整數, 佔用 4 個字節, 那麼 Class 的 佔用字節數 就是 4 + 4 = 8, 即 size = 8; , size 是由 編譯器 計算決定的
type.fieldList [ 0 ].name = "name";
type.fieldList [ 0 ].size = // String 是 引用類型, 因此這裏是 引用 的 Size
type.fieldList [ 0 ].type = "String"; // 假設 基礎庫 提供了 String 類型
type.fieldList [ 1 ].name = "age";
type.fieldList [ 1 ].size = 4; // 假設 int 是 32 位 整數類型
type.fieldList [ 1 ].type = "Int32"; // 假設 int 是 32 位 整數類型, 且 基礎庫 提供的 32 位 整數類型 是 Int32
type.methodList [ 0 ].name = "Sing";
// 由於 Sing() 方法 沒有 參數, 因此 argList [ 0 ] 長度爲 0, 不用 初始化
type.methodList [ 0 ].funcPtr = & ILBC_Class_B_Sing; // ILBC_Class_B_Sing 是 Sing() 方法 對應的 函數, 由 編譯器 生成
type.methodList [ 1 ].name = "Smile";
// 由於 Smile() 方法 沒有 參數, 因此 argList [ 0 ] 長度爲 0, 不用 初始化
type.methodList [ 1 ].funcPtr = & ILBC_Class_B_Smile; // ILBC_Class_B_Smile 是 Smile() 方法 對應的 函數, 由 編譯器 生成
return type;
}
ILBC_LoadClass_B_Animal() 函數 和 ILBC_LoadClass_B_Person() 函數 相似 。
當 程序 中 第一次 用到 程序集 時, ILBC 運行時(調度程序) 纔會 加載 程序集,
第一次 用到 程序集 是指 第一次 用到 程序集 裏的 類,
第一次 用到 類 是指 第一次 建立對象( new 類() ) 或者 第一次 調用靜態方法( 類.靜態方法() ) 、 第一次 訪問靜態字段( 類.靜態字段 ) 這 3 種狀況 。
類 也是 在 第一次 用到 時 加載,
固然, 第一次 加載 程序集 是 必定會 加載一個 類, 但 其它 的 類 會在 用到 時 才加載 。
加載類 完成時 會調用 類 的 靜態構造函數 。
調度程序 加載完 程序集 後, 會把 程序集 的 ILBC_Load() 返回的 ILBC_Assembly 結構體 的 指針 保存到一個 名字是 ILBC_AssemblyList 的 鏈表 裏,
新加載 的 程序集 的 ILBC_Assembly 結構體 的 指針 會 追加到 這個 鏈表 裏 。
ILBC_AssemblyList 是 調度程序 裏 的 一個 全局變量:
ILBC_LinkedList * ILBC_AssemblyList ;
ILBC_LinkedList 是一個 鏈表 實現, ILBC_LinkedList 自己是一個 結構體, 定義見下文, 再配合一些 向鏈表追加元素 、刪除元素 等函數 就是 一個 鏈表 實現, 函數 的 部分 略 。
struct ILBC_LinkedList
{
ILBC_LinkedListNode * first ; // 鏈表 頭指針
ILBC_LinkedListNode * last ; // 鏈表 尾指針
}
struct ILBC_LinkedListNode
{
ILBC_LinkedListNode * before ; // 上一個 節點
ILBC_LinkedListNode * next ; // 下一個 節點
void * element ; // 節點包含的元素, 就是 實際存放 的 數據
}
假設 有 A 、B 2 個 程序集, A 引用了 B,
B 中 包含 Class Person, Person 有 構造函數 Person() { } , 那麼, A 中 new Person() 的 代碼 會被 編譯成:
void * ILBC_Class_Person_Constructor = 0 ; // 這是 A 裏的 全局變量, 表示 Person 的 構造函數 的 函數指針, 0 表示 空指針, 也表示 未初始化
……
// 代碼 中 調用 Person 類 構造函數 的 代碼
// ILBC_Class_Person 是 高級語言(D#) 編譯器 生成的 表示 Person 類 的 Struct, 包含了 Person 類 的 字段
if ( ! ILBC_ifClassInit_Person )
{
ILBC_Init_Linked_Class_Person() ; // 初始化 Person 類
}
// ILBC_Linked_ClassSize_Person 是一個 全局變量, 表示 Person 類 佔用的 空間大小(字節數)
void * person = ILBC_gcNew( ILBC_Linked_ClassSize_Person );
// Person 類 初始化 後, 構造函數 指針 ILBC_Linked_Class_Person_Constructor 就被 初始化 了(填入了 Person 構造函數 的 地址), 就能夠調用了
ILBC_Linked_Class_Person_Constructor ( person ); // 調用 Person 類 構造函數, 把 person 結構體 指針 傳給 構造函數 進行 初始化
調用 Person 類的 靜態字段 和 靜態方法 的 代碼 和 上面 相似, 只須要把 最後一句 代碼 換成:
字段類型 變量 = * ILBC_Linked_Class_Person_靜態字段名 ; // 訪問 靜態字段
ILBC_Linked_Class_Person_靜態函數名 ( 參數列表 ) ; // 調用 靜態函數
ILBC_ifClassInit_Person 是一個 全局變量, 表示 Person 類 是否 已經 初始化, 定義以下:
char ILBC_ifClassInit_Person = 0 ;
B 程序集 的 Person 類 在 A 程序集 裏的 「初始化」 是指 完成了 Person 類 在 A 裏的 連接工做, 初始化 完成後, A 的 代碼 就能夠 訪問 Person 類 了 。
訪問 Person 類 包括 建立對象(new Person() )、調用函數 、訪問字段 。
連接工做 包括
類連接, 向 A 裏定義好的 保存 Person 類 的 佔用空間大小(Size (字節數)) 的 全局變量 寫入 類 的 佔用空間大小(Size (字節數)),
字段連接 是 向 A 裏定義好的 保存 Person 類的 各個字段的偏移量 的 變量 寫入 字段的偏移量,
函數連接 是 向 A 裏定義好的 保存 Person 類 的 各個方法 的 函數地址(函數指針) 的 變量 寫入 函數地址, 包括 構造函數 和 成員函數 。
ILBC_Linked_Class_Person_Constructor 是 一個 全局變量, 表示 Person 類 的 構造函數 的 函數指針,定義以下:
void * ILBC_Linked_Class_Person_Constructor ;
ILBC_Init_Linked_Class_Person () 的 代碼以下:
ILBC_Init_Linked_Class_Person ()
{
lock ( ILBC_ifClassInit_Person )
{
if ( ! ILBC_ifClassInit_Person )
{
ILBC_Type * type = ILBC_Runtime_GetType( "B", "Person" ) ; // 參數 "B" 表示 程序集 名字, "Person" 表示 類 名
ILBC_Linked_ClassSize_Person = type.size ;
// ILBC_Linked_Class_Person_name 是 保存 Person 類 name 字段 偏移量 的 全局變量, 由 編譯器 生成, 值 須要在 加載類 的 時候 初始化, 也就是 下面的 代碼 裏 初始化
// ILBC_Linked_ClassFieldType_Person_name 是 保存 Person 類 name 字段 類型(類型名字) 的 常量, 由 編譯器 生成, 值 由 編譯器 給出, 值 就是 name 字段 的 類型 的 名字
ILBC_Init_Linked_Class_Field( & ILBC_Linked_Class_Person_name, ILBC_Linked_ClassFieldType_Person_name, "name", type ); // 初始化 name 字段 的 偏移量
ILBC_Init_Linked_Class_Field( & ILBC_Linked_Class_Person_age, ILBC_Linked_ClassFieldType_Person_age, "age", type ); // 初始化 age 字段 的 偏移量
// 若是有 靜態字段, 也是 一樣的 初始化, 不過 靜態字段 應該 不是 初始化 偏移量, 而是 直接 是 地址,
// 靜態字段 的 指針變量 好比 「變量類型 * ILBC_Linked_Class_Person_靜態字段名 ;」
ILBC_Init_Linked_Class_Person_Constructor( type ); // 初始化 構造函數 的 函數指針
ILBC_Init_Linked_Class_Method( & ILBC_Linked_Class_Person_Sing, "Sing", type ); // 初始化 Sing() 函數 的 函數指針
ILBC_Init_Linked_Class_Method( & ILBC_Linked_Class_Person_Smile "Smile", type ); // 初始化 Smile() 函數 的 函數指針
// 若是有 靜態方法, 也是 一樣的 初始化, 靜態方法 的 指針變量 好比 「void * ILBC_Init_Linked_Class_Person_靜態方法名 ;」
ILBC_ifClassInit_Person = 1 ;
}
}
}
void ILBC_Init_Linked_Class_Field( int * fieldOffsetVar, char * fieldType, char * name, ILBC_Type * type )
{
for (int i = 0; i<type.fieldCount; i++)
{
ILBC_Field * field = & type.fieldList [ i ];
if ( field.name == name ) // 這句代碼是 僞碼 , 意思是 判斷 2 個字符串 是否相等
{
// 咱們這裏 判斷 類型 是否 相同 是 不嚴格的, 只是 判斷 了 名字
// 這裏 涉及到 類型檢查 和 類型安全, 詳細討論 見 文章 最後 總結 部分
if ( field.type ! = fieldType ) // 這句代碼是 僞碼 , 意思是 判斷 2 個字符串 是否相等
throw new Exception ( "名字爲 " + name + " 的 字段 的 類型 與 引用 的 元數據 裏的 類型 不符 。" ); // 這句代碼 是 僞碼, 應該是 函數 增長一個 errorCode 參數, 經過 errorCode 參數返回異常
* fieldOffsetVar = field -> offset;
return ;
}
}
throw new Exception( "找不到名字是 " + name + " 的 字段 。" ); // 這句代碼 是 僞碼, 應該是 函數 增長一個 errorCode 參數, 經過 errorCode 參數返回異常
}
void ILBC_Init_Linked_Class_Method ( void * funcPtrVar, char * name, ILBC_Type * type )
{
for (int i = 0; i<type.methodCount; i++)
{
ILBC_Method * method = & type.methodList [ i ];
if ( method.name == name ) // 這句代碼是 僞碼 , 意思是 判斷 2 個字符串 是否相等
{
* funcPtrVar = method -> funcPtr;
return ;
}
}
throw new Exception( "找不到名字是 " + name + " 的 方法 。" ); // 這句代碼 也是 僞碼, 應該是 函數 增長一個 errorCode 參數, 經過 errorCode 參數返回異常
}
相關的 全局變量 / 常量 總結以下:
char ILBC_ifClassInit_Person = 0 ; // Person 類 是否 已 初始化
int ILBC_Linked_ClassSize_Person ; // Person 類 佔用的 空間大小(字節數), 值 由 編譯器 在 編譯 A 項目時 根據 B 的 元數據 給出
int ILBC_Linked_Class_Person_name ; // Person 類 name 字段 的 偏移量
int ILBC_Linked_Class_Person_age ; // Person 類 age 字段 的 偏移量
const char * ILBC_Linked_ClassFieldType_Person_name ; // Person 類 name 字段 的 類型(類型名字)
const char * ILBC_Linked_ClassFieldType_Person_age ; // Person 類 age 字段 的 類型(類型名字)
void * ILBC_Linked_Class_Person_Constructor ; // Person 類 的 構造函數 函數指針
void * ILBC_Linked_Class_Person_Sing ; // Person 類 的 Sing 方法 函數指針
void * ILBC_Linked_Class_Person_Smile ; // Person 類 的 Smile 方法 函數指針
看到這裏, 你們可能會問, 若是 構造函數 和 方法 有 重載 怎麼辦 ?
確實 有這個問題, 這個 須要 再做 進一步 的 細化設計, 如今 先 略過 。
ILBC_Runtime_GetType() 函數 的 定義以下:
ILBC_Type * ILBC_Runtime_GetType( char * assemblyName, char * typeName )
{
先在 ILBC_AssemblyList 中查找 名字 是 assemblyName 的 程序集 是否已存在,
若是 不存在, 就先 加載 程序集,
加載程序集 的 過程 上文 中 提過, 就是 先把 程序集 加載 到 應用程序, 再調用 程序集 的 ILBC_Load() 函數, 返回一個 ILBC_Assembly 結構體 的 指針,
調度程序 把 這個 結構體 指針 保存 到 ILBC_AssemblyList 這個 鏈表 裏 。
找到 程序集 後, 再在 assembly.classLoaderList 裏 找 名字 是 className 的 classLoader,
找到 classLoader 之後, 看 classLoader.type 字段 是不是 空指針(0), 若是是, 就說明 Class 尚未 加載,
就 加載 Class, 加載 Class 獲得的 Type 對象 就存放在 classLoader.type 字段 裏 。
加載 Class 的 過程 上文中 講述過, 假設 加載 B 程序集 的 Person 對象,
就是調用 B 程序集 裏的 ILBC_LoadClass_B_Person() 函數, 該 函數 加載 Person 類, 並返回 表示 Person 類 的 Type 對象 的 ILBC_Type 結構體 的 指針 。
調用 類 的 靜態構造函數 ************* 這裏 加個 着重號, 類 加載 完成後 調用 類 的 靜態構造函數
返回 ILBC_Type 結構體 的 指針 。
}
訪問 Person 對象 的 字段 的 代碼 是:
void * person ;
……
char * name = * ( person + ILBC_Linked_Class_Person_name ) ;
int age = * ( person + ILBC_Linked_Class_Person_age ) ;
調用 Person 對象 的 方法 的 代碼 是:
void * person ;
ILBC_Linked_Class_Person_Sing ( person ) ; // 調用 Sing() 方法, person 參數 是 this 指針
ILBC_Linked_Class_Person_Smile ( person ) ; // 調用 Smile() 方法, person 參數 是 this 指針
總結一下:
ILBC 的 連接 是 相似 .Net / C# 的 動態連接,
ILBC 的 連接 以 程序集 爲 單位, 採用 延遲加載(Lazy Load) 的方式, 只有用到 程序集 的時候才加載, 「用到」 是指 第一次 用到 程序集 裏的 類(Class) 。
將 程序集 加載 到 應用程序 之後, 對 程序集 裏的 類(Class) 也採用 延遲加載(Lazy Load) 的方式,
第一次 用到 類 的 時候纔會 初始化 類 的 連接表, 連接表 初始化 完成後, 就 能夠 調用 類 了, 包括 建立對象,訪問 字段 和 方法 。
連接表 不是 一個 「表」, 而是 一堆 全局變量 / 常量, 就是 上文 中 列舉出的 全局變量 / 常量, 這裏再列舉出來看看:
char ILBC_ifClassInit_Person = 0 ; // Person 類 是否 已 初始化
int ILBC_Linked_ClassSize_Person ; // Person 類 佔用的 空間大小(字節數), 值 由 編譯器 在 編譯 A 項目時 根據 B 的 元數據 給出
int ILBC_Linked_Class_Person_name ; // Person 類 name 字段 的 偏移量
int ILBC_Linked_Class_Person_age ; // Person 類 age 字段 的 偏移量
const char * ILBC_Linked_ClassFieldType_Person_name ; // Person 類 name 字段 的 類型(類型名字)
const char * ILBC_Linked_ClassFieldType_Person_age ; // Person 類 age 字段 的 類型(類型名字)
void * ILBC_Linked_Class_Person_Constructor ; // Person 類 的 構造函數 函數指針
void * ILBC_Linked_Class_Person_Sing ; // Person 類 的 Sing 方法 函數指針
void * ILBC_Linked_Class_Person_Smile ; // Person 類 的 Smile 方法 函數指針
這些 全局變量 是 A 裏 定義 的, 是 A 裏 引用 B 的 連接表 。
注意, Class 的 加載 是 在 ILBC 運行時 裏 進行的, 一個 Class 的 加載 對於 整個 應用程序 只進行一次,
Class 的 連接表 初始化(Init) 是 和 程序集 相關的, 假設有 A 、B 、C 3 個 程序集 引用了 D 程序集,
那麼 當 A 用到 D 的時候, 會 初始化 A 裏 引用 D 的 連接表,
當 B 用到 D 的時候, 會 初始化 B 裏 引用 D 的 連接表,
當 C 用到 D 的時候, 會 初始化 C 裏 引用 D 的 連接表 。
連接表 是 屬於 程序集 的, 假設 A 引用了 B C D, 那麼 A 裏 會有 B C D 的 連接表,
也就是說 上面的 全局變量 會在 A 裏 聲明 3 組, 分別 對應 B C D 程序集 。
說到這裏, 咱們會發現, 上面的 全局變量 的 命名 沒有 包含 程序集 的 名字, 好比 ILBC_Linked_Class_Person_name,
這個 表示 Person 類 的 name 字段 的 偏移量,
可是 並無 表示出 Person 類 是 哪個 程序集 的 。
因此, 應該 給 變量 增長一個 分隔符(鏈接符) 來 分隔(鏈接) 各項信息,
咱們規定, InnerC 應支持 在 變量名 裏 使用 "<>" 字符串, 這樣可使用 "<>" 來 分隔(鏈接) 各項信息 。
注意, 是 "<>" 字符串, 不是 "<", 也不是 ">" , 也不是 "< …… >" ,
好比, a<>b 這個 變量名 是 合法的, a<b 是 不合法 的, a>b 是 不合法的, a<b>c 這個變量名 也是 不合法的 。
ILBC_Linked_Class_Person_name 能夠 這樣 來 表示:
ILBC_Linked<>B<>Person<>name , 這表示 連接(引用) 的 B 程序集 的 Person 類 的 name 字段 的 偏移量
"<>" 字符串 在 D# 裏 是 不能用於 程序集 名字空間 類 字段 方法 的 名字 的, 因此能夠在 C 中間語言 裏 用在 變量名 裏 做爲 分隔符(鏈接符) 。
ILBC 運行時 調度程序 應提供 如下 函數:
ILBC_Type * ILBC_Runtime_GetType( char * assemblyName, char * typeName )
該函數用於 返回 指定的 程序集名 的 程序集 中 指定的 類名 的 類 的 Type 對象
ILBC_Type 是 調度程序 中 定義的 結構體, 爲了能讓 程序集 訪問, 須要 高級語言(D#)編譯器 引用 調度程序 發佈 的 頭文件(.h 文件),
這個 頭文件 咱們 能夠命名爲 ILBC_Runtime.h , 裏面 會 包含 ILBC_Assembly 、ILBC_ClassLoader 、ILBC_Type 、ILBC_Field 、ILBC_Method 、ILBC_Argument 等 結構體 定義 。
void * ILBC_Runtime_heapNew ( int size )
該函數用於 從 堆 裏 分配 一塊 指定大小 的 內存塊, 參數 size 是 內存塊 大小(字節數) 。 返回值 是 內存塊 指針 。
ILBC 運行時 本身實現了一個 堆 和 GC 。
固然 對應的 還會有一個 void ILBC_Runtime_heapFree ( void * ptr, int size ) 函數,
C 語言 裏的 void free(void *ptr); 是沒有 size 參數的, So 。
沒事, 這個能夠保留討論 。
ILBC 程序集 應提供 如下 函數:
ILBC_Assembly * ILBC_Load()
該函數 在 ILBC 運行時 調度程序 加載 程序集 時 調用, 負責 程序集 的 初始化 工做,
包括 建立一個 ILBC_Assembly 結構體, 並 初始化 ILBC_Assembly 結構體 的 classLoaderList 字段, 能夠參考 上文 代碼 。
ILBC 運行時 調度程序 接收到 程序集 的 ILBC_Load() 函數 返回的 ILBC_Assembly 結構體 指針 後, 會 將 該指針 保存到 ILBC_AssemblyList 中,
ILBC_Assembly 是 調度程序 裏的一個 全局變量, 是一個 鏈表 。
說到 鏈表, 調度程序 裏 保存 Assembly 的 列表 ILBC_AssemblyList 是 鏈表,
Assembly 裏 保存 Type 的 列表 classLoaderList 是 數組,
Type 裏 保存 Field 、Method 的 列表 fieldList, methodList 也是 數組,
而 上文 中 根據 名字 查找 Field 、Method 的算法是 遍歷 數組, 查找 Assembly 、Type 的部分雖然沒有直接用代碼寫出來, 但應該是 遍歷 鏈表 / 數組 。
從 性能優化 的 角度 來看, 根據 名字 查找 成員(Assembly, Type, Field, Method 等) 應該 優化 爲 查找 Hash 表,
這個 優化 關係 到 加載 程序集 和 類 的 效率, 也是 反射 的 效率 。
動態連接 程序集, 加載 程序集 和 類, 就是一個 反射 的 過程 。
相傳 .Net 2.0 對 反射 性能 進行了優化, 使得 反射 性能 獲得了 明顯的 提高, 大概 也是 加入了 Hash 表 吧 ! 哈哈哈 。
而 .Net 對 反射 進行了 優化, 理論上 自己 就是 提高了 動態連接 程序集 、加載 程序集 和 類 的 效率, 也就是 提高了 .Net 運行 應用程序 的 效率 。
在 .Net / C# 裏, Hash 表 可使用 Dictionary, 但在 IL 裏, 估計 得 本身寫一個 。
不過 這也是一件 好玩的事情,
我接下來 會 寫一篇 文章 《本身寫一個 Hash 表》 。
《本身寫一個 Hash 表》 這篇文章已經寫好了, 見 http://www.javashuo.com/article/p-ervzhqtj-dk.html 。
調度程序 的 ILBC_Runtime_GetType() 、 ILBC_Runtime_heapNew() 、 ILBC_Runtime_heapFree() 和 程序集 的 ILBC_Link() 這 4 個 函數 是 操做系統 動態連接庫 規範 定義 的 動態連接庫 導出函數 。
這麼考慮 主要是 以前 並未打算 本身實現一個 C 編譯器,
但 如今 既然 咱們要本身 實現一個 C 編譯器(InnerC), 那麼 這些就 不成問題了,
這 4 個 函數 能夠 用 咱們本身 定義的 規則 來 訪問 。
好比, 咱們能夠 定義 在 調度程序 的 開頭 的 一段字節 來 保存 ILBC_Runtime_GetType() 、 ILBC_Runtime_heapNew() 、 ILBC_Runtime_heapFree() 這 3 個 函數 的 地址, 在 程序集 的 開頭 的 一段字節 來 保存 ILBC_Link() 函數 的 地址 。
這樣, 調度程序 和 程序集 之間 就能夠經過 函數指針 來 調用 接口函數, 速度很快 。
但 若是要這樣的話, 調度程序 和 程序集 應該是 同構 的, 同構 是指 同一種語言 、同一個編譯器 編譯 產生的 本地代碼 。
因此, 調度程序 也應該是 用 InnerC 編寫 和 編譯 生成的 。
這麼一來, InnerC 的 地位 就 很重要了 。 ^^
InnerC 是 ILBC 的 基礎 。
不過 這樣一來, InnerC 可能也須要 支持 結構體, 否則 很差寫 。 呵呵 。
這樣的話, ILBC 本地代碼 程序集 就 不須要 是 操做系統 定義的 動態連接庫, 而是 按照 ILBC 規範 編譯成的 本地代碼, 咱們能夠把 這種 按照 ILBC 規範 編譯成的 本地代碼 程序集 的 擴展名 命名爲 「.iln」, 表示 「ILBC Native Code」 。
關於 泛型, 忽然想到, 泛型 純粹 是 編譯期 檢查, 除此之外 什麼 都 不用作, 頂多爲 每一個 泛型類型 生成一個 具體類型, 經過 具體類型 能夠獲取 泛型參數類型 就能夠了 。
但 泛型 確實能 提升性能, 由於 泛型 不須要 運行期類型轉換(Cast),
運行期 類型轉換 就是 一堆 if else ,
咱們能夠看看 編譯後 生成的代碼,
源代碼:
B b = new B();
A a = (A) b ;
編譯後的代碼:
B b = new B();
A a;
Type aType = typeof(A) ;
Type bType = typeof(B);
if ( aType == bType )
a.ptr = b.ptr ; // 這句是 僞碼, 表示 b 引用 的 指針值 賦給 a 引用
else if ( aType 是 bType 的 父類)
a.ptr = b.ptr ;
else if ( 其它 轉型 規則 )
a.ptr = b.ptr ; // 或者 其它 轉型方式, 好比 拆箱裝箱
else
throw new CastException( "沒法將 " + bType + " 的 對象 轉換爲 " + aType + " 。" ) ;
而 泛型 是這樣:
List<string> strList = new List<string>();
strList [ 0 ] = "aa" ;
string s = strList [ 0 ];
編譯後的代碼:
List<string> strList = new List<string>();
strList [ 0 ] = "aa" ;
string s;
s.ptr = strList [ 0 ].ptr; // 指針 直接 賦值
由於 編譯期 已經作過 類型檢查, 因此 引用 的 指針直接賦值, 因此 泛型 沒有 性能損耗 。
固然, JIT 編譯器 須要爲 泛型類型 生成 具體類型, 使得 泛型類型 能夠按照 CLR 的 規則 「是一個 正常的 類型」, 經過 具體類型 能夠獲取 泛型參數類型 。
泛型類型? 具體類型? 泛型參數類型?
有點繞 。
假設有 class A<T> ,
那麼, A<T> 叫 泛型類型,
A<string> 叫 具體類型,
T , 叫 泛型參數類型, 好比 A<string> 的 泛型參數類型 是 string 。
對於 ILBC, 具體類型 能夠在 C 中間代碼 裏 生成 。
再來看看 基礎類型,
基礎類型 包括 值類型 、數組 、String,
ILBC 會 內置實現 基礎類型,
值類型 包括 int, long, float, double, char 等, 這些 類型 在 C 語言 裏 都有 對應的類型, 可是爲了實現 「一切皆對象」, 即 全部類型, 包括 值類型 和 引用類型 都從 object 繼承 這個 架構, 還須要 對 C 語言 裏的 int, long, float, double, char 等 作一個包裝, 用一個 結構體(Struct) 來把 int, long, float, double, char 等 包起來 。
包起來之後, 爲了提升執行效率, 編譯器 還須要 對 代碼 進行一些 優化, 對於 棧 裏 分配 的 int, long, float, double, char 等 的 加減乘除 等 運算 就 直接用 C 語言 的 int, long, float, double, char 等 的 加減乘除 等 運算, 即 不用 結構體 包起來, 而是 直接編譯爲 C 語言 裏 的 int, long, float, double, char 等 。
而 對於
void Foo( object o )
{
Type t = o.GetType() ;
}
這樣的代碼, 由於 參數 o 多是 任意類型, 因此 傳給 參數 o 的 int 類型 就 應該是 包裝過的 int, 也就是 一個 結構體, 好比:
struct Int32
{
int val ; // 值
string typeName ; // 類型名字, 或者 廣義的來講, 這個 字段 表示 類型信息
}
Object 的 GetType() 方法 經過 這個 字段 返回 Type 對象 。
而 對於 typeof(int) 則 能夠在 編譯器 編譯爲 Hard Code 返回 Int32 的 Type 對象 。
又好比 對於 Convert.ChangeType( object o, Type t ) 方法,
假設 參數 o 要傳一個 int 類型的話, 也須要 傳 包裝過的 int 類型, 也就是 上文 定義的 struct Int32 。
因此, InnerC 的 InnerC to Byte Code 模塊, 除了 語法分析器, 又增長了一個模塊, 優化器 。
So ……
語法分析器 產生表達式對象樹 後, 把 表達式樹 傳給 優化器, 優化器 能夠 閱讀 表達式樹, 發現能夠優化 的 地方 能夠修改 表達式樹,
修改後的 表達式樹 就是 優化後的 表達式樹, 再 傳給 Byte Code to Native Code, 編譯爲 本地代碼 。
能夠把 優化後 的 表達式樹 再 逆向爲 C 代碼, 這樣就能夠 看到 優化後 的 C 中間代碼 。
InnerC 的 InnerC to Byte Code 能夠提供 逆向 的 功能 。
再來看 結構體(Struct),
D# / ILBC 不打算 提供 結構體, 由於 結構體 沒什麼用 。 ^^
提供 結構體 會讓 ILBC 的 設計 變得 複雜, 增長了 研發成本 。
固然 結構體 使用 棧空間, 減小了 堆 管理 和 GC 的 工做, 可是 從 線程 的角度來看, 棧 比較大的話 線程切換 的 性能消耗 可能 也 比較大 。 看你怎麼看了 ~ 。
出於 動態連接 的 要求, .Net / C# 的 結構體 應該不是 在 編譯期 靜態分配內存空間 的, 而是 在 運行期 分配空間, 由於 結構體 保存 在 棧 裏, 因此 是 動態分配 棧 空間 。
因此, .Net / C# 裏 建立 結構體 也是用 new 關鍵字 。
D# / ILBC 的 DateTime 類型 是一個 引用類型(Class), 是一個 能夠用 D# 寫的 普通的 引用類型(Class) 。
.Net / C# 的 DateTime 是 值類型, 我估計 .Net / C# 如今 想把 DateTime 改爲 Class, 可是 改不過來了 。 哈哈哈哈。
如 上文所述, D# / ILBC 提供 的 基礎類型 是 基礎類型 值類型 、數組 、String, 值類型 包括 int, long, float, double, char 等,
基礎類型 由 D# / ILBC 內置實現 。
其它類型 由 D# 編寫, 包括 DateTime 及 基礎庫 裏的 各類類型 。
說到 基礎庫, 就會想到 和 本地代碼 的 交互性, 就是 訪問 本地代碼,
在 .Net / C# 裏, 託管代碼 和 本地代碼 之間 的 交互 使用 P / Invoke ,
對於 D# / ILBC, 會提供這樣一些接口:
1 指針
2 申請一段 非託管內存, 非託管內存 不會由 GC 回收, 須要 手動回收
3 回收一段 非託管內存
有了 這 3 個 接口, 基本上就夠了, 能夠 訪問 非託管代碼 了 。
非託管內存 和 託管內存 同屬一個堆, 只是 GC 不會回收 非託管內存 。
再來看 類型檢查 和 類型安全,
上文中 初始化 連接表 的 字段偏移量 時 會對 字段類型 進行 檢查, A 程序集 在 運行期 連接 的 B 程序集 的 Person 類 的 字段類型 應該 和 A 程序集 在 編譯期 引用 的 B 程序集 的 Person 類 的 類型一致, 不然 認爲 類型不匹配, 不容許連接, 也就是 不容許 使用 如今 的 Person 類 。
爲何要進行 類型檢查 ?
若是 類型不匹配, 會發生 訪問了不應訪問的內存 的 錯誤, 這種 錯誤 難以排查, 產生的 結果 是 意想不到 的,
這也是 java, .Net 這類 虛擬機(運行時) 出現 要 解決的 問題 吧 !
java, .Net 這類 虛擬機(運行時) 經過 運行期 類型檢查 來 實現 類型安全, 避免 類型錯誤 致使 訪問了錯誤的內存 。
.Net / C# 對 類型 的 檢查 是 嚴格準確 的, 全部類型 最終會 歸結到 基礎類型(值類型 數組 String),
而 基礎類型 都是 .Net 內置類型, 是 強名稱 的, 能夠 嚴格 的 檢查,
推而廣之, .Net 基礎庫 都是 強名稱 的, 能夠 準確 的 檢查 類型,
對於 開發人員 本身編寫 的 類, 也能夠 根據 字段 逐一校驗, 實際加載 的 程序集 的 類 的 字段 應包含 大於等於 編譯時 引用的 程序集 的 類 的 字段, 字段 名字 和 類型 必須 匹配, 好比 編譯時 引用 的 Person 類 的 name 字段 是 String 類, 那麼 運行期 加載的 B 的 Person 類 也應該要有 name 字段, 且 類型 應該是 String, 不然 認爲 類型 不匹配 。
咱們 上文 對 字段 類型 的 檢查 是 不嚴格 的, 只是 檢查 類型 的 名字 。
應該注意的是, 強名稱 類型檢查 不表明 內存安全, 強名稱 只是 驗證 程序集(類) 的 身份, 可是 類 若是 自己 存在 Bug, 也會發生 訪問了 自身對象 之外 的 內存 的 問題 。
可是, 因爲 數組 做爲 基礎類型 提供, 數組 中 會判斷 「索引 是否 超出 數組界限」, 因此, 開發者 寫的 代碼 通常 應該不會發生 訪問內存越界(訪問了 自身對象 之外 的 內存) 的 問題 。
固然 這僅限於 託管代碼, 對於 非託管代碼, 由於 指針 的 存在, 因此有可能發生 訪問內存越界 的 問題 。
.Net / C# 解決 這個問題的作法是, 把 指針 用 IntPtr 類型 封裝起來, 不容許修改, 只是做爲一個 常量數值 傳遞 。
另外一方面, 若是 Class Size(類佔用的空間大小(Size)) 、 字段偏移量 、 方法的函數地址 這 3 項 元數據 都是 動態連接 的話,
類型檢查 其實 也沒什麼 好查的 。 ^^
由於 這 3 項 元數據 都是 來源於 同一個 類, 是 自洽 的, 若是發生了 訪問內存越界 的問題, 是 類 自身代碼 的 邏輯問題 。
強名稱 檢查 是 驗證 程序集(類) 的 身份 。
爲何要 動態連接 Class Size(類佔用的空間大小(Size)) 、 字段偏移量 ?
這是爲了 兼容性, 好比, B 程序集 的 Person 類 如今有 name, age 2 個 字段, 後來又加了一個 favour 字段, 這樣就改變了 Class Size,
name, age 的 偏移量 也可能會發生改變,
可是 應該 讓 原來 引用了 B 程序集 的 應用程序 能 繼續 正常 使用 Person 類,
因此 須要 動態連接 Class Size 和 字段偏移量 。
考慮到 軟件 被 攻擊 和 破解 的 風險, 能夠考慮 加入 像 .Net / C# 同樣的 強名稱程序集 的 功能 。
不過若是 是 AOT 編譯 的話, 即便沒有 強名稱, 要 破解 也沒有那麼容易, 由於 AOT 編譯 生成的是 本地代碼 。 ^^
咱們上面說 程序集 和 類型 的 名字, 好比 調用 ILBC_Runtime_GetType( "B", "Person" ) 函數 返回 Person 的 ILBC_Type 結構體 指針,
"B" 是 程序集 名字, "Person" 是 類 名,
這段代碼 是 舉例, 咱們給 程序集 名字 和 類型 的 名字 下一個 定義:
程序集 名字 是 程序集 文件 的 文件名(不包含 擴展名),
類型 的 全名(Full Name) 是 「名字空間.類名」, 這個 和 C# 同樣 。
假設 名字空間 是 「B」, 則 Person 類 的 全名 是 「B.Person」,
上文 調用 ILBC_Runtime_GetType( "B", "Person" ) 函數 的 類名 應該是 類 的 全名 「B.Person」 。
若是 D# / ILBC 支持 強名稱 程序集, 則 對於 強名稱 程序集, Full Name 中 還會包含 強名稱 版本信息, 能夠認爲 和 .Net / C# 同樣 。
咱們再詳細說明一下 高級語言(D#)編譯 的 過程,
高級語言(D#) 編譯 會生成 2 個文件,
1 元數據 文件,
2 程序集 文件
上文中 沒有 交代 元數據 文件,
元數據 文件 保存了 程序集 的 元數據 信息, 包括 類, 類的字段(字段名 、字段類型), 方法(方法簽名),
高級語言(D#) 編譯器 能夠 根據 元數據 知道 程序集 有 哪些成員(類, 類的字段, 類的方法),
這樣能夠用於 開發時 的 智能提示, 以及 編譯時 的 類型檢查 。
最重要 的 是 高級語言(D#) 編譯器 須要 根據 元數據 生成 程序集 中 加載 Class 的 代碼,
加載 Class 的 代碼 就是 上文中的 ILBC_Type * ILBC_LoadClass_B_Person() 函數 ,
這個 函數 就是 「Class Loader」, 是 保存在 ILBC_Assembly 結構體 的 classLoaderList 字段中,
classLoaderList 是 一個 數組, 元素 是 ILBC_ClassLoader 結構體, ILBC_ClassLoader 結構體 的 load 字段 就是 保存 「Class Loader」 函數 的 函數指針 的 字段 。
程序集 文件 多是 Byte Code 程序集, 也多是 本地代碼 程序集,
若是是 JIT 編譯方式, 就是 Byte Code 程序集,
若是是 AOT 編譯方式, 就是 本地代碼 程序集,
高級語言(D#) 編譯器 編譯時 只須要 元數據 文件, 不須要 程序集 文件,
應用程序 運行的時候 只須要 程序集 文件, 不須要 元數據 文件 。
元數據 文件 就像是 C 語言 的 頭文件 。
因此, ILBC 涉及的 文件 會有 這麼幾種:
1 元數據 文件
2 C 中間代碼 文件, 這個 不是 必需 的, 可是 做爲 調試 研究 學習, 能夠生成出來 。
3 Byte Code 程序集 文件,
4 本地代碼 程序集 文件,
咱們 能夠 對 這 4 種 文件 命名 擴展名:
1 元數據 文件, 擴展名 「.ild」, 表示 「ILBC Meta Data」,
2 C 中間代碼 文件, 擴展名 「.ilc」, 表示 「ILBC C Code」,
3 Byte Code 程序集 文件, 擴展名 「.ilb」, 表示 「ILBC Byte Code」,
4 本地代碼 程序集 文件, 擴展名 「.iln」, 表示 「ILBC Native Code」,
好的, ILBC 規範 暫時 就寫這麼多 ,
接下來的 計劃 是 堆 、 GC 、 InnerC 語法分析器 。
有 網友 提出 不須要 沿襲 傳統的 面向對象 方式, 而是能夠用和 Rust 類似的方式,
我下面 寫一段代碼 把這種方式 描述一下:
class C1
{
int f1;
string f2;
}
void M1( C1 this )
{
……
}
void M2( C1 this)
{
……
}
這就是 C1 類 的 定義, 方法 定義在 外面, 相似 C# 的 擴展方法,
這至關於 傳統的 面向對象 裏 C1 類 有 2 個 方法(M1(), M2()),
咱們在 定義 一個 C2 類, 讓 C2 「繼承」 C1 類:
class C2 : C1
{
}
再把 M1() 的 定義 改一下:
void M1( C2 C1 this )
{
……
}
this 參數 的 類型 加入了 C2, 由 C2 C1 共同做爲 this 參數 的 類型,
這樣 C2 就 繼承 了 C1 的 M1() 方法,,, 注意 只 繼承了 M1() 方法, 沒有 繼承 M2() 方法 。
C2 能夠 添加 本身 的 字段, 也能夠 多繼承, 固然 若是 「父類」 之間有 重名 的 字段, 就 不能 同時繼承 有 重名 字段 的 父類 。
C2 也能夠 添加 本身 的 方法, 事實上 這也不能 說是 本身 的 方法, 這個 方法 不只僅 能在 「父子」 類 之間 共享,
也能在 「毫無關係」 的 類 之間 共享, 只要 方法 內 對 this 引用 的 字段 在 類 裏 存在就行 。
這種 作法 確實 挺 呵呵 的, 但也 很爽 。
這種作法 我稱之爲 「靜態綁定」, 由於 和 Javascript 的 「動態綁定」 類似, 只不過 這是 在 編譯期 進行的, 因此叫 「靜態綁定」 。
同時, 從 編譯期 「靜態」 的 角度, 又和 泛型 很像 。
網友 說 這種作法 「只須要 結構體 和 擴展方法 就行, 不須要 類 。」 ,
確實, 就是這樣, 只要有 結構體 和 擴展方法 就能夠 。
說的 直 一點, 只要有 結構體 和 函數 就能夠 。
我要 呵呵 了, 這算是 面向過程 -> 面向對象 -> 面向過程 麼 ?
通過後來的 討論 和 思考, D# 仍是不打算這樣作, D# 的 目標 是 實現一個 經典 的 簡潔 的 面向對象 語言 。
D# 會 支持 簡潔 的 面向對象 和 函數式 。
簡潔 的 面向對象 包括 單繼承 、接口 、抽象類 / 抽象方法 / 虛方法,
函數式 是 閉包 。
不過, 關於 上述 的 「靜態綁定」 的 作法, 卻是 討論清楚 了, 「綁定」 有 3 種:
1 靜態綁定, 在 編譯期 爲 每一個 綁定 生成一份 方法(函數) 代碼, 每一份 函數 代碼 邏輯相同, 區別是 訪問 對象 字段 的 偏移量 。
2 靜態綁定, 方法(函數) 只有一份, 但在 編譯期 爲 每一個 綁定 生成一段 綁定代碼, 綁定代碼 的 邏輯 是 把 對象 字段 的 偏移量 轉換爲 函數 裏 對應的 偏移量 。
3 動態綁定, 在 運行期 爲 綁定 生成 綁定代碼 。
關於 堆 和 GC, 個人 想法 是這樣:
GC 根據 2 張 表 來 回收 對象(內存),
1 引用表
2 對象表
這 2 張表 其實是 鏈表,
每次 new 對象 的 時候, 會把 對象 添加 到 對象表 裏,
每次 給 引用 賦值 的 時候, 會把 引用 添加 到 引用表 裏,
每次 引用 超出 做用域, 或者 引用 被賦值 爲 null 時, 會 將 引用 從 引用表 裏 刪除, 固然 這段代碼 是 編譯器 生成的 。
這樣, GC 回收 對象(內存) 的 時候, 就 先 掃描 引用表, 對 引用表 裏 的 引用 指向 的 對象, 在 對象表 裏 作一個標記, 表示 這個 對象 還在使用,
掃描完 引用表 後, 掃描 對象表, 若是 對象 未被標記 還在使用, 就表示 已經沒有 引用 在 指向 對象, 能夠 回收對象 。
而 要 在 每次 給 引用 賦值 的 時候 把 引用 添加到 引用表, 須要 lock 引用表, 把 對象 添加到 對象表 也須要 lock 對象表 。
lock 會 帶來 性能損耗, 經過 測試 能夠看到, C# 中 lock 的 時間 花費 大約 是 new 的 3 倍 (new 應該要 查找 和 修改 堆表, 因此 應該 也有 lock),
執行次數 比較小時, 小於 3, 好比 10 萬次,
執行次數 比較大時, 大於 3, 好比 1 億次,
因此, 看起來, C# 的 new 的 lock 的 效率 比 lock 關鍵字 的 lock 的 效率 高,
或者說, 若是 咱們 用 上述 的 架構, 給 引用 賦值 時 把 引用 添加到 引用表, 使用 lock 關鍵字 來 實現 lock,
這樣 對 性能 的 影響 很大, 只要 想一想 給 引用 賦值 的 性能花費 比 new 還大 就 知道 了,
從 測試結果 上來看, new 的 執行 應該是 指令級 的, 大概在 5 個 指令 之內 就能夠完成,
對於 .Net / C# 這樣有 GC 的 語言, 應該 只須要 從 剩餘空間 中 分配 內存塊 就能夠, 不須要 像 C / C++ 那樣 用 樹操做 查找 最接近 要 分配 的 內存塊 大小 的 空閒空間,
再加上 lock 的 時間, 所有加起來 大概 在 5 個 指令 之內,
lock 大概 佔 2 個 指令, 開始 lock 佔 1 個 指令, 結束 lock 佔 1 個 指令,
固然 這些 是 估算 。
因此 能夠看出來, .Net / C# 的 new 操做 對 堆表 的 lock 是 指令級 的, 不是調用 操做系統 的 lock 原語,
這樣 的 目的 是 讓 new 的 操做 很快, 接近 O(1),
對於 ILBC 而言, 若是 採用 給 引用 賦值 時 修改 引用表, new 對象 時 修改 對象表,
那麼, 修改 引用表 和 對象表 的 操做 也應該 接近 O(1), 就是 像 .Net / C# 的 new 同樣, 這樣纔有足夠的效率 。
這就是說, 修改 引用表 和 對象表 的 lock 也要像 .Net / C# 的 new 對 堆表 的 lock 同樣, 是 指令級 的 。
這就須要 咱們 本身 來 實現一個 lock, 而不是使用 操做系統 的 lock 原語 。
怎麼來 實現 本身的 一個 lock ?
根據 網上 查閱 的 結果, 光從 軟件 層面 是 不行 的, 光從 C 語言 層面 也不行, 須要 硬件 的 支持 和 彙編 編程 。
能夠參考 《聊聊C++中的原子操做》 https://baijiahao.baidu.com/s?id=1609585581486387645&wfr=spider&for=pc ,
《java併發中的原子變量和原子操做以及CAS介紹》 https://blog.csdn.net/wxw520zdh/article/details/53731146 ,
文中提到 「CAS …… 雖然看似複雜,但倒是 Java 5 併發機制優於原有鎖機制的根本。」 ,
而 CAS 是 經過 CPU 提供的 CMPXCHG 指令 支持, 能夠參考 《cpu cmpxchg 指令理解 (CAS)》 https://blog.csdn.net/xiuye2015/article/details/53406432 ,
因此 咱們能夠 用 CMPXCHG 指令 來實現 lock , 原理 是 這樣:
在 內存 裏用一個 字 來 存儲 lock 標誌(flag), 若是 是 64 位 處理器, 則 字長 是 64, 即 8 個 字節(Byte),
簡化起見, 咱們 就 不 考慮 32 位 處理器 了, 只 考慮 64 位 處理器 。
當要 lock 時, 用 CMPXCHG 指令 比較 flag 是否 等於 0, 若是相等 則 將 當前線程 ID 複製到 flag, 這表示 當前線程 得到了 鎖, 接着執行 鎖 裏 要執行 的 操做 就行 。
若是 不等於 0, 則 CMPXCHG 指令 會把 當前 flag 的 值 複製到 指定 的 寄存器 裏, 檢查 寄存器 裏 的 flag 值 是否 是 當前線程 ID, 若是 是, 表示 在 當前線程 的 鎖 範圍內, 接着執行 鎖 裏 要 執行 的 操做 就行 。
若是 flag 值 不等於 當前線程 ID, 表示 當前鎖 由 別的 線程 佔有, 則 當前線程 掛起, 掛起前 會把 指令計數器 再次指向 上述 檢查鎖 的 指令, 下次 恢復運行 時, 會 從新執行 上述 檢查鎖 的 操做 。
咱們能夠用 多個 字 來表示 多個 lock, 好比 用 一個字 表示 引用表 lock, 一個字 表示 對象表 lock, 一個字 表示 堆表 lock, 等等 。
固然, 爲了提升效率, 對象表 lock 和 堆表 lock 大概 能夠 合爲一個 lock, 由於 修改 對象表 和 堆表 都 發生在 new 操做 的 時候, 能夠把 new 操做 做爲一個 原子操做, 只用 一個 lock, 這樣, new 操做 包含的 2 個步驟 修改 對象表 和 修改 堆表 都在 一個 lock 裏 進行 。
這種作法 相比 操做系統 的 lock 原語, 可能更簡單, 可是 功能 也 相對侷限, 好比 不能支持 嵌套 lock, 以及 必須 預先 爲 每一種 lock 分配一個 字, 而 操做系統 lock 是 能夠 動態 lock 的, 好比 C# 中 只要 調用 Monitor.Enter() 方法 就能夠 開始 lock, 一般 咱們 是用 lock 關鍵字, 這在 編譯期 被 編譯器 處理爲 Monitor.Enter() 和 Monitor.Exit() 方法對, 可是 若是 在 運行期 調用 Monitor.Enter() 方法, 也是 能夠 開始 lock 的 。
操做系統 的 lock 可能 是 利用了 虛擬內存, 或者說 存儲管理部件, 只須要 在 存儲管理 的 鎖表 裏 設置 要鎖定 的 地址, 存儲管理 部件 會判斷 是否容許 訪問 該地址 。
設置 鎖表 的 原理 是, 在 鎖表 裏 設置 當前線程 ID 和 要鎖定的地址, 若是 相同 的 線程 ID + 鎖定地址 已經 存在, 則 設置失敗, 設置失敗 則 線程掛起, 等下次 恢復運行 時 再接着設置 。
設置成功 則 表示 當前線程 得到 對 指定地址 的 鎖, 存儲管理部件 將 只容許 當前線程 訪問 指定地址, 不容許 其它線程 訪問 指定地址 。
事實上, 咱們 用 CMPXCHG 指令 的 作法 也能夠 實現 和 操做系統 相似 的 效果, 包括 動態的鎖定 任意 的 對象(不須要 預先 分配字), 也 支持 嵌套 lock,
這須要 在 object 類(全部 引用類型 的 基類) 裏 加入一個 lock 字段, 當咱們 lock 某個 對象 時, 會先看 lock 字段 是否等於 0, 若是 等於 0, 則 寫入 當前線程號, 這樣 就 得到了 對 該 對象 的 鎖, 若是 不等於 0, 則 比較 是否等於 當前 線程 ID, 若是 等於, 表示 對象 被 當前對象 鎖定, 因而接着執行 鎖定 裏 的 操做, 若是 不等, 表示 對象 被 其它線程 鎖定, 則 當前線程 掛起, 等下次 恢復運行 時, 重複上述過程 。
這個過程 和 上面敘述的 利用 CMPXCHG 指令 實現 鎖 的 過程 是同樣的, 但不用 預先 分配 字, 用 object 的 lock 字段 做爲 這個 「字」 就能夠 。
判斷 object 的 lock 字段 是否 等於 0, 若 等於 則 寫入 當前 線程號, 返回 true, 不然 lock 字段不變, 返回 false, 這個操做是 「原子操做」, 這個 原子操做 就是 CMPXCHG 指令 實現的 。
但 用 咱們的 作法 有一個條件, 就是 須要在 全部 (可能 併發) 訪問 對象 的 地方 都 加上 lock,
而 操做系統 的 鎖 則 沒必要需, 操做系統 因爲是利用 虛擬內存(存儲管理部件) 實現的, 因此 在 代碼 的 a 處 加了 lock, b 處 不加 lock, 但 a 處 鎖定 對象, 則 b 處 將不能訪問 。
雖然如此, 咱們在 使用 操做系統 lock 的 時候, 一般 也會在 a 處 和 b 處 都 加上 lock, 這是爲了 設計意圖 的 須要, 咱們 須要 a 和 b 嚴格的 同步(互斥)通訊, 就 須要 給 a 處 和 b 處 都 加上 lock 。
我把 咱們 的 作法 稱爲 「IL Lock」 , 用 關鍵字 illock 表示,
把 操做系統 的 lock 稱爲 「System Lock」, 用 關鍵字 syslock 表示,
在 D# 中, 使用 IL Lock 能夠這樣寫:
illock ( obj )
{
……
}
使用 System Lock 能夠這樣寫:
syslock ( obj )
{
……
}
理論上, 咱們能夠提倡 使用 IL Lock, 這樣能夠 得到 比 System Lock 更高 的 性能 。 ^^
好的, 堆 和 GC 的 部分 基本 理清 了, 接下來 會開始 InnerC 語法分析器 。
到 目前爲止, InnerC 在 ILBC 的 地位 變得重要, InnerC 會是 ILBC 的 內核模塊 。
InnerC 支持 基礎類型(int, long, float, double, char), if else, for, while, 函數, 指針, 數組, 結構體,
InnerC 不保證 支持 Ansi C 的 所有標準,
InnerC 還會有一些 新的 特性:
1 對 void * 類型 的 函數指針 不檢查 函數簽名, 能夠調用任意的參數列表 和 返回任意的返回值, 固然調用了 不匹配 的 參數列表 就 會發生 錯誤, 可能致使 程序 崩潰, 這個 特性 是用在 C 中間代碼 裏, 不建議 開發人員 使用 。
對於 聲明瞭 函數簽名 的 函數指針, 仍然 會 檢查 調用的參數列表 及 返回值 是否 符合 函數簽名(指針類型), 開發人員 應使用 這種方式, 保證 安全性 。
2 爲了便於實現一些 動態特性 和 對 本地代碼 訪問 的 靈活性, InnerC 支持 用 函數指針 調用 動態的參數列表, 參數列表 是 一個 數組, 相似 .Net / C# 的 反射, 把 參數 放在 數組 裏 傳給 MethodInfo.Invoke( object[] args ) 方法 。
初步構想 能夠 增長一個 invoke 關鍵字, 能夠用於 函數指針 的 函數調用, 好比:
void * funcPtr ;
void * args ;
……
( * funcPtr ) ( invoke args ) ; // 調用 funcPtr 指向 的 函數, 參數列表 是 args
3 新增 casif 關鍵字 以 支持 casif 語句 。
casif 語句 相似 if 語句, 但 判斷條件 是 經過 CMPXCHG 指令 實現的 CAS 原子操做, CAS 全稱 「Compare and Swap」 。
casif 語句 格式 以下:
casif ( 參數1, 參數2, 參數3 )
{
語句塊 1
}
else
{
語句塊 2
}
參數1 是一個 變量 或者 常量, 參數2 是 一個 指針, 參數3 是 一個 變量 或者 常量,
當 參數1 和 參數2 指向 的 值 相等 時, 把 參數3 的 值 複製到 參數2 指向 的 存儲單元, 並認爲 判斷條件 成立, 執行 語句塊 1 。
不然 認爲 判斷條件 不成立, 執行 語句塊 2 。
其實 上面說的 用 CMPXCHG 指令 實現 IL Lock 的 作法 還有一點問題, 其實 不須要 向 對象 的 lock 字段 寫入 當前線程 ID, 只要 寫入 1 就能夠, 1 表示 對象 被 鎖定, 0 表示 對象 未被鎖定 。
這樣 邏輯 就 更 簡化了 。
對 引用表 對象表 堆表 的 lock 都會 統一使用 IL Lock 。
暫時先寫到這裏, ILBC 目前計劃 發展 2 門 高級語言, D# 和 c3 , c3 由 一位 網友 提出, 參考《c3 語言草案》 https://note.youdao.com/ynoteshare1/index.html?id=bec52576b45ec0d918a95f75db0ea68e&type=note#/ 。
內容有點多, 因此後面的內容放到了 《ILBC 規範 2》 http://www.javashuo.com/article/p-uqmiarbb-g.html 。