What goes on/what's going on:2021年04月27日分

2021/04/27(Tue)

[オレオレN6] UTF-8 LC_CTYPE(その2)

はい今回も完全にタイトルとは無関係な内容です。

前回はXPathがUCD(Unicode Character Database)にワンパンで瞬殺されたところまで、ということで代わりにSAX的なもので対抗することにする。 どうせXMLである必要すらないデータだからツリー構造とかどうでもええねん、というかCSVとかで配布すりゃいいのだこんなもん。最適なフォーマット選べないすなわちピーなのである。

まぁこんな貧者的プログラミングなんぞ、CPU/GPU/メモリ/ストレージ/ネットワークを強欲に独占すればオッケーなモノポリーの方々からすれば虫ケラの所業であろうが、お釈迦様のいう長者の万灯より貧者の一灯の精神であり荘子のいう蟷螂の斧でもある。 そういえばカマキリって英語だとPraying Mantisであの動作は歯向かうのではなくお祈りにみえるそうっすね、しょせん力無き者の反抗なぞ命乞いにしか見えんのだろうなぁ…

このUCDのデータ量なんぞなんぞビッグデータとかいう砂金掘りからみれば極楽の蓮花が咲く池の底に落ちたゴミくらいだろうが、道具の選択間違えるとコトかもだまずはベンチ取ってから作業なので?

@perl-5.32の場合

まずはいつものPerl、XML::SAXモジュールでucd.all.flat.xml(およそ193MB)をパースするだけ(ハンドラは空っぽ)のコードで実行環境はCygwin。

#!/usr/bin/perl
package MyHandler;
use XML::SAX;
use XML::SAX::Base;
use Module::Load;
use base qw(XML::SAX::Base);
sub start_element
{
}
my $class = $ARGV[0];
load  $class;
my $parser = $class->new(Handler => new MyHandler());
open(my $fh, '<ucd.all.flat.xml') || die;
$parser->parse_file($fh);
1;

いくつかSAXドライバにも種類があるので、

  • XML::SAX::PurePerl … XML::SAX同梱のデフォルト実装
  • XML::LibXML::SAX … libxml2のSAX APIよる実装
  • XML::LibXML::SAX::Parser … ストリームでなくDOMをパースするSAX実装、当然のようにメモリ不足になるので除外
  • XML::SAX::Expat … PerlによるExpat実装
  • XML::SAX::ExpatNB … ↑のNonBlocking版
  • XML::SAX::ExpatXS … libexpatによる実装

の実行時間を比較したのだけども

$ time ./unko.pl XML::SAX::PurePerl

real    42m43.255s
user    41m48.280s
sys     0m1.453s

$ time ./unko.pl XML::LibXML::SAX

real    1m6.316s
user    1m5.796s
sys     0m0.358s

$ time ./unko.pl XML::SAX::Expat

real    2m42.115s
user    2m40.062s
sys     0m0.311s

$ time ./unko.pl XML::SAX::ExpatNB

real    2m43.091s
user    2m42.248s
sys     0m0.625s

$ time ./unko.pl XML::SAX::ExpatXS

real    1m1.332s
user    1m0.937s
sys     0m0.296s

まぁPurePerlの性能が論値つーかあまりにもひどいのとDOMベースのSAXとは(哲学)NonBlockingの効果とは(哲学)なネタ枠はさておき、下り最速のExpatXSすらオシッコ漏れちゃいそうなほどに尋常に遅い。

@python-3.8の場合

こいつはxml.parsers.expat以外の実装あるのか知らんのでこんなもん。

#!/usr/bin/python
import xml.sax
import xml.parsers.expat
from xml.sax.handler import ContentHandler
class MyHandler(xml.sax.handler.ContentHandler):
  def startElement(self, name, attr):
    pass
parser = xml.sax.make_parser()
parser.setContentHandler(MyHandler())
parser.parse(open('ucd.all.flat.xml'))

実行結果は

$ time ./unko.py

real    0m11.335s
user    0m11.015s
sys     0m0.202s

とおー速い速い、p5-XML-SAX-ExpatXSの数倍以上ですわ。

なおワイのPython経験はTracにあった国際化まわりのバグを修正した15分程度なのでよくわからない俺は雰囲気でPythonを書いている以下略

@Rのつく言語

ついでにRのつく言語でも試してみる。

library(XML)
startElement <- function(name, attrs)
{
}
xmlEventParse('ucd.all.flat.xml', handlers = list(startElement = startElement))

つい先日までPowerShellがマイブームだったがその前はRを嗜んでおったはずなんよねもう記憶から完全に消えとるが。 こいつだけcygwin binaryではないがまぁ大した違いは無いはず。

$ time /cygdrive/c/Program\ Files/R/R-4.0.5/bin/Rscript.exe unko.R

real    0m12.728s
user    0m0.000s
sys     0m0.015s

うんやっぱりこのくらいは出るよなぁという。

いやRってそっちかよ!ってネタなのだが、ちゃんとRuby-2.6も試したよ!

まずはPureRubyなREXMLとかいうの。

#!/usr/bin/ruby
require 'rexml/parsers/sax2parser'
require 'rexml/sax2listener'
class MyHandler
  include REXML::SAX2Listener
end
parser = REXML::Parsers::SAX2Parser.new(File.read('ucd.all.flat.xml'), MyHandler.new)
parser.parse
$ time ./unko1.rb

real    2m43.624s
user    2m41.718s
sys     0m0.359s

まぁp5-XML-SAX-Expatと同程度すね。

そんでlibexpatではなくlibxml2バックエンドのNokogiriだとこんな感じ。

#!/usr/bin/ruby
require 'nokogiri'
class MyHandler < Nokogiri::XML::SAX::Document
  def start_document
  end
