++i は i++ よりも速い?

PHPコーディングに関する最適化TIPS 2009 の中に以下のようなものがあり、巷で話題になっています。

  • ++$i は $i++ より速い

つまり awk 的に書くと

  • ++i (プレインクリメント) は i++ (ポストインクリメント) より速い

ということですが、本当なのでしょうか?

こういうものは論より証拠で実証してみます。

$ time nawk 'BEGIN{for(i=1;i<=10000000;++i){}}'
nawk 'BEGIN{for(i=1;i<=10000000;++i){}}'  1.23s user 0.00s system 99% cpu 1.245 total

$ time nawk 'BEGIN{for(i=1;i<=10000000;i++){}}'
nawk 'BEGIN{for(i=1;i<=10000000;i++){}}'  1.53s user 0.00s system 98% cpu 1.549 total

ざっくりではありますが、10000000 回 (一千万回) のループで 0.3 秒ほどプレインクリメントの方が高速であることが分かります。 ここに詳細な記事が書かれていますが nawk のソースを見てみましょう。

Yacc ファイルである awkgra.y に変数と ++ や -- の位置についての解析情報が書かれています。

        | DECR var                      { $$ = op1(PREDECR, $2); }
        | INCR var                      { $$ = op1(PREINCR, $2); }
        | var DECR                      { $$ = op1(POSTDECR, $1); }
        | var INCR                      { $$ = op1(POSTINCR, $1); }

ytab.c の中でこれらの振る舞いを示しており、awkgram.y を参照しています。

case 148:
#line 368 "awkgram.y"
{ yyval.p = op1(PREDECR, yyvsp[0].p); }
break;
case 149:
#line 369 "awkgram.y"
{ yyval.p = op1(PREINCR, yyvsp[0].p); }
break;
case 150:
#line 370 "awkgram.y"
{ yyval.p = op1(POSTDECR, yyvsp[-1].p); }
break;
case 151:
#line 371 "awkgram.y"
{ yyval.p = op1(POSTINCR, yyvsp[-1].p); }
break;

maketab.c の中でこれらの演算子の記述について定義されています。

        { PREINCR, "incrdecr", "++" },
        { POSTINCR, "incrdecr", "++" },
        { PREDECR, "incrdecr", "--" },
        { POSTDECR, "incrdecr", "--" },

具体的な処理は run.c の中に書かれています。

Cell *incrdecr(Node **a, int n)         /* a[0]++, etc. */
{
        Cell *x, *z;
        int k;
        Awkfloat xf;

        x = execute(a[0]);
        xf = getfval(x);
        k = (n == PREINCR || n == POSTINCR) ? 1 : -1;
        if (n == PREINCR || n == PREDECR) {
                setfval(x, xf + k);
                return(x);
        }
        z = gettemp();
        setfval(z, xf);
        setfval(x, xf + k);
        tempfree(x);
        return(z);
}

つまり、プレインクリメント (PREINCR) またはポストインクリメント (POSTINCR) の場合に k (インクリメントされる数) が 1 に設定され、それ以外は -1 (プレデクリメントとポストデクリメント) が設定されています。

次に、プレインクリメント (PREINCR) とプレデクリメント (PREDECR) の場合には k を足したものを返していますが、それ以外 (ポストインクリメントとポストデクリメント) の場合には gettemp() で一時的に z を格納して k を足して、tmpfree(x) で x を z にコピーしたものを返しています。

このことは (ソースを追っているわけではありませんが) PHP も同じようで、前述の URL に翻訳された物が出ています。

なぜか? ポストインクリメントの場合、PHPはなんらかの値を持つ変数をコピーする必要があり、
コピー後に値のインクリメントを行ってからインクリメント前に保存しておいた値を
返すという動作をしている。

gawk のものは++i/i++ gawk のばあいに書かれています。 実際には最後の部分でポストインクリメントとポストデクリメントは関数を tmp_number() を呼び、ここからさらに mk_number() が呼ばれているため gawk でも遅くなっているのではないかと思われます。

OS がマルチタスクの OS なので実際にどのくらいの差があれば、本当に差があると言えるかは微妙なところですが、nawk, mawk, busybox awk, gawk, xgawk でも同じようにプレインクリメントの方が高速に動作するようです。

awk でプレインクリメントとポストインクリメントの速度差が問題になるケースは滅多にないと思いますが、頭の隅に置いておくとよいでしょう。

この記事については曖昧な部分が多いので、修正が必要なところがあれは言っていただければ修正します。

tag_nawk.pngtag_nawk.pngtag_nawk.pngtag_nawk.png