バイナリ形式にするだけでメモリ節約プログラミング

比較条件に関係しない値なら、バイナリにする価値あり

小さい数値を多く扱う場合は、バイナリ(可変長表現)にすると結構小さくできます。

データ量が小さくなれば、DBからの取得速度が向上し、アプリのメモリ上にもより多くのデータを保持できるようになります。

ただ、バイナリ形式のままだと大小比較などの計算ができないので、必要に応じて数値型に戻してあげる必要があります。

コード

今回比較に利用したコードは、githubに上げてあります。

https://github.com/TakiTake/seiseki

数値とバイナリの速度比較

学校の生徒100人毎日テストを5科目受けたときの成績管理用DBを想定

バイナリ形式のDBは、科目カラムに5科目分のデータを保持させます。

テストデータなんで、土日祝日お構いなしにテスト受けてますw

ノーマルなDB

カラム名
日付 text
学籍番号 text
科目1 integer
科目2 integer
科目3 integer
科目4 integer
科目5 integer

バイナリ形式のDB

カラム名
日付 text
学籍番号 text
科目 blob

結果

1年分のデータ

size

normal  1.9MB
binary  1.7MB

insert

user     system      total        real
normal  4.740000  11.920000  16.660000 ( 94.378284)
binary  4.660000  11.920000  16.580000 ( 93.093927)

select

user     system      total        real
normal  0.210000   0.010000   0.220000 (  0.221661)
binary  0.190000   0.010000   0.200000 (  0.201906)
5年分のデータ

size

normal  9.6MB
binary  8.8MB

insert

user     system      total        real
normal 23.860000  58.860000  82.720000 (476.608330)
binary 23.530000  58.400000  81.930000 (507.330652)

select

user     system      total        real
normal  1.190000   0.070000   1.260000 (  1.300539)
binary  0.980000   0.070000   1.050000 (  1.078891)
10年分のデータ

size

normal  19MB
binary  18MB

insert

user     system      total        real
normal 47.350000 118.030000 165.380000 (920.600014)
binary 46.610000 118.380000 164.990000 (935.340207)

select

user     system      total        real
normal  2.140000   0.100000   2.240000 (  2.236974)
binary  1.790000   0.100000   1.890000 (  1.891564)

なぜ、サイズが小さくなるのか?

小さい数値を表すのに、8 byte もいらないよね

環境差分はありますが、自分の環境ではRubyのFixnumは、8 byte でした。

0~255までの数値なら 1byte で表すことができるので、固定長ではなく可変長で数値を扱えば、 7byte 削減できというわけです。

どうやって変換すればいいの?

数列 -> バイナリ文字列

Array#pack のテンプレートに w(BER-compressed integer) を指定するだけです。

http://www.ruby-doc.org/core-1.9.3/Array.html#method-i-pack

バイナリ文字列 -> 数列

String#unpack テンプレートに w(BER-compressed integer) を指定するだけです。

http://www.ruby-doc.org/core-1.9.3/String.html#method-i-unpack

応用編

ただの文字列なので、文字列結合もできます

  [33, 49, 300, 99, 1000].pack('w5') // "!1\x82,c\x87h"
  'abc' + [33, 49, 300, 99, 1000].pack('w5') // "abc!1\x82,c\x87h"

また、こういった文字列処理には、StringScannerが便利です。

http://www.ruby-doc.org/stdlib-1.9.3/libdoc/strscan/rdoc/StringScanner.html

先頭から、3 byte 取得

  str = 'abc' + [33, 49, 300, 99, 1000].pack('w5')
  ss = StringScanner.new(str)
  ss.peek(3) // "abc"

3 byte 分ポインタを進める

  ss.pos += 3

ここから後ろは、可変長なので何byte分取得すればいいかわかりません。

そんなときは、文字列中にbyte数を持たせてあげれば、そこから判断できます。

サイズを含む文字列

  packed = [33, 49, 300, 99, 1000].pack('w5')
  
  # packした文字列のサイズをさらに、packする
  # C(8-bit unsigned)にすることで、この1byteを読めば続く文字列のサイズが分かる
  # 文字列のサイズが、8bitで表しきれない場合は、テンプレートにS, L, Qを適宜使う
  packed_size = [packed.size].pack('C')
  
  str = 'abc' + packed_size + packed
  ss = StringScanner.new(str)
  
  # "abc"の3byte分進める
  ss.pos += 3
  
  # 1byte取得して、unpackすることで保存しておいたサイズを取得
  # unpackは、配列で返ってくるので、最初の要素を取得している
  ss.peek(1).unpack('C').first // 7
  
  # サイズを格納している、1byte分進める
  ss.pos += 1
  
  # 最後に、先ほど判明した7byte分取得して、unpackすると
  # めでたく最初の数列が取得できる
  ss.peek(7).unpack('w5') // [33, 49, 300, 99, 1000]