end
parser = Nokogiri::XML::SAX::Parser.new(MyHandler.new)
parser.parse(File.open('ucd.all.flat.xml'))
$ time ./unko2.rb

real    0m32.263s
user    0m31.187s
sys     0m0.562s

ふーんp5-XML-LibXMLの倍は速い。

他にもRubyはSAX実装いっぱいあるっぽいけど他の人のベンチ見る限り期待できそうも無いので、いちばん速そうなlibexpat使うxmlparserだけ。

#!/usr/bin/ruby
require 'xml/parser'
class MyParser<XML::Parser
  def startElement(name, attr)
  end
end
parser = MyParser.new
parser.parse(File.open('ucd.all.flat.xml'))
$ time ./unko3.rb

real    0m12.909s
user    0m12.312s
sys     0m0.312s

やっぱlibexpatならこんくらいの速さ出るよね、やっぱりPerl5の遅さはアレやのう。

ただ残念なことにこれ誰もメンテしておらずobsoleteなようで

  • rb_raiseにフォーマット文字列を欠いてるケアレスミスで-Werror=format-securityによりビルドが止まる
  • ENC_TO_ENCINDEXというマクロが見つからず暗黙の関数宣言扱いになり同上

という問題があって今のRuby2.6だとまともにビルド通らんので、クッソ適当に以下のpatchあてて動かしている。

--- xmlparser.c.orig	2013-02-07 09:45:23.000000000 +0900
+++ xmlparser.c	2021-04-27 17:15:26.000000000 +0900
@@ -114,7 +114,7 @@ static ID id_skippedEntityHandler;
 #endif
 
 #define GET_PARSER(obj, parser) \
-  Data_Get_Struct(obj, XMLParser, parser)
+  Data_Get_Struct((VALUE)obj, XMLParser, parser)
 
 typedef struct _XMLParser {
   XML_Parser parser;
@@ -1780,7 +1780,7 @@ XMLParser_parse(int argc, VALUE* argv, V
       if (!ret) {
 	int err = XML_GetErrorCode(parser->parser);
 	const char* errStr = XML_ErrorString(err);
-	rb_raise(eXMLParserError, (char*)errStr);
+	rb_raise(eXMLParserError, "%s", errStr);
       }
     } while (!NIL_P(buf));
     return Qnil;
@@ -1803,7 +1803,7 @@ XMLParser_parse(int argc, VALUE* argv, V
       volatile VALUE encobj;
       volatile VALUE ustr;
       enc = rb_enc_find(parser->detectedEncoding);
-      if ((int)ENC_TO_ENCINDEX(enc) != rb_ascii8bit_encindex()) {
+      if (rb_enc_to_index(enc) != rb_ascii8bit_encindex()) {
         rb_enc_associate(str, enc);
         encobj = rb_enc_from_encoding(enc_xml);
         /* rb_str_encode may raises an exception */
@@ -1829,7 +1829,7 @@ XMLParser_parse(int argc, VALUE* argv, V
   if (!ret) {
     int err = XML_GetErrorCode(parser->parser);
     const char* errStr = XML_ErrorString(err);
-    rb_raise(eXMLParserError, (char*)errStr);
+    rb_raise(eXMLParserError, "%s", errStr);
   }
 
   return Qnil;

これで正しいかは知らないし他にもセキュリティ絡みの問題もあるかもしれないのでお勧めはしない。

ちなみにp**srcのやつは警告緩めて対処したのかUndefined symbolのままビルドされとるようで外部公開サービスで使ってたらDenial of Serviceできるよねこれ…やはり古いパッケージは抹殺すべき。

$ nm /usr/pkg/lib/ruby/vendor_ruby/2.6.0/x86_64-netbsd/xmlparser.so | grep ENC_TO_ENCINDEX
                 U ENC_TO_ENCINDEX

まぁ本家の事も知らない。

なお俺のRuby経験は仕事でRubyでって指定されたけど納期優先したけりゃ俺の知ってるPerlで書かせろと答えた15秒程度なのでわからないやっぱり雰囲気で書いて以下略。

@結論

もうLL言語なんか捨ててCでかかってこいベネット!って気分になったので以下のコードを試す。

#include <stdio.h>
#include <stdlib.h>
#include <expat.h>
static void
start(void *ctx, const XML_Char *elm, const XML_Char **attr)
{
}
static void
end(void *ctx, const XML_Char *elm)
{
}
int
main(int argc, char *argv[])
{
	FILE *fp;
	XML_Parser parser;
	char buf[BUFSIZ];
	int len;

	fp = fopen("ucd.all.flat.xml", "r");
	if (fp == NULL)
		abort();
	parser = XML_ParserCreate(NULL);
	if (parser == NULL)
		abort();
	XML_SetElementHandler(parser, &start, &end);
	while ((len = fread(buf, 1, sizeof(buf), fp)) > 0) {
		if (XML_Parse(parser, buf, len, 0) == XML_STATUS_ERROR)
			abort();
	}
	if (ferror(fp) || XML_Parse(parser, NULL, 0, 1) == XML_STATUS_ERROR)
		abort();
	XML_ParserFree(parser);
	fclose(fp);

	exit(EXIT_SUCCESS);
}

こいつの実行結果は以下の通り

$ gcc -o unko.exe unko.c -lexpat
$ time ./unko.exe

real    0m5.987s
user    0m5.859s
sys     0m0.093s

…美しい、これ以上の芸術作品は存在し得ないでしょう。

まぁCにはCなりのめんどくささがあるので単純比較はそらまあできんけど、UCD程度のしょーもない中身のデータならCで書くのが一番ストレス溜まらんなこりゃ。