ObjectScript 0.99-vm3-新しい高速仮想マシンと新機能。
いくつかの演算子は
clone, numberof
れます。たとえば、
clone, numberof
などは関数に置き換えられます。 関数の最後の値が自動的に返されます。 オブジェクトのメンバーにアクセスするための短いエントリ-
@varname
、関数を宣言するための新しい短い構文などを追加しました。 その他
パート1. ObjectScript仮想マシンを登録する
従来、プログラミング言語はスタックされた仮想マシン(Java、.Net、ActionScript、およびその他)を使用します。 このようなVMは、
push, pop
コマンドを使用して値をスタックに追加し、必要に応じて削除します。 数学演算を計算するとき、スタックの最上部にある2つの値が引数として使用され、演算の結果が引数を置き換えます。 たとえば、次のコード:
i = j + k
スタック上のVMはコマンドにコンパイルされます:
push_local j - push_local k - operator + - set_local i - pop -
ObjectScriptも以前はスタックVMを使用していましたが、現在はそうではありません。 仮想マシンは完全に書き直されました。 ObjectScriptは現在、レジスタベースの仮想マシンを使用し、上記の例は1つのコマンドにコンパイルされます。
add i, j, k
デバッグ情報のObjectScriptは、このコマンドを次のように示します。
var i = var j [operator +] var k
アクションを実行するためのコマンド数の違い(スタックVMに5つ、レジスター1に1つ)はすぐにわかります。 実際、スタックVMコードにはレジスター命令よりも多くの命令があり、VM命令の数は大きく異なります。 たとえば、ObjectScript VM2の以前のスタックバージョンには、36個のコマンドで現在のVM3に対して111個のコマンドがありました。 コマンドが少ないほど、VMが単純になり、保守と最適化が容易になり、必要に応じて、より簡単なJIT(マシンコードへのコンパイル)を含むさまざまなプログラミング言語での実装が容易になります。
変数の値を変更する例:
i, j = j, i
ObjectScriptは、このコードを3つのコマンドにコンパイルします。
move: # = var j move: var j = var i move: var i = #
ここで、#は一時変数で大文字と小文字を使用することを意味します。 コマンドを見ると、変数の値の変更が最適な方法で発生し、次のコードと完全に同等であることがわかります。
temp = j j = i i = temp
仮想マシンの登録とは何ですか?
レジスタ仮想マシンは、値のスタックではなくレジスタ内の値で機能します。 レジスターは、アクセス可能なときに常に存在する一時的なローカル変数です。
これにより、登録VMの最初のハイライトが非表示になります。 レジスタは常に読み取りと書き込みの準備ができており、VMはそのような変数(レジスタ)の存在、その割り当てと破棄について不要なチェックを行う必要はありません。
2番目のハイライトは、プッシュコマンドとポップコマンドがないことです。 そのようなコマンドは、VMにとって非常に高価です。 スタックを変更してその最上位を制御する必要がある場合、スタックのオーバーフローと実装の必要性を常にチェックする必要があります。 スタックを操作するためのコマンドを取り除くと、スタック上の一時的なシェルターをバイパスして、操作の結果を結果の変数にすぐに保存することが可能になります。
3番目のハイライトは、JITを含め、はるかに少ないコマンド数、より簡単なVMの実装です。
上記の例からわかるように、スタックされたVMのコマンドはアトミックアクションであり、VMの登録のコマンドよりも単純であり、占有するスペースが少なくなります。 たとえば、
push
コマンドは1バイトでエンコードでき、次のコマンドは次のバイトでエンコードできます。 これにはプラスとマイナスがあります。
スタックVMの利点は、命令がより少ないバイトでエンコードされ、結果のコードが占有するスペースが少なくなることです。
マイナスは、コマンドの一部が、例えば
push_double, jump
などのように1バイトでエンコードすることがまだ不可能であることです。そのようなコマンドは、追加の引数を必要とします。 byte-int32)トランジション用。 なぜなら スタックVM内のコマンドは互いに続き、どのようにも整列されないため、2進数またはオフセットint32は、任意のアドレスのメモリ内に存在できます。 プロセッサのアーキテクチャに精通している人は、すぐにこれに注意を払うでしょう。なぜなら、 このような引数は、単一のプロセッサコマンドで読み取ることはできません。 たとえば、メモリ内の奇数アドレスで
dword
を読み取ろうとすると、ARMアーキテクチャで例外が発生し、プログラムはエラーで終了します。 アーキテクトが異なれば、エラーが発生するか、パフォーマンスが劇的に低下する可能性があります。 したがって、バイトのストリームからこのようなマルチバイト引数を読み取るには、バイトごとに必要なだけで、オフセットのビット演算を使用して最終引数を形成します。 これにより、VMの速度がわずかに低下します。
レジスターVMでは、多くのコマンドに3つの引数があり、4バイトを(引数とともに)占有します。 上記の問題を解決するために、ObjectScriptのすべてのコマンドは整列されており、実際には使用されていないビットがあっても常に4バイトを占有します。 たとえば、ObjectScriptの
move
コマンドには2つの引数があります。 ただし、この場合でも、ObjectScriptは次のようになります。 多くの場合、
move
コマンドは互いに続き、2つの連続したレジスタを埋めます。 この場合、ObjectScript(最適化段階)は、2つの
move
ではなく1つの
move2
コマンドを生成します。
move2 R, A, B
これは、2つの
move
コマンドとして実行されます。
move R, A move R+1, B
この場合、
move2
は3つの引数があり、4バイトを完全に使用します。 コマンドの調整により、1つのプロセッサコマンドでコマンドを読み取ることができるため、VMの速度が向上します。
他の利点と併せて、新しいレジスタVM3を備えたObjectScriptは、以前のスタックVM2よりも3倍高速になりました。
レジスタVMはどのように機能し、レジスタはどこから来たのか
ObjectScriptコンパイラは、プログラマが関数で使用するローカル変数だけでなく、すべての一時変数(コンパイラによって設定された)に関する情報も収集します。 したがって、レジスタは関数内の一時的なローカル変数です。 VMはローカル変数とレジスタを指し、インデックスでは完全に同等です。 そのため、同じVMコマンドでローカル変数とレジスタを使用でき、操作の結果を一時変数をバイパスしてローカル変数にすぐに保存できます。
たとえば、次のコード:
k = i - j*k / (x + y - z*i / j) + i
ObjectScriptのコンパイル結果:
# (59) = var j (5) [operator *] var k (6) # (60) = var x (7) [operator +] var y (8) # (61) = var z (9) [operator *] var i (4) # (61) = # (61) [operator /] var j (5) # (60) = # (60) [operator -] # (61) # (59) = # (59) [operator /] # (60) # (58) = var i (4) [operator -] # (59) var k (10) = # (58) [operator +] var i (4)
括弧内のインデックスは、ローカル変数またはレジスタが配置されている場所です(インデックスはコンパイル時に計算されます)。 すべてのローカル変数(一時変数-レジスタを含む)についてコンパイラによってコンパイルされた情報により、この機能を実行するために必要な最大スタックサイズを計算できます。 関数が起動すると、スタックの最上位が保存され、コンパイラーによって決定されたスタックサイズが予約されます。 レジスタ(ローカル変数)へのインデックスは、格納されたスタックの最上部からの相対オフセットとして使用されます。これにより、難なく再帰的な関数呼び出しを行うことができます。
レジスターVMでの関数呼び出し
関数を呼び出すために、ObjectScriptは、関数の引数を配置するレジスタの連続シーケンスを予約します。 次に、関数自体が、最初の引数の開始位置(シーケンスの開始)およびシーケンス内の値の数に関する情報とともに呼び出されます。 最初の引数は新しい関数のスタックの先頭になり、引数は既にスタック内の必要なオフセットにあり、ローカル変数になります。 3つのパラメーターを使用して関数を呼び出すときの表示例:
func(i, k - x*y * (z + i), j*k)
ObjectScriptのコンパイル結果:
begin call move: # (59) = var func (11) move: # (60) = const null (-1) move: # (61) = var i (4) # (63) = var x (7) [operator *] var y (8) # (64) = var z (9) [operator +] var i (4) # (63) = # (63) [operator *] # (64) # (62) = var k (10) [operator -] # (63) # (63) = var j (5) [operator *] var k (10) end call: start 59, params 5
関数は、インデックス59から始まる5つの値のシーケンスで始まります。最初の2つの値は、ユーティリティパラメータ、つまり59-関数自体、60-関数の場合(この場合はnull)です。
move: # (59) = var func (11) move: # (60) = const null (-1)
次に、パラメータ自体が転送され、レジスタ61〜63に配置されます。最初のパラメータは
i
変数のみで、レジスタ61にコピーされます。
move: # (61) = var i (4)
2番目のパラメーターは、数学演算
k - x*y * (z + i)
の結果であり、レジスタ62に格納されます。
# (63) = var x (7) [operator *] var y (8) # (64) = var z (9) [operator +] var i (4) # (63) = # (63) [operator *] # (64) # (62) = var k (10) [operator -] # (63)
3番目のパラメーター(
j*k
)はレジスター63に保管されます。
# (63) = var j (5) [operator *] var k (10)
これでシーケンスの準備が完了し、関数を呼び出すことができます。
使用される登録仮想マシンはどこですか
Register VMはLua 5.0以降でも使用され、quakec(quake 1および多数のポート用のスクリプトプログラミング言語がありました)には、Java用の登録VM(Dalvik VMなど)があります。 正式には、マシンコードにコンパイルされるC ++ / Cおよびその他の言語のプログラムは、内部操作用のレジスタモデルと関数呼び出し用のスタックを使用します。
合計
新しいRegister ObjectScript Virtual Machine (VM3)は、以前のスタックVM2の3分の1だけ高速で、VM2の111個に対して36個のコマンドしかありません。 少数のチームがVMを大幅に簡素化し、必要に応じてJITを実装する機会を増やします。
スタックはローカル関数変数に使用され、再帰呼び出しの可能性を提供します。 必要なスタックサイズは、関数が呼び出されたときに一度予約されます。VM3コマンド自体はスタックを実装しません。
スタックはObjectScript APIでも使用され、カスタムコードとの統合を簡素化します。 ObjectScript APIは変更されず、以前のバージョンと完全に互換性があります。
継続
次のパートでは、ObjectScriptに登場した他の技術革新について説明します。たとえば、階乗関数は次のように記述できます。
print "factorial(10) = " .. {|a| a <= 1 ? 1 : a * _F(a-1)}(10)
出力されます:
factorial(10) = 3628800
他の多くの その他
ObjectScriptに関するその他の関連記事: