MasterYodaの昼下がり

日々の備忘録的なアレ

phpの==の挙動

phpの==はお節介な挙動をみせる。
散々言われてることだけど、厳密比較(===)を使うようにしよう。
さもないと思わぬバグを生み出す。

phpの==は、よしなに型を合わせる。
それが便利なこともあるが、大抵の場合は余計なお世話である。

サンプル

<?php

if (1 == '1_1') {
    echo "True!?";
}

結果

True!?

phpの型の相互変換の挙動(特に文字列の数値への変換)を知らないとまさか!?と思うような結果になる。

数値として文字列が評価された時の挙動として、

文字列の最初の部分により値が決まります。文字列が、 有効な数値データから始まる場合、この値が使用されます。

サンプルの例だと、1_1なので、1に変換されif (1 == 1)で真となる。
厳密比較を使うと、当然ながら偽となる。

マニュアルの比較演算子の項から抜粋

名前 結果
$a == $b 等しい 型の相互変換をした後で $a が $b に等しい時に TRUE。
$a === $b 等しい $a が $b に等しく、および同じ型である場合に TRUE 。

大事なことだからもう一度
===を使おう

HashMapの最適化

参考資料
http://docs.oracle.com/javase/jp/8/api/java/util/HashMap.html

HashMapのインスタンスには、その性能に影響を与える2つのパラメータである初期容量および負荷係数があります。容量はハッシュ表のバケット数であり、初期容量は単純にハッシュ表が作成された時点での容量です。負荷係数は、ハッシュ表がどの程度いっぱいになると、その容量が自動的に増加されるかの基準です。ハッシュ表エントリ数が負荷係数と現在の容量の積を超えると、ハッシュ表のハッシュがやり直され (つまり、内部データ構造が再構築され)、ハッシュ表のバケット数は約2倍になります。

JavaのHashMapのコンストラクタは、HashMap内部にもつHashTableの初期容量と負荷係数の値を渡せるようになっている。
初期容量のデフォルトは16、負荷係数は0.75

ドキュメントには、HasmMapの要素数が初期容量と負荷係数の積を超えると内部データ構造が再構築されるとある。
この再構築(rehash)が結構コストが高い。

コンストラクタに何も渡さなかった場合、
16(初期容量のデフォルト) * 0.75(負荷係数のデフォルト) = 12
を超えた時点でrehashが実行される。

格納される要素数がある程度事前にわかっている場合、初期容量に対して適切な値を設定してインスタンスを生成すると効率的。
ただ大きい値を設定すれば良いというわけではない。
ドキュメントにも、

反復処理の性能が重要な場合は、初期容量をあまり高く(負荷係数をあまり低く)設定しないことが非常に重要

とある

初期容量の値は、以下の計算で求めると良い。

挿入される要素数(予想される要素数) * 4/3

この素数 * 4/3という値は、負荷係数0.75を掛けた時にちょうど1になる値
負荷係数はほとんどの場合、変更する必要は無いと思う。

簡単に処理時間を計測してどの程度効果があるのか試してみる。

サンプルコード

package main;

import java.util.HashMap;

public class HashMapSample {

    public static void main(String[] args) {

        long start;
        long stop;
        HashMap<Integer, Integer> map;

        start = System.currentTimeMillis();
        map = new HashMap<Integer, Integer>();
        for (int i = 0; i < 100000; i++) {
            map.put(i, i);
        }
        stop = System.currentTimeMillis();
        System.out.println("デフォルト:" + (stop - start) + "msec");

        start = System.currentTimeMillis();
        map = new HashMap<Integer, Integer>(130000);
        for (int i = 0; i < 100000; i++) {
            map.put(i, i);
        }
        stop = System.currentTimeMillis();
        System.out.println("初期容量130000:" + (stop - start) + "msec");

    }

}

結果

デフォルト:44msec
初期容量130000:23msec

今回は要素数を多めにして試してみたので、時間的には大きめに出てる。
素数少なめでも1msecぐらいは効果があったので、何回も実行されるような箇所なら地味に効いてきそう。

cloneの挙動

http://php.net/manual/ja/language.oop5.cloning.php

オブジェクトのクローンが作成される際、PHP 5 は、そのオブジェクトのプロパティを 全てシャローコピーします。他の変数へのリファレンスを保持する全てのプロパティは、 リファレンスのままとなります

勝手にディープコピーだと思い込んで、厄介なバグを生みかけた。 内部で保持してるオブジェクトも別のインスタンスになると思ってると厄介なことになる。 マニュアルはちゃんと読もう。

リファレンスが保持されていて、シャローコピーされるということは、cloneしても同じオブジェクトへの参照を保持してるわけで、 片方のオブジェクト経由で適用した変更は、もう片方のcloneしたオブジェクト経由で取得した際にも当然反映されてることになる。

