エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

フォントデータを ETS で保存する

前回の続きです。

前回は BDF ファイルをパースして読み込む話をしました。

そして、BDF ファイルは単純なテキストファイルだし、Nerves アプリケーションへそのまま持っていっても大丈夫だろう、と高を括っていたのですが。

Raspberry Pi ZERO W の非力さを甘くみていました。

起動時に BDF ファイルを読み込ませるようにしたら、電源を入れてもなかなか入力に反応しない。 何かを壊したかとあせりもしたのですが、結局テキストのパースに時間がかかっている様子でした。

一旦読み込み終えてしまえば、あとはメモリ上のアクセスのみになるので、その後の動作には影響しません。 しかし電源を入れてから使えるようになるまで時間がかかるのは問題です。 そのため、読み込んだデータを別の形式で保存しておきすぐに読み出せるようにできる方法を模索しました。

せっかくなら文字コードをキーにアクセスできるように、key-value ストレージのようなものでなにか扱いやすいもの。

Hex も検索してみたのですが。 よく考えてみればあるではないですか、標準装備のストレージが。

ETS です。

www.erlang.org elixirschool.com

どのように利用するとデータが扱いやすくなるのか、いくつか格納方法を変えて試してみました。

今回もフォントデータには 16 ドットの東雲フォントを利用しています。 ファイルサイズは約 1.1 M バイト。

-rw-r--r--@ 1 matsumotoeiji  staff  1135668  9 15  2004 shnmk16.bdf

構造体で保存する

Erlang Term Storage の名の通り、Erlang Term であればなんでも格納できるとあって、まずは構造体をそのまま格納。

BDF モジュールは前回の記事で登場したモジュールです。

{:ok, fonts} = BDF.load("shinonome-0.9.11/bdf/shnmk16.bdf")

table = :ets.new(:shnmk16, [:set])

Enum.each(fonts, fn font ->
  :ets.insert(table, {font.encoding, font})
end)

:ets.tab2file(table, ~c"priv/fonts/shnmk16.ets")

できたファイルのサイズは約 2 M バイト。

$ mix run bdf2ets-1.exs
$ ls -l priv/fonts/shnmk16.ets
-rw-r--r--  1 matsumotoeiji  staff  2080992  2 27 19:44 priv/fonts/shnmk16.ets

tab2file/2 で書き出したデータは、file2tab/1 で簡単に復元でき、lookup/2 で検索できます。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.513635613.2690514945.38635>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078,
   %BDF.Font{
  encoding: 0x4E6E,
  dwidth: %BDF.Font.DWIDTH{dwx0: 16, dwy0: 0},
  bbx: %BDF.Font.BBX{bbw: 16, bbh: 16, bbxoff0x: 0, bbyoff0y: -2},
  bitmap: [0x0000, 0x3FFC, 0x0100, 0x7FFE, 0x4102, 0x7D7A, 0x4F3E, 0x0000, 0x1FF8, 0x0000, 0x7FFE, 0x0248, 0x1248, 0x0A50, 0x7FFE, 0x0000]
}
}
]

ただしこれは BDF モジュールが定義されている環境のばあい。

モジュールが定義されていない環境で読み込んでみると。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.217574499.542769155.19734>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078,
   %{
     encoding: 20078,
     __struct__: BDF.Font,
     dwidth: %{__struct__: BDF.Font.DWIDTH, dwx0: 16, dwy0: 0},
     bbx: %{
       __struct__: BDF.Font.BBX,
       bbh: 16,
       bbw: 16,
       bbxoff0x: 0,
       bbyoff0y: -2
     },
     bitmap: [0, 16380, 256, 32766, 16642, 32122, 20286, 0, 8184, 0, 32766, 584,
      4680, 2640, 32766, 0]
   }}
]

読み込めないわけではないものの、構造体が定義されていないので、内部構造が丸見えになったマップとして扱われています。

タプルで保存する

一度構造体に格納したものを、ただのタプルにしてしまうのもどうかと思いましたが。 内容の単純さや利用シーンを考えるとタプルでもさほど不便はないかと思い直すなど。

