Hatena::ブログ(Diary)

hnwの日記 このページをアンテナに追加 RSSフィード

[プロフィール]
 

2016年1月4日(月) CloudFlareで運用中の独自ドメインをダイナミックDNSとして利用する このエントリーを含むブックマーク このエントリーのブックマークコメント

CloudFlareといえば無料CDNサービスが有名ですが、高機能な無料DNSサービスも提供しています。筆者は趣味で独自ドメインを管理しており、以前はRoute53を使っていたのですが、昨年の2月からCloudFlareを利用しています(参考:「CloudFlareのCNAME FlatteningをGitHub Pagesで使ってみた」)。10ヶ月ほどの運用で全く不満はありません。


独自ドメインでダイナミックDNS

さて、せっかく独自ドメインを持っているのですから、ダイナミックDNS独自ドメインでやりたいところですよね。実は、各種ダイナミックDNSサービスの自動更新を行ってくれるPerl製のツールddclientが最新版3.8.3からCloudFlareに対応しています。これを使えば独自ドメインのダイナミックDNSがお手軽にできるというわけです。


Debianのddclientにパッチを当てる

今回、筆者はRaspberry Pi 2上でddclientを利用しました。Raspberry PiOSであるRaspbianは現在Debian 8 (jessie) ベースになっているのですが、残念ながらjessieで提供されているddclientのバージョンは3.8.2です。


パッケージ版をあきらめてソースコードから入れてしまうという選択肢もありますが、Debianのパッケージ版だとDebianの流儀でデーモンとして管理できる点が大きなメリットです。そこで、今回はパッケージ版に手動でパッチを当てることにしました。


具体的には、下記手順のようにddclient 3.8.3のtar ballからddclientスクリプトを取り出し、ごく小さなパッチを当てた上でDebianパッケージ版の /usr/sbin/ddclient を書き換えました。


$ sudo apt-get install ddclient libjson-any-perl
$ tar xvjf ddclient-3.8.3.tar.bz2
$ curl -L -O https://gist.github.com/hnw/3e2b08d934ce3d85ff73/raw/ddclient-3.8.3-debian.patch
$ patch -p0 < ddclient-3.8.3-debian.patch
$ sudo cp ddclient-3.8.3/ddclient /usr/sbin/ddclient

ちなみに、ddclientの動作に JSON::Any が無いと怒られるのですが、依存パッケージに含まれていないようなので手動でインストールしています。


設定ファイルを手動で設定する

Debianのddclientインストール時に設定ウィザード的なものがあれこれ質問をしてきて設定ファイルを埋めようとしてくるのですが、当然ながらCloudFlareには対応していません。Escキーを連打するなどして適当にやり過ごしましょう。


その後、改めて設定ファイルを手動で書いていきます。まず、/etc/ddclient.confの設定です。筆者は次のように記述しました。


# Configuration file for ddclient generated by debconf
#
# /etc/ddclient.conf

protocol=cloudflare

ssl=yes
use=web
server=www.cloudflare.com
zone=[CloudFlareに預けてあるドメイン名]
login=[CloudFlareのログインメールアドレス]
password=[API key]
[更新したいホスト名のFQDN]

ここまでで動作確認をしてみましょう。この際、CloudFlareの管理コンソールから今回利用したいAレコードをあらかじめ登録しておく必要があるので注意してください。うまくいけば下記のようにCloudFlareAPIを叩いてレコードが更新されるはずです。


$ sudo ddclient -daemon=0 -debug -verbose -noquiet
(略)
SUCCESS:  home.example.com -- Updated Successfully to 192.0.2.1
$

上記で正常に動作していることを確認したところで、/etc/default/ddclientを書き換え、run_daemon="true"とします。これで5分に1回外向きのIPをチェックし、必要ならCloudFlareのAレコードが自動更新される環境が手に入りました。


下記のようにserviceコマンドを使えば現在の状況が確認もできます。


