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で書くのが一番ストレス溜まらんなこりゃ。