ファイル圧縮の形式は世にいろいろありますが、昨今はzipがもっとも一般的でしょうか。
zipは圧縮・展開にかかる時間もほどほど、圧縮率もほどほど、そしてWindowsでもLinuxでも対応ソフトウェアあり、など扱いやすさの面でリードしています。
そんななか、特定分野で人気なのがXZ形式。圧縮にはパワーも時間も使いますが、zipよりも圧縮率が高くかつ短時間で伸長できる、という性質があります。圧縮は一度で、何度も伸長する、という例えばソースコード一式やインストーラの配布といった用途に向いていると言えるでしょう。
あくまで圧縮形式、なのでアーカイブ機能はありません。
今回はWindows向けにXZ形式の圧縮を実装します。アーカイブ形式としてはTarを使います。
もくじ:
はじめに
単に使えればよい、という側面もありますが圧縮時の負荷が大きいという点からは、やはり少しでも速いほうがいいですよね。Python (3.x系)ではlzma
モジュールで使えるようになっていますが、今回はNimでも実装して速度を比較してみます。
XZの圧縮機能、ツールは開発元からSDK (XZ Utils)が提供されており、世に出回っているソフトウェアの多くはそれに依存しているもよう。本体はパブリックドメイン、ツール類を含めるとGPLでライセンスされているようです。
検証した環境はWindows 10 Pro, Python 3.6, Nim 0.18.0です。
PythonでのXZ圧縮
直にlzma
モジュールを叩いてもいいのですが、tar作成の関係もありshutil
モジュールを利用します。内部的にはlzma
モジュールとtarfile
モジュールに依存しているようです。
実装はshutil
モジュールがかなり抽象化しているので、1行です。
1 2 | import shutil res = shutil.make_archive("path/to/destfile", "xztar", "path/to/sourcedir") |
もとのディレクトリは"path/to/sourcedir"
で、サブディレクトリ以下のファイルも含まれます。
返り値(上の例ではres
)には作成された結果のファイルのパスが入り、この場合は"path/to/destfile.tar.xz"
のように拡張子が付加されたものになります。
第2引数はアーカイブの形式を指定する文字列で、環境依存ですが他にもzip, bztar, gztarなどが利用できます。
NimでのXZ圧縮
実装、といってもスクラッチで作るのは面倒なので、素直にXZ Utilsを使います。
Windows環境なのでビルド済みのDLLを使ってNimから呼び出すようにします。
NimbleにもLZMAのラッパはあるのですが、動かしてみるとどうやらこれはバグっている様子。参考にしつつもファイル圧縮のところのみ抜き出して実装します。
Tar作成には別記事の実装を利用します。中間ファイルを吐かずオンザフライに作るよう実装するのが面倒だったので、一旦tarファイルを作成したのち、XZ形式で圧縮することにします。
XZのエンコーダは圧縮の始めから終わりまでステートフルになっており、ちゃんとオンザフライ(中間ファイルを作成しないやり方)にするならtarファイルを抽象化するストリームで実装することになると思われます。
実際試してみると分かりますが、計算処理の負荷で言えば、tarファイルを作成する処理よりも圧倒的にXZ圧縮の処理のほうが重くなります。
XZ圧縮のプログラム例については後ほど記載します。
速度比較
気になるのは速度ですね。そもそも想定する用途が違う(もちろんshutil
のほうが汎用的に作られています)のでapple to appleな比較にはなりませんが、まあご参考までに、ということで。
このテストでは、そこらへんにあったディレクトリを圧縮の対象としました。もとのファイル(tar作成時点で)は約120MB、圧縮をかけると約80MBになりました。
XZの圧縮オプション(preset)はデフォルト(6)、誤り訂正オプションはCRC(64)です。
複数回(7回)実行させた平均値は下記のようになりました。計測のためPythonではtime.time()
、Nimではtimes.cpuTime()
を使っています。あまり精度は良くないようですが、試行間の差は0~3秒程度でした。
Python 3.6 | Nim (liblzma.dll) | |
---|---|---|
Time (7-times avg.) | 50.0[sec] | 34.3[sec] |
Pythonでの実装(スクリプト)とNimでの実装(バイナリ)を用意しておき、PowerShellからInvoke-Expression
でランダムに呼び出しています。
さすが静的型付け言語、Nimのほうが速いですね。もちろん世のプロフェッショナルによる枯れた実装があるなら、わざわざ素人が自作する必要はありません。先達に感謝を捧げつつ利用するのみ。しかしながら、かゆい所を色々試してみるのは楽しいものです。
Pythonの高速化と言えばNumbaやCythonがありますが、外部のモジュールに依存している場合やうまく処理を切り出せる場合、細かくチューニングしたい場合など、静的言語を使うのもアリかもしれませんね。
Nimでのプログラム例
詳細は省略しますが、Nimでliblzma.dllのラッパを実装した例(というか上記のパクリ)が下記です。
xz
プロシージャにファイルパスを渡すと、対象のファイルと同じ場所でXZ圧縮をかけます。元のファイルは削除しません。
NimからDLLで定義されている関数を呼んでいるだけなので、Linuxでもshared objectを用意してコンパイルすれば動くと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | import os type XZStream {.final, pure.} = object nextIn*: cstring availIn*: int totalIn*: int nextOut*: cstring availOut*: int totalOut*: int allocator*: pointer internal*: pointer reservedPtr1*: pointer reservedPtr2*: pointer reservedPtr3*: pointer reservedPtr4*: pointer seekPos*: int reservedInt2*: int reservedInt3*: int reservedInt4*: int reservedEnum1*: int32 reservedEnum2*: int32 XZlibStreamError = object of Exception const liblzma = "libs/liblzma.dll" # specify path to your library LZMA_PRESET_DEFAULT = 6.int32 ## Default compression level LZMA_STREAM_INIT = XZStream( nextIn: nil, availIn: 0, totalIn: 0, nextOut: nil, availOut: 0, totalOut: 0, allocator: nil, internal: nil, reservedPtr1: nil, reservedPtr2: nil, reservedPtr3: nil, reservedPtr4: nil, seekPos: 0, reservedInt2: 0, reservedInt3: 0, reservedInt4: 0, reservedEnum1: 0, reservedEnum2: 0) ## Initialize stream # returns LZMA_OK* = 0 ## Operation completed successfully LZMA_STREAM_END* = 1 ## End of stream was reached # checks LZMA_CHECK_NONE* = 0.int32 ## No Check is calculated LZMA_CHECK_CRC32* = 1.int32 ## CRC32 using the polynomial from the IEEE 802.3 standard LZMA_CHECK_CRC64* = 4.int32 ## CRC64 using the polynomial from the ECMA-182 standard LZMA_CHECK_SHA256* = 10.int32 ## SHA256 # actions LZMA_RUN* = 0.int32 ## Continue coding LZMA_FINISH* = 3.int32 ## Finish the coding operation proc lzma_code( strm: var XZStream, action: int32): int {.cdecl, dynlib: liblzma, importc: "lzma_code".} proc lzma_end( strm: var XZStream) {.cdecl, dynlib: liblzma, importc: "lzma_end".} proc lzma_easy_encoder( strm: var XZStream, preset, check: int32): int {.cdecl, dynlib: liblzma, importc: "lzma_easy_encoder".} proc xz*(source: string): string = if not source.existsFile(): raise newException(OSError, "source file missing :" & source) const XZ_BUFFER = 1024 * 512 let preset = LZMA_PRESET_DEFAULT check = LZMA_CHECK_CRC64 var fin = open(source) fout = open(source & ".xz", fmWrite) ## result as xz appended path inbuf: array[XZ_BUFFER, char] outbuf: array[XZ_BUFFER, char] strm = LZMA_STREAM_INIT stat = lzma_easy_encoder(strm, preset, check) ret: int action = LZMA_RUN defer: fin.close() fout.close() if stat != LZMA_OK: raise newException(XZlibStreamError, "failed to initialize :" & $stat) while true: # input loop strm.nextIn = inbuf.addr strm.availIn = fin.readBuffer(inbuf[0].addr, XZ_BUFFER) if strm.availIn == 0: action = LZMA_FINISH while true: # output loop strm.nextOut = outbuf.addr strm.availOut = XZ_BUFFER ret = lzma_code(strm, action) # handle returned code if ret != LZMA_OK and ret != LZMA_STREAM_END: raise newException(XZlibStreamError, "failed to encode :" & $ret) # write to output file discard fout.writeBuffer(outbuf[0].addr, XZ_BUFFER - strm.availOut) if strm.availOut != 0 or ret == LZMA_STREAM_END: break if action == LZMA_FINISH: break lzma_end(strm) return source & ".xz" |
おわり。