{:ok, fonts} = BDF.load("shinonome-0.9.11/bdf/shnmk16.bdf")

table = :ets.new(:shnmk16, [:set])

Enum.each(fonts, fn font ->
  :ets.insert(
    table,
    {
      font.encoding,
      font.dwidth.dwx0,
      font.dwidth.dwy0,
      font.bbx.bbw,
      font.bbx.bbh,
      font.bbx.bbxoff0x,
      font.bbx.bbyoff0y,
      font.bitmap
    }
  )
end)

:ets.tab2file(table, ~c"priv/fonts/shnmk16.ets")
$ mix run bdf2ets-2.exs
$ ls -al priv/fonts/shnmk16.ets
-rw-r--r--  1 matsumotoeiji  staff  767103  2 27 19:52 priv/fonts/shnmk16.ets

約 767 k バイト。構造体で保存した時の 3 分の 1 程度。

構造から名前が失われ、読みだせばたただの数字の羅列。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.2786847046.275644418.110991>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078, 16, 0, 16, 16, 0, -2,
   [0, 16380, 256, 32766, 16642, 32122, 20286, 0, 8184, 0, 32766, 584, 4680,
    2640, 32766, 0]}
]

しかしタプルの中の位置がわかっていればよいので、これでも構わない気もしてくる。

バイナリで保存する

フォントデータはビット操作で加工したりするわけだし、いっそのこと全体をバイナリにしてしまっても構わないのでは? と思ってやってみました。

ETS に格納できるのは先頭の要素をキーとしたタプルのみなので、文字コードだけをそのままに、残りのデータを一つのバイナリに詰め込み。

{:ok, fonts} = BDF.load("shinonome-0.9.11/bdf/shnmk16.bdf")

table = :ets.new(:shnmk16, [:set])

Enum.each(fonts, fn font ->
  bitmap = for line <- font.bitmap, into: <<>>, do: <<line::size(font.bbx.bbw)>>

  :ets.insert(
    table,
    {
      font.encoding,
      <<
        font.dwidth.dwx0::8,
        font.dwidth.dwy0::8,
        font.bbx.bbw::8,
        font.bbx.bbh::8,
        font.bbx.bbxoff0x::8,
        font.bbx.bbyoff0y::8,
        bitmap::binary
      >>
    }
  )
end)

:ets.tab2file(table, ~c"priv/fonts/shnmk16.ets")
$ mix run bdf2ets-3.exs
$ ls -l priv/fonts/shnmk16.ets
-rw-r--r--  1 matsumotoeiji  staff  406276  2 27 20:01 priv/fonts/shnmk16.ets

約 406 k バイト。タプルで保存したときの半分強くらい。構造体で保存したときの 5 分の 1 くらい。 コンパクトにはなりました。

読みだせば、値と値の区切りも喪失した、タプルで格納したときよりもさらに面妖な状態。

iex(1)> {:ok, table} = :ets.file2tab(~c"priv/fonts/shnmk16.ets")
{:ok, #Reference<0.3552940925.8519682.187654>}
iex(2)> :ets.lookup(table, 0x4E6E)
[
  {20078,
   <<16, 0, 16, 16, 0, 254, 0, 0, 63, 252, 1, 0, 127, 254, 65, 2, 125, 122, 79,
     62, 0, 0, 31, 248, 0, 0, 127, 254, 2, 72, 18, 72, 10, 80, 127, 254, 0, 0>>}
]

ベンチマークは未実施

ライブラリの関数を使ってデータを一括で読み込むので時間もかからず、格納のしかたによってファイルサイズを小さくできることもわかりました。 が。 利用時の効率はまだ測れていません。

Elixir がバイナリ操作を得意としているとはいえ、構造を持つタプルの操作と比べると、バイナリの操作には余分に時間がかかるのではと想像します。 その差は微々たるものなのでしょうが、Raspberry Pi でテキストファイルを読み込ませたら存外時間が取られたという前科があるので、油断はできません。 あるいは、表示デバイスとの IO の方の影響の方がずっと大きくて、バイナリの操作にかかる時間は気にするほどのことでないのかもしれません。

こればかりはベンチマークするしかないので、もう少し調べてみたいと思います。