その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

先読みとautovivificationの話,あるいはマイクロオプティマイゼーションの話

Perlの話です.が,先読みの辺りはどの言語でも共通なのでは,という感じです.

追記

なんか先読み関係ない感じになってるのでコメント見ると良いです.この記事の情報は誤っているので後で書き直す.

ここから先は読まなくても良い

さて,配列を走査するような処理を書く時,「1個後のアイテムと現在のアイテムを比較したい」というようなニーズから,1個後のアイテムを読んでおきたいというような事があると思います.

ナイーブに実装するとしたら以下のようになるでしょう (C-Style Loopで書いている理由は以降の比較のためなので深く考えないで下さい).

my @array = (1..100);
for (my $i = 0; my $item = $array[$i]; $i++) {
    my $next_item = $array[$i+1];
    # do something...
}

このコードは,先読みした値を変数に格納しておいて,それを使いまわすというような効率的な書き方に変形する事が出来ます.

my @array = (1..100);
for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    # do something...
}

さてこの両者でベンチマークを取ってみると以下の様な感じになります.

use Benchmarks sub {
    my @array = (1..100);

    my $normal = sub {
        for (my $i = 0; my $item = $array[$i]; $i++) {
            my $next_item = $array[$i+1];
            # do something...
        }
    };

    my $read_ahead = sub {
        for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
            $next_item = $array[$i+1];
            # do something...
        }
    };

    +{
        normal     => $normal,
        read_ahead => $read_ahead,
    };
};
__END__
              Rate     normal read_ahead
normal     39711/s         --       -25%
read_ahead 52609/s        32%         --

速いですね! めでたしめでたし!

めでたくない場合

しかしながら走査するオブジェクトの構造が変わるとそうは問屋がおろしません.
以下の様なコードの場合を考えてみましょう.

use Data::Faker;
my $faker = Data::Faker->new;
my @array;
for (1..100) {
    push @array, +{
        name => $faker->name,
        age  => int rand(80),
    };
}

for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    $next_item->{name};
}

このコードは永遠に終わりません.無限ループします.

このコードで配列の最後の要素にアクセスしている時,$next_itemundefとなりますが,このundefに対してハッシュリファレンスのようにアクセスすると,undef{}という風に空のハッシュリファレンスに変化してしまいます (もちろん配列リファレンスで同じようなことを行なった場合も同様です).以下の様な感じ.

my $foo = undef;
$foo->{not_exists}; # <= この時点で$fooは{}になっている!

Perlでは空のハッシュリファレンスは真値であるので,論理和の評価は空ハッシュである$next_itemが返ってきてしまい,for文の条件が常に真として扱われてしまい無限ループしてしまうという訳ですね.

このように,走査対象の配列の中身がハッシュリファレンスで,なおかつ1個次の要素を見る必要がある場合,上手くいかずにハマる場合があるわけです.

そこで我々はどうすべきか

1. ローカル変数を1個作って,そこに$next_itemをコピーしてそっちにアクセスする
for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    my $_next_item = $next_item;
                                                                            
    # do something...
    $_next_item->{name};
}

このようにすると無限ループは起きないものの……

use Data::Faker;
use Benchmarks sub {
    my $faker = Data::Faker->new;
    my @array;
    for (1..100) {
        push @array, +{
            name => $faker->name,
            age  => int rand(80),
        };
    }

    my $normal = sub {
        for (my $i = 0; my $item = $array[$i]; $i++) {
            my $next_item = $array[$i+1];

            # do something...
            $next_item->{name};
        }
    };

    my $read_ahead = sub {
        for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
            $next_item = $array[$i+1];
            my $_next_item = $next_item;

            # do something...
            $_next_item->{name};
        }
    };

    +{
        normal     => $normal,
        read_ahead => $read_ahead,
    };
};
__END__
              Rate read_ahead     normal
read_ahead 19855/s         --       -11%
normal     22399/s        13%         --

先読みしたほうが遅い!!!! 意味無いじゃん!!!!
……まあそりゃそうですよねという感じ.

2. no autovivificationする

undef->{something}->[42]のようにアクセスするとそれぞれ空の{}[]という風になってしまうのが問題なので,その挙動を潰してやれば良い!