$ sudo service ddclient status
● ddclient.service - LSB: Update dynamic domain name service entries
   Loaded: loaded (/etc/init.d/ddclient)
   Active: active (running) since Sun 2016-01-03 18:55:28 UTC; 7h ago
  Process: 385 ExecStart=/etc/init.d/ddclient start (code=exited, status=0/SUCCESS)
   CGroup: /system.slice/ddclient.service
           └─536 ddclient - sleeping for 230 seconds

Jan 03 18:55:28 raspberrypi systemd[1]: Started LSB: Update dynamic domain name service entries.
Jan 03 21:01:02 raspberrypi ddclient[2282]: WARNING:  TIMEOUT: checkip.dyndns.org after 120 seconds
Jan 03 22:09:44 raspberrypi ddclient[3131]: WARNING:  TIMEOUT: checkip.dyndns.org after 120 seconds
Jan 03 22:31:56 raspberrypi ddclient[3422]: WARNING:  TIMEOUT: checkip.dyndns.org after 120 seconds
Jan 03 23:15:30 raspberrypi ddclient[3947]: WARNING:  TIMEOUT: checkip.dyndns.org after 120 seconds

/var/log/以下のエラーも集約してくれて便利です。どうやら自分のIPアドレスを調べるサービスが落ちていたようですね…。


参考URL

トラックバック - http://d.hatena.ne.jp/hnw/20160104

2015年12月31日(木) 2015年をふりかえる このエントリーを含むブックマーク このエントリーのブックマークコメント

年末なので今年のふりかえりを書きます。基本的には自分用エントリなんですが、何かの参考になれば。


書いた

2015年はてなダイアリーに本記事を含め31本の記事を書きました。人気があったのは下記の記事です。



Qiitaでは79本の記事を書きました。小ネタを投稿する場所として利用していましたが、案外人気が出た記事もありました。



また、会社ブログにも4本記事を書きました。



参加した

勉強会カンファレンス発表を9件行いました。今年は招待されてお話しする機会が多かったのが特徴的でした。



また、昨年に引き続き会社主催のプログラミングコンテスト天下一プログラマーコンテスト2015」に運営スタッフとして参加しました。頭をひねりまくった問題を作った結果、「制限時間内で解ける問題じゃない」と言われるような問題ができました。本戦参加者の皆さんは化け物揃いの印象なので、これくらいの問題じゃないと止められないかなと思ったんです…。


ちなみに、AtCoderアカウントがあれば過去問にチャレンジできます。学生さんターゲットの大会ではありますが、予選は社会人の方も参加できますので、興味のある方はぜひ来年トライしてみてください。



私と同僚の若者の2人で苦心して作った問題がこちらです。良ければチャレンジしてみてください。



勉強会の主催側になった

僕はこれまで勉強会を主催したことはなかったのですが、このたび闇PHP勉強会の幹事になりまして、無事勉強会を開催することができました。



色々と不慣れで参加者の皆さまに助けて頂いている感が強いですが、僕自身が楽しみにしている勉強会なので、ボチボチ続けていきたいと考えています。


GitHubから賞金をもらった

昨年末からGitHub登録されている鍵の強度について調べていましたが、GitHubに報告したところ、バグとして認められて賞金がもらえました。この話題については賞金をもらう前ともらった後とそれぞれ社外勉強会で紹介しましたし、社内勉強会でも発表しました。



PHP7に関してあれこれ調査した

2015年はPHP7に関する発表を行う機会が多く、その下調べの意味でもかなりPHP7のソースコードを読んだ気がします。Qiitaにも調べた内容を逐一記録していったりしました。PHP7リリース後もかなり参照されているようで、なかなか良かったかなと思っています。


電子工作に興味を持った

今年の7月頃から急に電子工作に興味を持ち、自分で色々試していました。Raspberry Pi 2を買い、さらにeBayArduino互換機をバラバラに15個購入し、話題のESP-WROOM-02も入手して遊ぶなどしました。何事も初心者から初級者にステップアップするくらいの時期が一番楽しいですね。


関連して何本か記事を書きました。



スマートフォンセキュリティに興味を持った

