お問い合わせはこちら
+81-80-2437-0232
hosokawa@dear-jpn.com

Cで書く誘惑に耐える(PHP/libxml2)

PHPを使ったサイトは遅いと感じることが多い。

普通にCの使えるメンバーばかりでウェブサイトを作るとなると、PHPみたいなものよりCで書いた方がいいんじゃないかという話が出た。

デバッグのやりやすさとか、サイトが簡単に落ちないようにするということを考えると普通に軽いスクリプト言語でいいやんという意見と対立。
Cが良いという側の意見は処理速度。

このメンバーならPHPでフレームワーク選んでそれに慣れて、、、という時間を考えるとCでごりごり書いても納期的にはあんまり変わらない可能性があると。もちろんこれに加えて処理速度が圧倒的に速いという意見だ。

もう一方のPHPみたいなスクリプト言語使う派は、出来上がったものの信頼性はなんだかんだ言ってもZendみたいなフレームワーク使った方が高いという点。変化の激しい今どきのウェブのことを考えると修正がやりやすいスクリプト言語系が良いとの意見。速度が遅いのはこのあたりとのトレードオフだが、ハードウェアの進化の速さやAWSみたいなクラウド環境なら遅いと感じたらPHPが動くサーバを増やしちゃえばいいという話。

結局、PHP派が勝ったのだが、それを決定づけたのが今回紹介するPHPのコードとCのコード。

ウェブアプリを作るなら当然ながら世の中に転がっているライブラリを使うのは当たり前。PHPはこのあたりを軽くラッピングしているだけなのでオーバーヘッドは少ない。

ならばライブラリ内はCの速度で動いているわけだから結局出来上がるアプリケーションの処理速度はCを使ってもせいぜい2倍程度にしかならないんじゃないかという話。
で、まずはちろっとPHPでこんなコードを書いてみた。

$ cat ul.php
<?php
function one_html() {
    $info_keys = array(
        'author' => '著者',
        'title' => 'タイトル',
        'genre' => 'ジャンル',
        'price' => '価格',
        'publish_date' => '出版日',
        'description' => '解説',
    );
    $xml = simplexml_load_file("../sample/sample.xml");
    $html = "<ul>";
    foreach ($xml->book as $book) {
        $html .= "<li";
        foreach ($book->attributes() as $kbook => $vbook) {
            $html .= " " . $kbook . '="' . $vbook . '"';
        }
        $html .= "><dl>";
        foreach ($book->children() as $child) {
            $html .= '<dt>' . $info_keys[$child->getName()] . '</dt>';
    	    $html .= '<dd>' . $child . '</dd>';
        }
        $html .= "</dl></li>";
    }
    $html .= "</ul>";
    return $html;
}

$start = microtime(true);
for ($i = 0; $i < 100000; $i++) {
    $html = one_html();
    if ($i == 0) {
        echo $html . "\n";
    }
}
$end = microtime(true);
echo ($end - $start) / 100000.0 * 1000.0 . "ms\n";

このファイル内で読み込んでいるサンプルのXMLファイルは、「サンプル XML ファイル (books.xml)」からのもらいものだ。

XMLファイルを読み込んで、liタグを使った形に変換するだけのもの。

10万回実行させて一回あたりの平均値を取得している。

次は、このPHPのコードをもとにCで書きなおしたもの。

さすがに文字列連結とかがCでは弱いので、そのあたりのコードを書く必要があったが、やっていることは同じだ。

$ cat ul.c
#include <stdio.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include "charbuf.h"
#include "timediff.h"

static const char *
getTitle(const xmlChar *key)
{
    const char *keys[] = {
        "author",
        "title",
        "genre",
        "price",
        "publish_date",
        "description",
        ""
    };
    const char *values[] = {
        "著者",
        "タイトル",
        "ジャンル",
        "価格",
        "出版日",
        "解説"
    };

    for (int i = 0; keys[i][0]; i++) {
        if (xmlStrcmp(key, BAD_CAST(keys[i])) == 0) {
            return values[i];
        }
    }
    return 0;
}

static void
one_html(struct charbuf *pbuf, const char *filename)
{
    xmlDoc *doc = NULL;
    xmlNode *root_element = NULL;
    xmlNode *book = NULL;

    doc = xmlReadFile(filename, NULL, 0);

    if (doc == NULL) {
        printf("error: could not parse file %s\n", filename);
    }

    root_element = xmlDocGetRootElement(doc);
    charbuf_append(pbuf, "<ul>");
    for (book = root_element->children; book; book = book->next) {
        if (book->type == XML_ELEMENT_NODE) {
            xmlAttr *curr_attr = NULL;
            xmlNode *curr = NULL;
            charbuf_append(pbuf, "<li");
            for (curr_attr = book->properties; curr_attr; curr_attr = curr_attr->next) {
                xmlChar* value = xmlNodeListGetString(curr_attr->doc, curr_attr->children, 1);
                charbuf_append(pbuf, " ");
                charbuf_append(pbuf, (const char *)curr_attr->name);
                charbuf_append(pbuf, "=\"");
                charbuf_append(pbuf, (const char *)value);
                charbuf_append(pbuf, "\"");
                xmlFree(value); 
            }
            charbuf_append(pbuf, "><dl>");
            for (curr = book->children; curr; curr = curr->next) {
                if (curr->type == XML_ELEMENT_NODE) {
                    xmlChar* value = xmlNodeGetContent(curr);
                    charbuf_append(pbuf, "<dt>");
                    charbuf_append(pbuf, getTitle(curr->name));
                    charbuf_append(pbuf, "</dt>");
                    charbuf_append(pbuf, "<dd>");
                    charbuf_append(pbuf, (const char *)value);
                    charbuf_append(pbuf, "</dd>");
                xmlFree(value); 
                }
            }
            charbuf_append(pbuf, "</dl></li>");
        }
    }
    charbuf_append(pbuf, "</ul>");

    xmlFreeDoc(doc);
    xmlCleanupParser();
}