ということでautovivificationの出番です.
https://metacpan.org/pod/autovivification

no autovivificationという風に宣言してやると,そのレキシカルスコープでvivification,つまりundefを自動で空のハッシュリファレンスや配列リファレンスに変換する処理を無効にすることが出来ます.

no autovivification;
for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
    $next_item = $array[$i+1];
    # do something...
    $next_item->{name}; # <= ここでundefが変換されない!
}

ベンチマークの結果は以下のとおり

no autovivification;
use Data::Faker;
use Benchmarks sub {
    my $faker = Data::Faker->new;
    my @array;
    for (1..100) {
        push @array, +{
            name => $faker->name,
            age  => int rand(80),
        };
    }

    my $normal = sub {
        for (my $i = 0; my $item = $array[$i]; $i++) {
            my $next_item = $array[$i+1];
            # do something...
        }
    };

    my $read_ahead = sub {
        for (my $i = 0, my $next_item; my $item = $next_item || $array[$i]; $i++) {
            $next_item = $array[$i+1];
            # do something...
        }
    };

    +{
        normal     => $normal,
        read_ahead => $read_ahead,
    };
};
__END__
              Rate     normal read_ahead
normal     36885/s         --       -18%
read_ahead 44776/s        21%         --

おっ,速い!

まとめ

  • undefにリファレンスアクセスするといつの間にかundefが別のものになる
  • うかつに先読みしない
  • とは言え先読みしたい,なおかつ少しでもパフォーマンスが気になる場合はno autovivificationを使うのが良いのでは無いか.


という感じです

Table要素をCSVとかXLSXとかに変換して保存出来るChrome拡張書いた

皆さん年に2,3回は,「ウェッブページのTable要素をXLSX (もしくはCSV) に変換して保存したいワ!」となることがあると思います.僕はあります.

そういう時は,「心をこめてTableをコピーしてExcel (もしくはお好みのヒョーケーサンソフト) にペースト!」みたいな感じになると思うんですが,コピペギョームはTableの行数がめっちゃ多い時 (1画面で収まらないTable要素とか) にだるいし,コピペ処理中になんらかの事故が起きてミスった結果えらい人に呼ばれて怒られる可能性なども否めないので,そういうことをクリック2発で実現するChrome拡張を書きました.

https://chrome.google.com/webstore/detail/table-to-spreadsheet/haidhlbpihfihbjcggmffnmhgiddjcoc?hl=ja

何をするChrome拡張かというと,Table要素を右クリックして,XLSX (あるいはCSV) に変換するメニューを選択すると,Tableの内容がそれらの形式に変換されてローカルに保存できるというシンプルな拡張となっております.

色々あって,Table以外の部分でもコンテキストメニューが表示されてしまう感じになっていますが,Table以外の箇所でメニューを選択してもalertが出るだけという親切設計 (!?) になっているので安心 (!!??) です *1
あと既知の問題として,テーブルの中にテーブル,というような入れ子構造のテーブルに上手く対応できないというものがあって,これはどうしたもんかなあと思っております.何か良いアイデアがある方がいらっしゃいましたら教えて下さい.


とりあえずなんやかや便利な感じなのでご利用下さいませ.


なおソースは以下です.CSV.jsjs-xlsxがトンデモなく便利でヤバい!
https://github.com/moznion/crx-table-to-spreadsheet

*1:ダサいからなんとかしたい

ページャNight 1ページ目という勉強会やりました


録画したustの様子はこちら

http://www.ustream.tv/recorded/49544381


ページャNight <[1]> on Zusaarというイベントを開催致しました.
実に冗談みたいな理由から興ったイベントでしたが,ページャ (ページネーション) というWebサービスの1コンポーネントに焦点を絞った非常に濃厚かつ興味深いトークの数々を聞くことができて,非常に良い勉強会になったと思います.

以下,発表の一覧です.

15分トーク

@さん お前自分ちのページャUIが本当に速いと思ってんの?


@さん APIのページングの話


@さん ページャ専用DBのご提案


@さん ふつうのページャーをつくろう


LT

@さん 前に進むだけがページャじゃない
@さん ページラング


発表してくださった皆様,お集まり頂いた皆様,そして快く会場を提供してくださったピクシブ株式会社の皆様,ありがとうございました!