去年から継続的にAndroidiOSセキュリティ周りに興味を持っていまして、手元のNexusroot取得してアプリに対してgdbをアタッチしてみるなどの実験をしていました。ただ、この手の話ってネット上に公開しにくいので、良い勉強会などあれば参加したいと考えています。


OSSへのcontribution

7個のプロジェクトに対してPull Requestを計25件投げました。



また、PHP本家に対して4本バグレポを送りました。



まとめ

今年はアウトプットの機会が多かったせいか、インプットとアウトプットのバランスが少し悪い1年だったような気がしています。アウトプットを減らさずにインプットを増やしたいですね。


来年もがんばるぞー

トラックバック - http://d.hatena.ne.jp/hnw/20151231

2015年12月30日(水) PHP7からstrlen関数に特化した高速化が採用された このエントリーを含むブックマーク このエントリーのブックマークコメント

(2016/01/01 02:20追記)mbstring.func_overloadの章を盛大に書き換えました。なぜか廃止されたと思い込んでたんですが、特に廃止もされてなくて、PHP7でも動くことは動きます。ただ、仕組み上strlenだけ言うことを聞かなくなっていますので、使い道としては厳しいと思います。


みなさん、もうPHP7は試してみましたか?


PHP7のセールスポイントと言えば高速化ですよね。その高速化ですが、個人的には「そこ速くする余地あったの?」と思えるような箇所が高速化されていたりします。本稿では、そうした意外な高速化ポイントの一つとしてstrlen関数に関する高速化について紹介します。


strlen関数最適化

念のため説明しておきますと、strlen関数というのは文字列の長さを返す関数です。ところで、PHPでは文字列の長さはあらかじめ文字列本体とは別に格納されています(PHP文字列はヌル文字でターミネートされていないので、文字列長がないと文字列末尾がわかりません)。つまり、元々わかっている数字を返すだけですから、strlen関数自体は大した仕事はしません。


大した仕事をしていない関数最適化の対象になるというのは少し不思議な気もしますが、そういうわけではありません。実はPHP関数呼び出しはコストが比較的高いので、関数呼び出し自体が最適化の対象になっているというわけなのです。


実際に、PHP7では関数呼び出し自体のコストを下げるような最適化も行われました(参考:「PHP7調査(16)高速な引数パーサの導入」)。しかし、strlen関数については「そもそも関数呼び出しをしない」という最適化が採用されています。なんと、strlen関数関数呼び出しではなくZend VMの1命令に格上げになっています。


このことを確認してみましょう。vld拡張でstrlen関数コンパイル結果を確認してみると、見慣れないSTRLENというopcodeが見つかります。


$ php -dextension=vld.so -dvld.active=1 -r 'strlen($foo);'
(略)
line     #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
   1     0  E >   STRLEN                                           ~1      !0
         1        FREE                                                     ~1
         2      > RETURN                                                   null
(略)
$

貴重なopcodeをこんなことで1個消費していいのか?他の関数では同じ最適化をしないのか?という疑問も湧いてきますが、頻繁に呼ばれる割に大したことをしていない数少ない関数ということなのかもしれません。


対応するCソースコードを確認する

PHP 7のZend/zend_compile.cを見ると、次のように一部の関数を特別扱いしている処理が見つかります。


