関数の定義における落とし穴(出題編) - 野良C++erの雑記帳
に対する解答例です。
その1
まず、ここ。
for _, x in ipairs(t1) do
ipairs は標準関数、つまりグローバル環境に定義された値です。
なので ipairs(t) は、おおよそ _G["ipairs"] のように解釈され、
この文が実行されるたび、グローバル環境へのテーブルアクセスが生じることになります。
一般に Lua におけるテーブルアクセスは非常に高速であり、
普通はそれが速度上の問題になることは殆どありえないわけですが、
それでも、毎回テーブルをアクセスするのは、明らかな時間の無駄です。
また、無駄という他に、もし append を定義した後に、どっかで
ipairs = nil
といったコードが呼ばれた場合、この append は最早 使えなくなってしまいます。
というわけで、 ipairs はローカル変数にキャッシュしましょう。
do local ipairs = ipairs -- new! function append( t, t1, ... ) -- 実装は変わらないので略 -- ... end end
その2
同様に、関数末尾の再帰部分でも、グローバル環境へのアクセスが起こっています。
return append( t, ... )
それが証拠に、以下のようなコードは動きません。
http://ideone.com/ayPlQ
-- append の定義(略) -- append を別の関数にする appendtable = append function append( s1, s2 ) return s1..s2 end -- テスト local t = { 1, 2 } appendtable( t, { 3 }, {}, { 4, 5 }, { 6, 7, 8, 9, 10 } ) -- エラー!
この問題に対処するには、まず local で関数を定義した後、グローバルに公開します。
この時、 local で定義した関数の名前と、グローバルで公開したい関数の名前が
一致している場合、うまくグローバルに公開することが出来ないので注意します。
というわけで、さっきの ipairs と合わせて、以下のように書き直しましょう。
do local ipairs = ipairs -- 再帰関数は local で定義して local function append_( t, t1, ... ) if t1 == nil then return end local n = #t for _, x in ipairs(t1) do n = n + 1 t[n] = x end return append_( t, ... ) end -- 公開 append = append_ end
これで安全になり、またテーブルアクセスが減って高速化出来ます。
その3
このコードでは、再帰のたびに、毎回 #t を呼び出して t のサイズを取得しています。
local n = #t
この呼び出しは、確かに、最初の関数呼び出しの時には必要です。
なぜなら、テーブルに対する # 演算はテーブルサイズ N に対して大体 Ο(lgN) であり、
「単純にテーブルサイズを取得する」ならば、最も妥当な手段だからです。
しかし、よく見ると、最初の呼び出し以降は、既に t のサイズは分かっていますね?
そう、関数終了時点でローカル変数 n に格納された値が、それです。
というわけで、 n を再利用すれば、その分だけ高速化出来ます。
最終的に出来たコードは、以下の通りになるでしょう:
do -- 使用するグローバル変数をローカル変数に格納 local ipairs = ipairs -- 再帰関数は local で定義する local function append_( t, n, t1, ... ) if t1 == nil then return end for _, x in ipairs(t1) do n = n + 1 t[n] = x end return append_( t, n, ... ) end -- 公開する関数は local で定義した再帰関数を単に呼び出すだけ function append( t, ... ) return append_( t, #t, ... ) end end -- テスト local t = { 1, 2 } append( t, { 3 }, {}, { 4, 5 }, { 6, 7, 8, 9, 10 } ) print( table.concat( t, ", " ) )
なお、再帰での処理をやめてループでの処理に書き換えれば、
再帰にかかるコスト(末尾再帰は goto ですが、それでもゼロではない)を削減できますし、
さっきの配列長取得問題も自然に解決できますが、それは僕はいちいち書かないことにします。
結論
おまけ
実は、このコードには更に最適化の余地があったりします。
それはテーブルのバッファ確保の問題で、
もし Lua のテーブルに、配列部分のバッファサイズを伸長する関数が有った場合、
その関数を仮に table.reserve(t, n) とすると、
do local ipairs = ipairs local function append_( t, n, t1, ... ) if t1 == nil then return end table.reserve( t, n + #t1 ) -- 配列バッファを確保する for _, x in ipairs(t1) do n = n + 1 t[n] = x end return append_( t, n, ... ) end function append( t, ... ) return append_( t, #t, ... ) end end
このように、バッファ追記の前に呼び出すことで、更に最適化することが出来ます。
なぜそうすることが最適化になるかは、興味があったら調べてみてください。
問題は、そのような関数が Lua に存在していない点で、
かろうじて API ではテーブル製作時に配列バッファ長を指定できますが、
既に構築されたテーブルに対してバッファサイズを伸長するような動作は、今の Lua では行えません。
勿論、そんなものを明示的に呼び出す必要は、無いといえば無い、というか、
基本的に言語側に勝手によろしくやってもらった方が効率的な場合も多いのですが、
今回のような「言語側からは分からない情報」を使って最適化したい場合も多いので、
もし深い理由があってそうなっているのでなければ、 reserve 欲しいなーと思う次第です。
*1:詳しくはhttp://ideone.com/LlnCi。val欲しい。
*2:この例では n