さて

Webサービスを構築する上で今回の「ページャ」のようなコンポーネントはそれ単体では小さな存在の為,それらを主として知見を共有したり議論したりするという機会にはあまり恵まれていないように感じています.
どっこいWebサービスというのはこうした小さな・細かなコンポーネントを根気よく組み合わせて構築していくものなので,小さな存在だからと言って蔑ろにしてはならない.Webサービスを構成している要素は,その規模の大小に関わらず等しく重要なポジションを占めているので,それら個別に関する勉強会があっても良いじゃないか! という動機で今回のイベントを催しました.結果的に数々の知見がお披露目され,非常に有益な会になったように思います.

こうした「細かいコンポーネントシリーズ」はコンスタントに開催できれば面白いのでは,と思った次第.

またお会いしましょう.

【補欠の方も参加いただけます】 今週金曜日,7月4日はページャNightです

参考

http://www.zusaar.com/event/5477013
http://moznion.hatenadiary.com/entry/2014/06/11/163303

今週金曜日はページャNightです.会場はピクシブ株式会社様です.
たくさんの方に発表していただけることとなり,当初の想定の100倍くらい実りのある会となりそうであります.

そしてなんと!!!

ピクシブ様のご厚意で補欠の方でも立ち見で参加頂ける運びとなりました!!
ぜひふるってご参加下さいませ.

Time::SecondsのONE_MONTHとONE_YEARについて

Time::SecondsONE_MONTHONE_YEARを使う場合,本当にその方法で良いのかよく考えたほうが良いと思います.バグが出る可能性が高い気がします.

例えば以下の様な場合

use Time::Piece;
use Time::Seconds;

my $tp = localtime->strptime("2014-04", "%Y-%m");
say $tp->mon; # => 4
say $tp->ymd; # => 2014-04-01

$tp -= ONE_MONTH;
say $tp->mon; # => 3
say $tp->ymd; # => 2014-03-01

$tp -= ONE_MONTH;
say $tp->mon; # => 1 (!)
say $tp->ymd; # => 2014-01-30 (!!)

このコードは,Time::PieceのオブジェクトからONE_MONTHを引いてやる事によって1ヶ月前の月を表現したいという意図を表していますが,上手く動きません.
それもそのはず,Time::Secondsの実装は以下のようになっており,

https://github.com/rjbs/Time-Piece/blob/master/Seconds.pm#L29-L31

ONE_MONTHは2629744秒 (つまりONE_YEAR / 12) のようにして定義されています.
30日を秒に表すと60 * 60 * 24 * 30 = 2592000秒だからONE_MONTHよりも小さいわけですね.
加えて2014年2月は28日しか無く,つまり60 * 60 * 24 * 28 = 2419200秒なので,ここで決定的に差が生まれてしまったという訳ですね.これは具合が良くない! もちろん「1ヶ月前の今日」みたいなものも上手くは取れないですね.

そしてONE_YEARONE_YEARで31556930秒,つまり365.24225日として表現されている為,こちらも普通に使うとおかしな事になってしまいます.

use Time::Piece;
use Time::Seconds;

my $tp = localtime->strptime("2014", "%Y");

$tp -= ONE_YEAR;
say $tp->year; # 2012 (!)
say $tp->ymd;  # 2012-12-31 (!!)

のっけからやってくれる!!!! まあそれはそうですよね,という感じ.
こちらも「1年前の今日」みたいなものは上手く取れない.


で,どうするかというと,Time::Piece::Monthを使うと言った方法が考えられますが,このモジュールは内部でDate::Simpleを使っていて,Date::Simpleは呪われている(呪われていた)という問題があるので少し渋い.この方法でも問題なさそうだったらこれで良いとは思います.
問題が複雑では無かったら,LEAP_YEARNON_LEAP_YEARONE_FINANCIAL_MONTHや,あるいはONE_REAL_MONTHONE_REAL_YEARを駆使して自分で組み立てるというのも1つの手かもわかりません.

とにかく,ONE_MONTHONE_YEARを使って何らかの日付け操作をしているコードを見つけた時は疑ってみたほうが良いと思います.