int zend_try_compile_special_func(znode *result, zend_string *lcname, zend_ast_list *args, zend_function *fbc) /* {{{ */
{
        if (fbc->internal_function.handler == ZEND_FN(display_disabled_function)) {
                return FAILURE;
        }

        if (zend_string_equals_literal(lcname, "strlen")) {
                return zend_compile_func_strlen(result, args);
        } else if (zend_string_equals_literal(lcname, "is_null")) {
                return zend_compile_func_typecheck(result, args, IS_NULL);
        } else if (zend_string_equals_literal(lcname, "is_bool")) {
                return zend_compile_func_typecheck(result, args, _IS_BOOL);
        } else if (zend_string_equals_literal(lcname, "is_long")
                || zend_string_equals_literal(lcname, "is_int")
                || zend_string_equals_literal(lcname, "is_integer")
        ) {
                return zend_compile_func_typecheck(result, args, IS_LONG);
        } else if (zend_string_equals_literal(lcname, "is_float")
                || zend_string_equals_literal(lcname, "is_double")
                || zend_string_equals_literal(lcname, "is_real")
        ) {
                return zend_compile_func_typecheck(result, args, IS_DOUBLE);
        } else if (zend_string_equals_literal(lcname, "is_string")) {
                return zend_compile_func_typecheck(result, args, IS_STRING);
        } else if (zend_string_equals_literal(lcname, "is_array")) {
                return zend_compile_func_typecheck(result, args, IS_ARRAY);
        } else if (zend_string_equals_literal(lcname, "is_object")) {
                return zend_compile_func_typecheck(result, args, IS_OBJECT);
        } else if (zend_string_equals_literal(lcname, "is_resource")) {
                return zend_compile_func_typecheck(result, args, IS_RESOURCE);
        } else if (zend_string_equals_literal(lcname, "defined")) {
                return zend_compile_func_defined(result, args);
        } else if (zend_string_equals_literal(lcname, "call_user_func_array")) {
                return zend_compile_func_cufa(result, args, lcname);
        } else if (zend_string_equals_literal(lcname, "call_user_func")) {
                return zend_compile_func_cuf(result, args, lcname);
        } else if (zend_string_equals_literal(lcname, "assert")) {
                return zend_compile_assert(result, args, lcname, fbc);
        } else {
                return FAILURE;
        }
}
/* }}} */

たしかにstrlen関数のための分岐があるのがわかります。


また、strlen関数以外にもis_long関数などが特別扱いされていることがわかります。実はこれらの関数関数呼び出しを行わないような最適化が行われています。


また、assert関数も特別扱いされていることがわかります。PHP 7からassert関数の内側に任意の式を書けるようになったわけですが、assert引数を普通の引数と同じように評価するわけにはいかないので、式として保存するような処理をしてるんだろう、と想像がついたりするわけです。


mbstring.func_overloadが期待通りに動かなくなっている件とstrlen関数最適化

(ごめんなさい、この章は思い込みで書いていたので全面的に書き換えました…)


PHP 5まではmbstring.func_overloadを指定することでsubstr関数の呼び出しに対してmb_substr関数が呼ばれるような機能がありました。マルチバイト非対応のPHPアプリケーションを無理矢理マルチバイト対応にするための大昔のハックなんだと思います。


ところで、この機能は今回紹介した最適化と密接な関係があります。というのも、strlenもmbstringによる関数オーバーロードの対象だったからです。関数オーバーロード関数テーブルの置き換えにより実現されていましたが、今回のように関数呼び出しを行わないような最適化をしてしまうと同じ方法では挙動を変更できません。そのため、PHP7ではstrlenのオーバーロードは動かなくなっています。


まとめ

  • PHP7からstrlen関数のためだけにopcodeが1個増えた
  • PHP7では、一部の関数関数呼び出しされなくなった
  • mbstringのオシャレ機能mbstring.func_overloadは生き残っているが、strlen関数の置き換えが動かないので使い道がなさそう

sasezakisasezaki 2015/12/31 23:17 > そこで、「この機能って最適化の邪魔なんだけど、そもそも日本人使ってんの?」というような議論

これってどこで話されてました?参考リンクあります?

hnwhnw 2016/01/01 02:05 https://marc.info/?l=php-internals&m=142719755322933&w=2

この辺かなと。

hnwhnw 2016/01/01 02:32 その議論の末に廃止されたんだと思い込んでましたが、ちゃんと生き残ってましたね…。ご指摘ありがとうございました!記事も修正させて頂きました。

トラックバック - http://d.hatena.ne.jp/hnw/20151230

2015年12月23日(水) PHP起動時からインターン化されている文字列がどれくらいあるのか このエントリーを含むブックマーク このエントリーのブックマークコメント

このエントリは闇PHP Advent Calendar 2015の23日目です。


