不知道大家有沒有思考過「強型別」語言跟「弱型別」語言的差異在哪邊,在強型別語言中每一種變數的「型別」都是固定的,也就是說當我們「編譯」的時候就會知道「類型」不對而回報錯誤,而弱型別的語言則到等到執行的時候才會發現「型別不對」這件事情。
舉例來說,我們在 C 語言定義了一個 uint16_t
的變數,假設不檢查型別直接當作 uint8_t
來使用的話,大致上就會發生類似下面的情況。
// uint16_t v = 2 的資料 (16bit ~ 2byte)
// 0000 0000 0000 0010
// 當成 unit8_t 處理時
// 0000 0000
因為 uint8_t
比較小,所以我們會遺失了後面的 000 0010
這個資訊,也因此編譯器會警告我們「型別不對」假設我們透過某些方式(像是指標)繞開了這個限制,那麼編譯器就不會知道有問題,同時出現的就會是我們「讀取到奇怪的數值」或者發生 Segment fault 錯誤(可能是讀取不到或者超出合理的範圍)
但是在 Ruby 這類語言裡面,我們會用一個統一的「箱子」把它包裝起來,因為我們從是用「足夠的大小」把他封裝,也因此就不會出現太小的問題,相反的就會浪費一部分的記憶體在記錄這些資訊。這樣的缺點是當 CPU 在處理的時候,一次性就不能處理太多的變數,速度就會相對的慢下來。
這幾年比較知名的例子應該是 PHP7 在效能上的改進中,將變數結構的大小做了改善進而獲得了一定程度的效能提升。我在網路上找了一篇有提到這件事情的文章,而這件事情是在幾年前聽台灣 PHP 大神 c9s 的分享知道的,而關於 CPU 處理速度的問題通常可以在一些遊戲開發的書籍發現,前陣子 jserv 的課程說明也有提到 Google 在 ML 領域如何利用這個方式加速。
mruby 裡面對於如何封裝
mrb_value
有三種處理方式,並且用boxing
來稱呼這個處理。
我們來看一下沒有做 boxing 版本的 mrb_value
大致上是如何的
union mrb_value_union {
#ifndef MRB_WITHOUT_FLOAT
mrb_float f;
#endif
void *p;
mrb_int i;
mrb_sym sym;
};
typedef struct mrb_value {
union mrb_value_union value;
enum mrb_vtype tt;
} mrb_value;
上面是節錄自 mruby 原始碼中 mruby/boxing_no.h
這個檔案的內容,結構其實非常的簡單。主要就是 tt
和 value
兩個資訊存在裡面,而 tt
用來區分變數的類型,而 value
則使用 union
來儲存變數的數值。
union 的概念有點抽象,因為他會把設定在 union 的資料重疊的儲存,簡單來說當下只會有一個數值會是對的,剛好跟
tt
搭配要讀取哪一個數值才會正常。
在 mrb_value_union
裡面的結構算好理解,基本上就是指標、整數跟符號的資訊,當我們要讀取這個變數的數值時基本上就會這樣處理。
// mrb_value v;
switch(v.tt) {
case MRB_TT_TRUE:
return 1;
case MRB_TT_FALSE:
return 0;
case MRB_TT_OBJECT:
return v.value.p;
// ...
}
不過因為型別差異的關係不太可能這樣處理,而是先用 MRB_NIL_P(v)
之類的檢查是什麼類型再實際上處理,而這也是因為我們經常會看到說要做「型別推導」的原因之一,在我們透過 v.tt
判斷之前都是一個未知的數值,直到我們去檢查才能得到對應的數值。
如此一來跟強型別語言比起來自然會相對的慢,但是在開發上來說可能就會因為不用耗費心力在思考型別的設計與規劃而能快速製作原型。
其實效能這類問題很多時候並不是好或不好,而是要從當下的情境來評估,像是商業上的問題比起效能可能更需要「快速的開發」已成熟的產品來說更需要讓團隊能更好協作的「可讀性」而有規模的服務就需要「效能」來減少成本,但困難的地方是前面的條件大多是「同時出現的」也因此我們需要的是評估「現在這部分」該怎麼處理,而不是單純的去追求某一種情況的極致。
首先新增 src/value.h
然後將 mrb_value
的定義加入
// src/value.h
#ifndef _IRON_VALUE_H_
#define _IRON_VALUE_H_
enum mrb_vtype {
MRB_TT_FALSE = 0,
MRB_TT_TRUE,
MRB_TT_FIXNUM,
MRB_TT_STRING
};
typedef struct mrb_value {
union {
void *p;
int i;
} value;
enum mrb_vtype tt;
} mrb_value;
#endif
然後我們針對 mrb_callinfo
做一些調整,將 int*
改為 mrb_value*
來儲存,首先把 src/iron.h
和 src/iron.c
的一些定義改正。
// src/iron.h
// ...
#include "value.h"
// ...
typedef struct mrb_callinfo {
int argc;
mrb_value* argv;
}
// ...
IRON_API mrb_value* mrb_get_argv(mrb_state* mrb);
// src/iron.c
IRON_API
mrb_value* mrb_get_argv(mrb_state* mrb) {
return mrb->ci->argv;
}
接著修正 src/vm.c
的實作,將原本製作的 int argv[c]
改為 mrb_value argv[c]
陣列。
// src/vm.c
// ...
mrb_value argv[c];
for(int i = 1; i <= c; i++) {
mrb_value v = { .value.i = reg[a + i], .tt = MRB_TT_FIXNUM };
argv[i - 1] = v;
}
// ...
這邊我們重新製作了 mrb_value
並且將它存放到 argv
陣列裡面,最後只要在 src/main.c
自訂的 mrb_puts
方法裡面修正寫法就可以改為由 mrb_value
讀取到數值。
// src/main.c
void mrb_puts(mrb_state* mrb) {
int argc = mrb_get_argc(mrb);
mrb_value* argv = mrb_get_argv(mrb);
for(int i = 0; i < argc; i++) {
printf("%d\n", argv[i].value.i);
}
}
如此一來我們基本上就將變數「部分導入」到了 Ruby VM 後面會在依照需求逐步的修改取代原本直接儲存數值的處理,改為統一由 mrb_value
處理變數。
下一篇我們會加入一些巨集輔助我們設定和處理變數,然後我們就要將成果透過 PlatformIO 放到微控制器上面運行了。