ImageMagick の convert で PDF を生成する場合、どんな感じでメモリを消費するだろうか。
まず、1000x1000 の白い PBM ファイルを (netpbm の) pbmmake で生成する。このファイルのサイズは 125kB くらいである。
% pbmmake -white 1000 1000 > white.pbm % wc -c white.pbm 125013 white.pbm
このファイルから convert で n ページの PDF を生成し、その途中の /proc/pid/status の VmSize を 0.01秒間隔で観測する。
% ruby -e ' STDOUT.sync = true puts "n,t[s],vmsize[kB]" 1.upto(100) {|n| args = ["convert"] + ["white.pbm"]*n + ["x.pdf"] t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC) pid = spawn(*args) until Process.waitpid(pid, Process::WNOHANG) status = File.read("/proc/#{pid}/status") vmsize = status[/VmSize:\s*(\d+)\s*kB/, 1].to_i t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC) puts "#{n},#{t2-t1},#{vmsize}" sleep 0.01 end } ' > convert-pdf-gen.csv
グラフを描くと次のようになる。
convert-pdf-gen.R:
library(ggplot2) d <- read.csv("2016-04/convert-pdf-gen.csv") p <- ggplot(d, aes(t.s.,vmsize.kB.,color=n,group=n)) + geom_line() + scale_x_continuous("t[s]") + scale_y_continuous("VmSize[kB]") print(p)
これをみると、ページ数に比例するメモリを最初に確保(消費)し、ページ数に比例する時間の処理を行い、終了する、という挙動がみてとれる。
VmSize のピークは、n=100 だと 931912kB、n=1 だと 153524kB なので、だいたい 1ページ増えるごとに 8MB 弱増加するようだ。画像のサイズは 1000x1000 なので、8byte/pixel で消費しているのかな。
処理時間がページ数に比例するのはしかたがない。でも、メモリ量がページ数に比例するのはよくないと思う。
PDF は基本的には先頭からページ単位に生成していけるはずで、少なくともすべての画像を同時にメモリに置いておく必要はないだろう。1ページごとに画像を読み込んでは解放するようにしていけば、こんなにメモリを使わなくて済むのではないか。
そういう省メモリなツールはないのかな。
たぶん、1ページごとに convert で PDF に変換した後に、PDFの連結ツールを使えばいいとは思うのだが、一発でやるツールがあってもおかしくないのではないか。
まぁ、cairo あたりを使って実装してもそれほど難しくはないと思うけれど。
rcairo を使ってちょっと書いてみた。
images2pdf:
#!/usr/bin/ruby # usage: # images2pdf img1 img2 ... output.pdf require 'gdk3' require 'cairo' output = ARGV.pop inputs = ARGV surface = Cairo::PDFSurface.new(output, 0, 0) context = Cairo::Context.new(surface) inputs.each {|input| pixbuf = Gdk::Pixbuf.new(input) surface.set_size pixbuf.width, pixbuf.height context.set_source_pixbuf(pixbuf) context.paint context.show_page GC.start } surface.finish
同様にグラフを描いてみると以下のようになり、メモリ消費がページ数に依存しないようになっていることがわかる。
なお、このようにちゃんと省メモリにするには、GC.start を明示的に呼び出す必要があった。たぶん、Gdk::Pixbuf がかなり大きなメモリを消費していることに Ruby が気がつかないため、GC が起動しないせいだと思う。
これは RTypedData を使えば (rb_data_type_struct の dsize を設定すれば) メモリを消費していることに GC が気がついて、自動的に GC が動くようにできると思うが、確認していない。
Coq で、Dependent type error in rewrite というエラーに出会った。
たとえば、以下のようにすると (最後の rewrite Hxy のところで) 発生する。
From Ssreflect Require Import ssreflect ssrbool eqtype ssrnat. Record Rec : Type := rec { m : nat; n : nat; c : m < n }. Goal forall x y z cx cy, x = y -> m (rec x z cx) = m (rec y z cy). Proof. move=> x y z cx cy Hxy. rewrite Hxy.
実際のエラーメッセージは以下のようなものである。
Error: Dependent type error in rewrite of (fun _pattern_value_ : nat => m {| m := _pattern_value_; n := z; c := cx |} = m {| m := y; n := z; c := cy |})
Hxy は x = y なので、x を y に書き換えられるとおもいきや、エラーになる。
エラーメッセージを眺めて想像すると、cx が x < z の証明であって、y < z の証明ではないのが問題なのだろう。
x < z の証明と x = y の証明から自動的に y < z の証明を作ってくれても良いと思うのだが、rewrite はしてくれないようだ。
Ruby 2.4 には Array#sum メソッドが入るのだが、sum メソッドはすでにさまざまな gem が提供している。いくつかの実装を比べてみよう。
activesupport-4.2.6:
module Enumerable def sum(identity = 0, &block) if block_given? map(&block).sum(identity) else inject { |sum, element| sum + element } || identity end end end
facets-3.0.0:
module Enumerable def sum(*identity, &block) if block_given? map(&block).sum(*identity) else reduce(*identity) { |sum, element| sum + element } end end end
simple_stats-1.1.0:
module Enumerable def sum(&block) return map(&block).sum if block_given? inject(0) { |sum, x| sum + x } end end
production_log_analyzer-1.5.1:
module Enumerable def sum return self.inject(0) { |acc, i| acc + i } end end
hash-utils-2.2.0:
class Array def sum self.inject(:+) end end
gcstats-1.0.4:
class Array def sum inject {|a, e| a + e } end end
gauntlet-2.1.0:
class Array def sum sum = 0 self.each { |i| sum += i } sum end end
さまざまな実装があることがわかる。
こうもいろいろとあると、実は今までにも互換性の問題が発生していたかもしれないな。
Ruby 2.4 では (現在の実装では) Array に定義して、与えられたブロックは適用し、要素がなかったときの値を指定でき、指定しなければ 0 を返し、指定した場合は結果に加えられる。まぁ、ブロックを使わない (無視する) ようにすると、エラーもなく値が変わって不幸だし、map するように変えるのは呼出側のコードが長くなっちゃうしな。要素がなかったときの値は悩みどころだが、結果に加えた方が仕様の中の場合分けが少なくなるのでいいだろう。
[latest]