先日の記事「PHPのインターン化文字列とは何か」でインターン文字列について紹介しましたが、記事だけでは実際にどう使われているかピンと来なかった方も多かったかもしれません。今回、PHP 7.0.1のCLI版とPHP-FPM版とで、PHPプログラム起動直後のインターン文字列の一覧を取り出してみましたので、その結果を紹介します。


下記リンクからインターン文字列をprint_rした結果が辿れます。デカいのでご注意ください。



中を見てみると、標準関数や拡張モジュールに含まれる関数名・クラス名・メソッド名などが全てインターン化されていることがわかると思います。これほどの個数の文字列が起動時にインターン化されているというのは多くの人にとって予想外なのではないでしょうか。


また、注意深く見るとPHP-FPM環境のリストのみXdebug系の関数が登場していないこともわかります。これは、今回の方法ではPHP本体のインターン文字列しか取り出せていないためです。


前回記事でも紹介した通り、OPcacheにはインターン文字列を共有メモリに乗せる仕組みがあります。ただし、これはOPcacheが読み込まれてからでないと使えません。今回読み込んでいる拡張モジュールについて言えば、OPcacheより後で読み込まれるのはXdebugだけなので、Xdebugの提供する関数関数名だけがOPcacheの用意したインターン文字列バッファに乗り、結果としてPHP本体のインターン文字列としては見えなくなったというわけです。


いずれOPcache側のインターン文字列も確認したいと思ってるんですが、OPcacheの外からは素直にアクセスできない気がしてるんですよね。そんなことないのかな…。


補足

ちなみにPHP5系でも似たような結果になります。PHP7の方が個数が若干多くなってるのが謎ですけどね…。


さらに補足

上記リストは hnw/php-strdumper で生成しました。

トラックバック - http://d.hatena.ne.jp/hnw/20151223

2015年12月14日(月) PHP7から文字列の無駄なコピーが減った話 このエントリーを含むブックマーク このエントリーのブックマークコメント

このエントリは闇PHP Advent Calendar 2015の14日目です。


本稿では、PHP7のzend_string構造体導入によるメリットの話をします。


PHP5とPHP7の文字列型の扱い

PHP5では、文字列型の変数は次のようにメモリに割り当てられます(横幅いっぱいが8バイト)。


f:id:hnw:20151213140944p:image


文字列の本体以外はzval構造体で管理し、文字列の本体は別途メモリ確保するという形になっています。一方、PHP7では次のようになります。


f:id:hnw:20151213050154p:image


1つの文字列変数が、zval構造体とzend_string構造体の組み合わせで実現されています。


これだけ見ると、PHP7では文字列長と参照カウンタrefconuntzvalから追い出されてzend_stringに移動したくらいで、PHP5とPHP7のメモリ消費量に大きな違いは無いように思えるかもしれません(あるいはPHP7の方が不利に見えるかもしれません)。しかし、実際には参照カウンタがzend_stringに移動したことで文字列のコピー回数を抑えることができ、メモリの節約を実現できています。


以下、実例を2つ紹介していきます。


文字列操作関数での文字列コピーが減った

まずはPHP関数strtolower()の挙動について見てみましょう。下記のコードを実行すると、$a$bの値はどちらも"a"になります。


<?php
$a = chr(0x61); // "a"
$b = strtolower($a); // "a"

ところで、PHP5で上記プログラムを実行すると、$a$bとの文字列の実体は2つに分かれています。


f:id:hnw:20151213124603p:image


一方、PHP7で動かした場合は$a$bとの文字列の実体は同じになります。


f:id:hnw:20151213124604p:image


なぜこのような差が生まれるのでしょうか。PHP5のstrtolower関数の実体は次のようなコードになっています。


/* ext/standard/string.c */
PHP_FUNCTION(strtolower)
{
        char *str;
        int arglen;

        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &arglen) == FAILURE) {
                return;
        }

        str = estrndup(str, arglen);
        php_strtolower(str, arglen);
        RETURN_STRINGL(str, arglen, 0);
}