int
main(int argc, char **argv)
{
    struct timeval *pstart, *pend;
    if (argc != 2)
        return 1;

    LIBXML_TEST_VERSION

    pstart = timediff_get();
    for (int i = 0; i < 100000; i++) {
        struct charbuf *pbuf = charbuf_init();
        if (pbuf == 0) {
            return 1;
        }

        one_html(pbuf, argv[1]);

        if (i == 0) {
            printf("%s\n", charbuf_get(pbuf));
        }
        charbuf_uninit(pbuf);
    }
    pend = timediff_get();

    printf("%lfms\n", timediff_diff(pstart, pend) / 100000.0 * 1000.0);
    timediff_free(pstart);
    timediff_free(pend);

    return 0;
}

上のメインのコードで使用しているcharbuf関係のコードは以下のとおり。

$ cat charbuf.c
#include <stdlib.h>
#include <string.h>
#include "charbuf.h"

struct charbuf *
charbuf_init()
{
    struct charbuf *pcharbuf = calloc(1, sizeof(struct charbuf));
    if (pcharbuf) {
        pcharbuf->current_buf_size = CHARBUF_DEFAULT_LENGTH;
        pcharbuf->charbuf = malloc(pcharbuf->current_buf_size);
        if (pcharbuf->charbuf == 0) {
            free(pcharbuf);
            return 0;
        }
    }
    return pcharbuf;
}

void
charbuf_uninit(struct charbuf *pcharbuf)
{
    free(pcharbuf->charbuf);
    free(pcharbuf);
}

const char *
charbuf_append(struct charbuf *pcharbuf, const char *str)
{
    int new_size;
    int str_len = strlen(str);
    for (new_size = pcharbuf->current_buf_size; new_size < str_len + pcharbuf->current_length + 1; new_size *= 2)
        ;
    if (new_size != pcharbuf->current_buf_size) {
        char *new_charbuf = realloc(pcharbuf->charbuf, new_size);
        if (new_charbuf == 0) {
            return 0;
        }
        pcharbuf->charbuf = new_charbuf;
        pcharbuf->current_buf_size = new_size;
    }
    strcpy(pcharbuf->charbuf + pcharbuf->current_length, str);
    pcharbuf->current_length += str_len;
    return pcharbuf->charbuf;
}
$ cat charbuf.h
struct charbuf {
    char *charbuf;
    int current_length;
    int current_buf_size;
};

#define CHARBUF_DEFAULT_LENGTH (1024 * 16)

extern struct charbuf *charbuf_init();
extern void charbuf_uninit(struct charbuf *pcharbuf);
#define charbuf_get(pcharbuf) ((pcharbuf)->charbuf)
extern const char *charbuf_append(struct charbuf *pcharbuf, const char *str);

同様にtimediff関連のコードは以下の通り。

$ cat timediff.c
#include <stdlib.h>
#include <sys/time.h>

struct timeval *timediff_get()
{
    struct timeval *ptv = calloc(1, sizeof(struct timeval));
    if (!ptv) {
        return 0;
    }
    gettimeofday(ptv, NULL);
    return ptv;
}

void timediff_free(struct timeval *ptv)
{
    free(ptv);
}

double timediff_diff(struct timeval *start, struct timeval *end)
{
    double start_ms, end_ms, diff;
     
    start_ms = (double)start->tv_sec * 1000000 + (double)start->tv_usec;
    end_ms = (double)end->tv_sec * 1000000 + (double)end->tv_usec;
     
    diff = ((double)end_ms - (double)start_ms) / 1000000;
     
    return diff;
}
$ cat timediff.h
extern struct timeval *timediff_get();
extern void timediff_free(struct timeval *ptv);
extern double timediff_diff(struct timeval *start, struct timeval *end);

これらをコンパイル、リンクするためのMakefileはこんな形。たぶんclangの代わりにgccでも行けると思う。

all: ul

clean:
	rm -f ul *.o

ul: ul.o charbuf.o timediff.o
	clang -O2 -Wall -o ul ul.o charbuf.o timediff.o `xml2-config --libs`

ul.o: ul.c charbuf.h timediff.h
	clang -O2 -Wall -c -o ul.o `xml2-config --cflags` ul.c

charbuf.o: charbuf.c charbuf.h
	clang -O2 -Wall -c -o charbuf.o charbuf.c

timediff.o: timediff.c timediff.h
	clang -O2 -Wall -c -o timediff.o timediff.c

というわけで、衝撃の処理時間の比較。

PHP版:0.13061670064926ms
C版:0.090119ms

思っていたほどには差がつかなかった。Cの側が率にして3割ちょい速くなっている程度。
何回か実行させて計測してみたが、この傾向は変わらず。
今回のように処理時間の大半がXMLライブラリ(libxml2)内で時間が消費されているような場合だからこの結果で、もう少しフレームワークのような大きめのロジックの入っているコードが中心だとこのような結果にはならないのかもしれないが。