サンプルコード

<?php

class A {

    private $object;

    public function getObject()
    {
        if (!$this->object) {
            $this->object = new B();
        }
        return $this->object;
    }

}

class B {

    private $count = 0;

    public function inc()
    {
        $this->count++;
    }

    public function getCount()
    {
        return $this->count;
    }

}

// $a->getObjectしてBのカウントをインクメント
$a = new A();
$a->getObject()->inc();

// $aをclone
$a_clone = clone $a;

// ここで一回$a、$a_cloneそれぞれのBオブジェクトのカウントを出力
// 両方1が出力される。
print($a->getObject()->getCount() . "\n");
print($a_clone->getObject()->getCount() . "\n");

// $aで保持してるBオブジェクトのカウントをインクリメント
$a->getObject()->inc();

// $a、$a_cloneそれぞれで保持してるBオブジェクトのカウントを出力
// $a,$a_cloneそれぞれで保持してるのは、Bオブジェクトへのリファレンスの為、
// 両方2が出力される
print($a->getObject()->getCount() . "\n");
print($a_clone->getObject()->getCount() . "\n”);

実行結果

$ php clone_test.php
1
1
2
2
$

サンプルコード中で$a->getObject()を実行する前に($aでBへの参照を保持する前に)、cloneすれば$a、$a_cloneそれぞれのBへの参照は別のインスタンスへの参照となる。

usort、uasortの罠

phpのusort、uasort関数はユーザ定義の比較関数で配列の要素をソートできる便利な関数。

がしかし、この関数には思わぬ罠がある。

ユーザ定義関数の処理中で、下記3つの処理のいづれかが行われるとphp warningが発生する。

  • sort対象のデータをvar_dumpもしくはprint_r

  • debug_backtrace()を実行

  • Exceptionを投げる、もしくはExceptionのインスタンスを生成

発生するphp warningの内容は、

Array was modified by the user comparison function

という内容のもの。

set_error_handlerなんかでwarningを捕まえてエラーにしている環境の場合、 思わぬところで落ちたりする。

冒頭で罠と書いたが、この挙動はphpのバグらしい。

https://bugs.php.net/bug.php?id=50688

サンプルコード

<?php

$list = array(2,1);

function cmp($a, $b)
{
    if ($a === $b) {
        return 0;
    }

    $e = new Exception();

    return $a - $b;
}

usort($list,"cmp");
var_dump($list);

結果

PHP Warning:  usort(): Array was modified by the user comparison function in /tmp/test.php on line 16
array(2) {
  [0]=>
  int(1)
  [1]=>
  int(2)
}

system関数の戻り値

昔のメモを見返してたらperlのsystem関数で実行した外部コマンドのリターンコードをうまく取得できずにハマったことが書いてあるのを見つけた。新卒2年目でperlって何?美味しいの??状態であったのが思い出される。 

ハマったポイントは、リターンコードを取得してそのまま利用してたら256とか返ってきて「エッ」ってなった点。当時はsshでコマンド投げて、コマンドの実行ステータスを取ろうとしてsshのリターンコードに256ってあったっけとか思いながらmanページを小一時間眺めてた。

system関数について
参考情報:perldocの日本語訳サイトから
http://perldoc.jp/func/system

上記の説明にも、
返り値は、wait が返すプログラムの exit 状態です。実際の exit 値を得るには 右に 8 ビットシフトしてください
とある。

このことがどういうことか理解するには以下を参照すると良い
http://perldoc.jp/variable/%24%3F

perldocの説明によると、system関数の戻り値は
伝統的な Unix の wait() システムコールが返した 16 ビットのステータス
とある。

Unixのwaitシステムコールが返すステータスは、子プロセスの終了時の情報がまとめられた16ビットの情報として返される。
また上位8ビットが子プロセスが設定した終了ステータスを示すことになっている。

その為、system関数で実行した外部コマンドの戻り値を取得するには、system関数の戻り値を8ビット右シフトしなければならない。

残りの8ビットは、
そのプロセスを止めたシグナルの番号($? & 127)
コアダンプがあるかどうか($? & 128)
を示す。

サンプルコード

#!/usr/bin/perl

sub main {

$re = system("~/tmp/perl_test/return1_test.sh");
$bit_shift_re = $re >> 8; # 8ビット右シフト
print $re . "\n";
print $bit_shift_re . "\n";

}

&main($ARGV);

サンプルコード中で実行しているシェルはexit 1しているだけのテスト用のもの

実行結果

$ ./test.pl 
256
1
$

androidでホスト名に_(アンダースコア)が入っていると通信に失敗する

タイトルの通りandroidでホスト名に_(アンダースコア)が入っていると通信に失敗する。
同じホストに対しiosで通信を試みたところ問題は発生しなかった。

通信失敗時のスタックトレース

E/AndroidRuntime(1278):Causedby: java.lang.NullPointerException
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpConnection$Address.hashCode(HttpConnection.java:343)
E
/AndroidRuntime(1278):>---at java.util.HashMap.get(HashMap.java:298)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpConnectionPool.get(HttpConnectionPool.java:67)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpConnection.connect(HttpConnection.java:128)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpEngine.openSocketConnection(HttpEngine.java:308)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:460)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:432)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:282)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:232)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:273)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:479)
E
/AndroidRuntime(1278):>---at libcore.net.http.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:133)