元の文字列を複製してから、php_strtolower()で小文字にするという処理になっています。この実装だと文字列の実体が別になってしまうのは必然です。PHP7の方はどうでしょうか。処理の実体であるphp_string_tolower()を見てみましょう。


/* ext/standard/string.c */
PHPAPI zend_string *php_string_tolower(zend_string *s)
{
        unsigned char *c, *e;

        c = (unsigned char *)ZSTR_VAL(s);
        e = c + ZSTR_LEN(s);

        while (c < e) {
                if (!islower(*c)) {
                        register unsigned char *r;
                        zend_string *res = zend_string_alloc(ZSTR_LEN(s), 0);

                        if (c != (unsigned char*)ZSTR_VAL(s)) {
                                memcpy(ZSTR_VAL(res), ZSTR_VAL(s), c - (unsigned char*)ZSTR_VAL(s));
                        }
                        r = c + (ZSTR_VAL(res) - ZSTR_VAL(s));
                        while (c < e) {
                                *r = tolower(*c);
                                r++;
                                c++;
                        }
                        *r = '\0';
                        return res;
                }
                c++;
        }
        return zend_string_copy(s);
}

/* Zend/zend_string.h */
static zend_always_inline zend_string *zend_string_copy(zend_string *s)
{
        if (!ZSTR_IS_INTERNED(s)) {
                GC_REFCOUNT(s)++;
        }
        return s;
}

php_string_tolower()引数zend_string *sが元々全て小文字なら参照カウンタを1増やして同じ文字列を返し、それ以外のときは新たにメモリを確保して小文字化した文字列を返すという処理になっているため、無駄な文字列コピーを減らすことができています。


逆に、PHP5でコピーが行われてしまう理由はphp_strtolower()引数char *型であるためだとも言えます。char *には参照カウンタのような概念は無いので、同じポインタをそのまま返すというわけにはいきません(参照カウンタ無しに文字列を共有してしまうと、誰がいつメモリを解放していいかわからなくなります)。かといって、文字列が入っているとは限らないzval *を渡すわけにもいきませんから、PHP5のデータ構造のままでは改善しにくい点だと言えるでしょう。一方、PHP7では内部的な文字列操作関数zend_stringを渡せるようになったので、参照カウンタを活用して無駄なコピーを避ける実装が現実的になったというわけです。


ハッシュキーでの文字列コピーが減った

今度は配列のキーに文字列を使った場合に注目してみましょう。さて、次のコードの$k$vとでは文字列のコピーが発生するでしょうか?


<?php
$a = chr(0x61); // "a"
$b = strtolower($a); // "a"
foreach ([$a => $b] as $k => $v) {
    var_dump($k, $v);
}

PHP5でも、$v$bと同じ実体となり、コピーは発生しません。一方、$k$aと別の実体になります。PHP5のハッシュ実装では文字列キーをzval *でなくchar *で管理しているため、文字列のコピーが避けられません。文字列操作関数のときと同じ問題がここでも起こっているわけです。


f:id:hnw:20151213124602p:image


PHP7の場合は$a,$b,$k,$v全ての文字列の実体が同一になります。PHP7ではハッシュ文字列キーがzend_stringで管理されるようになったので、参照カウンタを使って文字列を共有することができるのです。


f:id:hnw:20151213124601p:image


おまけ

この記事を書いていてstrtolower()/strtoupper()文字列コピーをもう少し削減できることに気付いたのでPull Requestを投げたところ、無事採用されたようです(「Optimize strtolower()/strtoupper()」)。誤差程度だろうと思いますが、PHP 7.0.2からPHPが更に速くなるかもしれません。


まとめ

PHP7では内部的な文字列の管理がzend_string構造体に統一されました。zend_stringはメンバ変数として参照カウンタを持っており、これを利用して文字列コピーの回数を減らせていることを紹介しました。zvalなど他のデータ構造の変更と同様、高速化および省メモリ化を意識した変更だと言えるでしょう。

トラックバック - http://d.hatena.ne.jp/hnw/20151214
 
ページビュー
1734129