手が空いたらここらへんをケアするシンプルなモジュールを書くのもやぶさかではありませんが,果たして手が空くのか!


[追記]
Time::Piece::Plus使えばええんや!!!!! という僕の中の僕が囁きましたのでシェアします.

[追記]
id:karupanerura氏「add_months/add_years ってメソッド使えや!!!」
とのこと.なんで僕はこのメソッドの存在を知らなかったのだろうか!?

ともかくONE_MONTHとかONE_YEARとかつかってたら大体おかしいことには違いないので,そういう場合は疑うってことでひとつ.

WEB+DB Press Vol.81のPerl Hackers Hubに寄稿しました

WEB+DB Press Vol.81のPerl Hackers Hubに「Perlにおける静的解析」というタイトルで寄稿致しました.商業誌デビューです.
そうそうたる著者の皆様の末席を汚しているのが僕です.そうそうそいつそいつ.

WEB+DB PRESS Vol.81

WEB+DB PRESS Vol.81

最近個人的に,Perlに限らず静的解析周りに興味があったことや,TPFのPerl::Lintの話などもあり,それじゃまあそこら辺の話で良ければ書きますよ〜〜〜〜って事で書きました.


以下おしながきです

  • 静的解析とはなにか
  • 静的解析の (Javaでの)
  • Perlにおける静的解析の背景と課題
  • PPIを使ってみる
  • Compiler::LexerおよびCompiler::Parserを使ってみる
  • 便利なPerl向け静的解析ツールの紹介
  • Compiler::Lexerを使って実際に簡単な静的解析ツールを書いてみる


Compiler::LexerやCompiler::Parserが台頭してきたり,App::PRTなどの静的解析を用いた便利なツールがぽこぽこリリースされたりするなど,Perlの静的解析周りはここ最近面白い状況になっており,その辺の話題を中心に触れてみました.Perlの静的解析にまつわる歴史的経緯についても軽く紹介しています.


手前味噌な感じで恐縮ですが,Compiler::Lexerを使った静的解析ツールの書き方を日本語で解説しているのは中々珍しいのではないかな〜と思います.
解説に従って実際にコードを書いてみると,ソースコード中のタイポを検出するスクリプトが「あら簡単!」という感じに出来上がる寸法となっています.
基本的にはこの特集で紹介しているような方法を使えば,たいていの静的解析ツールは作成出来ると思うので,「静的解析ツール,書いてみたいワ!」という方の手助けになれば幸いです *1


また,Perl Hackers HubだというのにJavaツールの解説に1ページほど割いており,我ながら最高な気分になりました.


発売日は6月24日(火)です.
興味のある方はぜひお手にとってみて下さいませ!

*1:あとは複雑さの程度問題なので,実装者の根気次第といったところでしょう

ページャNightというイベントをやります

参考: http://togetter.com/li/678306








http://www.zusaar.com/event/5477013

ページャNightというイベントをやります.皆さん冗談だと思われていたようですが冗談ではない.

イベントのページにも書きましたが,ページャはWebサービスに於いて極めて重要なポジションを占めています.ページャの実装いかんでは本当にタコな感じになってUXを損なったりしますし,ページャは一見瑣末な存在に見えてしまうので何かと多機能を求めがちですが,そのようにするとその裏では多大なる異常な努力が必要となってしまいます.
このあたりのさじ加減やら技術的なテクニックやらは,人類がインターネットを手にしてから脈々と受け継がれている筈なんですが,ぎっちょん人々はページャの話をあまり進んでしたがらないので,そうしたデッド知見を共有しようじゃあないですか皆さん!!!! というモチベーションで開催致しました.ふるってご参加下さい.

とまあこう書きましたが別にlessとかmoreとかポケベルの話とかでも良いと思います *1


というわけでページャにお詳しい皆様のトークをお待ちしています.
「喋ってもいいよ〜」という人はイベントページのコメントか,@までメンション等でお知らせいただければと思います.
まだ喋ってくれる方の数が少なくて参っているので,ぜひともよろしくお願いします!!!


[追記]
ページャってのはpaginationのこととして話を進めていましたが,正しい表現ではなかったですね.まあもはやページャだったら何でも良いです!!!

*1:僕個人はWebのページャに興味がありますが!