コードから原因を追っていったところ、スタックトレースにも出ているとおり
libcore.net.http.HttpConnection$Address.hashCodeメソッド内の
uriHost.hashCode()NullPointerExceptionが発生していた。

uriHostjava.net.URIクラスのgetHostメソッドの結果が格納されている。
java.net.URIクラスはRFC2396に準拠しており、ホスト名に使用する文字は以下のように定義されている。

RFC2396
3.2.2.Server-based NamingAuthority

hostport
= host [":" port ]
host
= hostname |IPv4address
hostname
=*( domainlabel ".") toplabel ["."]
domainlabel
= alphanum | alphanum *( alphanum |"-") alphanum
toplabel
= alpha | alpha *( alphanum |"-") alphanum

上記のようにホスト名として定義されてる文字として_(アンダースコア)は定義されておらず、
ライブラリの実装によっては問題を引き起こす模様/(^o^)\。

今回の事象の場合、ホスト名に_(アンダースコア)が入っているとjava.net.URI.getHost()の返り値がnullになってしまう。
という予想が立ったので以下のテストコードで挙動を確認。

・テストコード
URLは仮のもの

package test;

import java.net.URI;
import java.net.URISyntaxException;

public class UriWithUnderScoreTest {

/**
* @param args
* @throws URISyntaxException
*/

public static void main(String[] args) throws URISyntaxException {
// TODO Auto-generated method stub

String uri_underscore ="http://api_dev.hogehoge.net/player/get";
System.out.println("uri="+ uri_underscore);
System.out.println("URI.getHost()="+new URI(uri_underscore).getHost());

String uri_hyphen ="http://api-dev.hogehoge/player/get";
System.out.println("uri="+ uri_hyphen);
System.out.println("URI.getHost()="+new URI(uri_hyphen).getHost());
}

}

・結果

uri=http://api_dev.hogehoge.net/player/get
URI
.getHost()=null
uri
=http://api-dev.hogehoge.net/player/get
URI
.getHost()=api-dev.hogehoge.net

結果に出力されたとおり、ホスト名に_(アンダースコア)が入っているとnullが返り、
_(アンダースコア)を-(ハイフン)に置き換えたところ期待どおりの結果が返ってくる。

pymysqlでbulkインサート

pymysqlでbulkインサートする方法メモ

executeの代わりにexecutemanyを使う
該当コードは以下
https://github.com/PyMySQL/PyMySQL/blob/master/pymysql/cursors.py#L136

PyMySQLのversionが0.6.1以下だとforループでexecute発行しているだけなので、versionを上げないとあまり意味がない。
0.6.1
https://github.com/PyMySQL/PyMySQL/blob/pymysql-0.6.1/pymysql/cursors.py#L106-112
0.6.2
https://github.com/PyMySQL/PyMySQL/blob/pymysql-0.6.2/pymysql/cursors.py#L136-181

PyMysqlのversionを上げるにはpipを使ってれば以下

pip install --upgrade pymysql
$ pip install --upgrade pymysql
Downloading/unpacking pymysql from https://pypi.python.org/packages/source/P/PyMySQL/PyMySQL-0.6.2.tar.gz#md5=5d7016e8f13e6c4edb539aa736896ffe
DownloadingPyMySQL-0.6.2.tar.gz (53kB):53kB downloaded
Running setup.py egg_info forpackage pymysql

Installing collected packages: pymysql
Found existing installation:PyMySQL0.6.1
UninstallingPyMySQL:
Successfully uninstalled PyMySQL
Running setup.py install for pymysql

Successfully installed pymysql
Cleaning up...
$

実装例

import pymysql
conn
=MySQLdb.connect()
cur
= conn.cursor()
sql
='INSERT INTO test VALUES (%s,%s,%s,%s)'
param
=[]
for data in target_datas:
param
.append([data[0],data[1],data[2],data[3]])
cur
.executemany(sql,param)
conn
.commit()

VALUES (%s)の%sをパラメータの数と合わせないといけないことに気づかず若干ハマった。(当たり前である。
gziip&msgpack化したデータをblob型へ突っ込むのもこのままでいける