Lua 5.4の新機能
原文は Ben Hoyt により に公開されました。 このブログを購読する
Luaバージョン5.4は6月末にリリースされた。1993年の誕生以来、軽量スクリプト言語としては15番目のメジャーバージョンとなる。5.4での新機能として、ガベージコレクタに世代別モードが追加され、短命なアロケーションを大量に行うプログラムでの性能が向上している。言語自体もローカル変数の「アトリビュート」に対応し、開発者は変数を定数(const)として、あるいはリソースをクローズ可能(close)としてマークできるようになった。5.3からの大幅な性能向上に加え、多数の細かな変更も含まれている。
Luaは他のアプリケーションへの組み込みに最適化されたプログラミング言語で、RedisやAdobe Lightroomなどが著名な採用例だ。多くのコンピュータゲームのスクリプト言語としても使われており、World of WarcraftやAngry Birdsといったビッグタイトルも含まれる。2009年のゲーム業界調査では、Luaが最も使われているスクリプト言語だった。Luaが組み込みに適している理由の一つは、その小ささにある。最もシンプルなアプリケーションでさえ数メガバイトのダウンロードが当たり前となった現在でも、Lua 5.4の配布物全体(ソースとドキュメントを含む)は349KBのアーカイブに収まる。デフォルト設定でLuaインタプリタをビルドするには、開発者はmakeと入力してコンパイルを5秒ほど待つだけでよく、結果として200〜300KBの自己完結したバイナリが得られる。
メジャーバージョンは特に決まったリリースサイクルはなく、数年ごとにリリースされる。1つ前のメジャーバージョンである5.3は5年以上前の2015年1月にリリースされ、独立した整数型(それまでLuaは浮動小数点数のみを用いていた)、ビット演算子、基本的なUTF-8ライブラリ、そして多くの細かな機能が追加された。
言語の変更
Lua 5.4の興味深い新機能の一つが、ローカル変数の「アトリビュート」の追加だ。ローカル(ブロックスコープ)変数を宣言する際に、変数名の後に<const>や<close>を付けることで、そのアトリビュートを与えられる。constアトリビュートは分かりやすい。C言語のconstと同様に、宣言時の初期化の後でその変数への再代入ができないことを意味する。constアトリビュートはデータ構造自体を不変にするわけではない。テーブルをconst変数に格納した場合でも、そのテーブルの中身を変更することは妨げられないが、変数名自体への再代入はできない。constアトリビュートはコンパイル時の安全性をわずかに高めてくれる。定数に誤って再代入しようとすると、コンパイラがエラーを出すからだ。
do
local x <const> = 42
x = x+1
end
-- ERROR: attempt to assign to const variable 'x'(同様に風変わりな構文ではあるが)おそらくより有用なのがcloseアトリビュートだ。これは変数がスコープを抜ける際に、そのオブジェクトの__close()「メタメソッド」を呼び出すようLuaに指示する。C++のRAIIやPythonのwith文と同様に、メモリの解放、ファイルのクローズ、その他のリソースの確実な終了を決定的なタイミングで行うための仕組みだ。例えば、組み込み関数io.open()が返すファイルオブジェクトは<close>とともに使える。
do
local f <close> = io.open("/etc/fstab", "r")
-- read from file 'f'
end
-- file is automatically closed here<close>アトリビュートはユーザー定義のオブジェクトでも使える。
function new_thing()
local thing = {}
setmetatable(thing, {
__close = function()
print("thing closed")
end
})
return thing
end
do
local x <close> = new_thing()
print("use thing")
end
-- "thing closed" is printed here after "use thing"従来、開発者はこの目的のために__gc()メタメソッドを使わなければならなかったが、これはブロックの終了時ではなく、オブジェクトがガベージコレクトされるタイミングで、しかもいくらか時間が経ってから呼び出されるため、決定的ではない。
世代別GC
バージョン5.4では、オブジェクトの寿命が通常短いタイプのプログラムで性能が向上する、新しい世代別ガベージコレクション(GC)モードも導入された。世代別GCは「ほとんどのオブジェクトは若くして死ぬ」という観察に基づいており、「若い」オブジェクトを頻繁に走査して参照されなくなったものを解放する一方で、1回以上のGCパスを経ても参照され続けているような古いオブジェクトについては走査頻度を落とす。興味深いことに、Luaの生みの親の一人であるRoberto Ierusalimschyは2017年に次のように述べている。
Luaは5.1以来インクリメンタルなガベージコレクタを備えている。世代別コレクタは5.2で実験的に導入され、その後5.3で削除されたものだ。それが5.4で再び登場するが、今度はおそらく定着することになるだろう。
Ierusalimschyは2019年に講演を行い(PDFスライドとYouTube動画)、インクリメンタルGCがどのように動作するのか、5.2の世代別GCがなぜあまりうまく機能しなかったのか、そしてLuaチームがそれを何に置き換えたのかについて詳しく説明している。5.2のバージョンでは、オブジェクトは1回のGCサイクル(コレクタのパス)を生き延びるだけで「古い」とみなされたが、5.4では2回のGCサイクルを生き延びる必要があり、これは実際のLuaプログラムをより正確にモデル化している。2サイクルの方式は実装がより複雑になるが、多くのプログラムでGC性能が向上する――ただしすべてではない。Ierusalimschyは、大きなデータ構造を構築するプログラムでは恩恵がないと指摘している。おそらくそのため、Luaチームはデフォルトを変更しなかった。5.4でもデフォルトは依然としてインクリメンタルコレクタであり、世代別GCを有効にするには開発者がプログラムに「collectgarbage("generational")」を追加する必要がある。
lua-lメーリングリストで、Gé Weijersは世代別GCの「マイナーコレクション」(若いオブジェクトを回収するための頻繁なGCパス)が、新しい<close>機能(以前は「toclose」と呼ばれていた)とどのように関わるかを説明している。
5.4のガベージコレクタは世代別モードを実装している。オブジェクトがマイナーコレクションを生き延びると、到達不能になった後でその__gcメタメソッドが呼び出されるまでに非常に長い時間がかかることがある。特に、プログラムが主に短命なオブジェクトを生成する場合はそうだ。そのため、__gcは手軽なRAII代替としての有用性が低くなる。
新しい「toclose」機能は、リソースの解放やロックの解除をタイムリーに行う上で、はるかに有用だ。
高速化
5.4のあまり注目されていない特徴の一つに、大幅に高速化されたインタプリタがある。リリースノートでは見過ごされているが、私が64ビットのmacOSマシンでGabriel de Quadros Ligneul氏のLua Benchmarksスイートを使って行ったテストでは、スイートに含まれる11のベンチマーク全体で、バージョン5.4はバージョン5.3より平均40%高速だった。
同様の向上はElmar Klausmeier氏の性能比較でも示されている。確かに、これらはいずれもかなり人工的なベンチマークだ――ゲームエンジンのような場面でLuaを使う場合、グラフィックスや行列演算のような性能が重要なコードは間違いなくCで書かれるだろう。それでも、数値中心のコード(これらのベンチマークのほとんどがそうだ)でこれほどの改善が見られるのは軽視できない。Dibyendu Majumdar氏は2018年にlua-lメーリングリストで、これらの改善の理由の一部を説明している。5.4では、関係する型が数値であるとLuaのパーサが推論できる場合に利用できる、数値演算向けの新しい最適化されたバイトコード命令が追加された。例えばGETIとSETIは、インデックスが定数の整数である場合のテーブル参照に使われる2つの新しい命令だ。
さらに高い性能が必要な人は、Mike Pall氏のLuaJITを利用できる。これはLua 5.1向けのジャストインタイムコンパイラで、標準のLuaインタプリタよりも大幅に高速だ。しかし、LuaJITはバージョン5.1(2006年リリース)以降、Luaの新機能を一切取り込んでいない。それを行うのは、5.2での新しいスコープルールや5.3での新しい整数型を含む、多くの破壊的変更のためにかなりの大仕事になるからだ。このため、Pall氏はLuaチームが行う後方互換性のない変更について声高に批判してきた。
これは単なる些末なエッジケースの問題ではなく、実際に深刻な問題のようだ。Lua Benchmarksスイートのベンチマークのうち2つが5.4で「C stack overflow」エラーで失敗した(5.3では正常に動作する)のだ。そのため、実行前にそれらを除外しなければならなかった。ackとfixpoint-factのベンチマークが失敗するが、おそらく5.4での再帰的な末尾呼び出しの扱いの違いが原因だろう。5.4における互換性のない変更のほとんどは文書化されているが、そのリストの長さは、大きなLuaスクリプトをアップグレードしようとする人々にとって依然としてかなりの苦痛となる可能性がある。RedisやWorld of Warcraftのように長期的な安定性を必要とするツールが、特定の古いバージョンのLua(どちらもバージョン5.1)に固定しているのは、おそらくこのためだと私は推測している。JITコンパイラが存在することや、それ以降の変更が比較的軽微であることを理由に5.1を使い続ける人々がいるなど、コミュニティにはある種の分断があるように見える。
Luaのバージョン間の非互換性は、Luaに統一された標準ライブラリが存在しないという問題にも寄与している可能性があり、LWNは2月にそのことについて書いている。新しいLuaバージョンが出るたびにライブラリ作者が多くの作業を強いられるのであれば、最新の状態に保とうとする意欲は低下しかねない。その結果、新しいLuaバージョンで動作するフォークを誰かが作成したり、単に新しいライブラリを書いたりする可能性が高まる。
細かな変更
大きな変更に加え、Lua 5.4では多くの細かな機能も追加されている。基盤となるCライブラリのrand()関数の代わりにxoshiro256**アルゴリズムを用いた新しい乱数生成器もその一つだ。ファイナライザや__close()メソッドでエラーが発生した際に使われるシンプルな警告システムも新たに設けられた。さらに、「userdata」を持つLuaの値が複数のユーザー値を保持できる機能も追加された(userdataはLua C APIで作成されたメモリブロックへのポインタであり、この機能によりC拡張で作成されたオブジェクトに複数のメモリブロックを関連付けられるようになる)。
セマンティクスにもいくつかの小さな変更があった。forループにおけるラップアラウンドのエッジケースの扱いがわずかに異なったり、整数における文字列から数値への強制変換が調整されたりしている(例えば、"10"+1は5.4では整数の11だが、5.3では浮動小数点数の11.0になる)。
全体として、Luaはその領域(大規模なシステムやアプリケーションへの組み込み)において優れた言語であるように思われる。5.4のリリースは、コアチームによって継続的に改善が行われていることを示している。Luaには明確なロードマップがないため、この早い段階で5.5でどのような変更が計画されているのか、あるいはいつリリースされる可能性が高いのかを知るのは難しい(Lua開発者のPierre Chapuis氏は、次のバージョンはバージョン番号6.0を伴う「非常に大きな影響を与える変更
」になるかもしれないとさえ推測している)。いずれにせよ、5.4の新機能はほとんどのユーザーにとっては比較的軽微なものにとどまるだろうが、性能向上はうれしい成果となるはずだ。
記事をランダムに読む

コメント
ログインしてコメントする