memcachedサーバへのアクセスモジュールは数ありますが、一般的にはlibmemcachedが使われる事が多いと思います。
Perlにおいても
- Cache::Memcached
- Cache::Memcached::Fast
- Cache::Memcached::libmemcached
- Memcached::libmemcached
と数種類存在し、一般的に使用されるlibmemcachedのラッパインタフェースであるCache::Memcached::libmemcachedが使われる事が多い様に思います。
libmemcachedに関しては、
以前Windowsへのポーティングを行い、
オフィシャルへのパッチ送付も行いました。
成果物としては
codereposに置いてあります。
オフィシャルからもリンクを張って頂けるようになりました。
さらにCache::Memcached::Fastについても、
以前Windowsへのポーティングを行い、Cache::Memcached::Fast version 0.13に取り込まれました。
つまり特殊な事をせずにWindowsから利用出来る高速なPerlのmemcachedクライアントライブラリとしてはCache::Memcached::Fastになります。
ちなみこのCache::Memcached::Fast、実はlibmemcachedと比較しても格段に速く、
tokuhiromさんが取ったベンチマークでも素晴らしい結果を叩き出してくれています。
さて今日は、このCache::Memcached::Fastが内部で使用しているXSクライアントモジュールを使用して、高速にmemcachedにアクセスする物を作ってみたいと思います。
このCache::Memcached::FastのXSコードは、Perlに依存した部分とPerlに依存していない部分で分けられており、その後者は一般的なC言語のソースから呼び出しが可能になっています。
なぜこのCache::Memcached::Fastが速いかと言うと、libmemcachedの様に逐次送信を行っているのではなくwritev(2)を使った一括送信を行っているからです。またCache::Memcached::Fastはselect(2)ではなくpoll(2)を使っている為、FD_SETの設定を毎回行わなくて良いというのも微量ではありますが影響しているのではないかと思っています。
このクライアントモジュールは、libmemcachedの様に単一取得(memcached_get)や複数取得(memcached_mget)のAPI呼び出し時に逐次送受信されるのではなく、client_prepare_get/client_prepare_setを使った前準備方式を使っています。これにより複数の問い合わせに対しても内部では一括送信してくれ、一括で受信してくれます。libmemcachedは複数の問い合わせに対してそれぞれ結果待ちをし、全ての問い合わせが完了した時点で制御が戻ります。これについてはMEMCACHED_BEHAVIOR_NO_BLOCKを使用する事でよく似た動作をする事が出来ます。
このクライアントモジュールを使用した実際のサンプルコードは以下の様になります。
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "client.h"
static void* alloc_value(value_size_type value_size, void **opaque) {
*opaque = (char *) malloc(value_size + 1);
memset(*opaque, 0, value_size + 1);
return (void *) *opaque;
}
static void free_value(void *opaque) {
free(opaque);
}
static void result_store(void *arg, void *opaque, int key_index, void *meta) {
char* res = (char*) opaque;
if (res) *(char**)arg = strdup(res);
else *(char**)arg = strdup("");
}
int main(int argc, char* argv[]) {
struct client* c = NULL;
const char* host = "127.0.0.1";
const char* port = "11211";
const char* key = "foo";
const char* value = "bar";
struct result_object object;
char* result = NULL;
int ret;
c = client_init();
client_add_server(c, host, strlen(host), port, strlen(port), 1.0, 1);
object.alloc = NULL;
object.store = result_store;
object.free = NULL;
object.arg = NULL;
client_reset(c, &object, 1);
printf("setting value of '%s' as '%s'\n", key, value);
client_prepare_set(c, CMD_SET, 0, key, strlen(key), 0, 0, value, strlen(value));
client_execute(c);
object.alloc = alloc_value;
object.store = result_store;
object.free = free_value;
object.arg = &result;
client_reset(c, &object, 0);
printf("getting value of '%s'\n", key);
client_prepare_get(c, CMD_GET, 0, key, strlen(key));
client_execute(c);
printf("result value of '%s' is '%s'\n", key, result);
free(result);
return 0;
}
少し変わったコードになりますが、メモリの確保から開放まで自分でハンドリングでき、自前の構造を使った処理も行えるかと思います。
なお、libmemcachedとCache::Memcached::Fastのクライアントモジュールでget/setを繰り返すベンチマークを取ってみました。
まずはlibmemcachedのコード
#include <winsock2.h>
#include <memcached.h>
#include <stdio.h>
#define SERVER_NAME "127.0.0.1"
#define SERVER_PORT 11211
#define KEY "foo"
#define VALUE "bar"
int main(void) {
memcached_return rc;
memcached_st *memc;
char* value;
int value_length = 0;
int flags = 0;
int n;
memc = memcached_create(NULL);
memcached_server_add(memc, SERVER_NAME, SERVER_PORT);
for (n = 0; n < 30000; n++) {
memcached_set(memc, KEY, strlen(KEY), VALUE, strlen(VALUE), 0, 0);
value = memcached_get(memc, KEY, strlen(KEY), &value_length, &flags, &rc);
}
memcached_free(memc);
return 0;
}
次にCache::Memcached::Fast(CMF)のクライアントモジュールのコード
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include "client.h"
#define SERVER_NAME "127.0.0.1"
#define SERVER_PORT "11211"
#define KEY "foo"
#define VALUE "bar"
static void* alloc_value(value_size_type value_size, void **opaque) {
*opaque = (char *) malloc(value_size + 1);
memset(*opaque, 0, value_size + 1);
return (void *) *opaque;
}
static void free_value(void *opaque) {
free(opaque);
}
static void result_store(void *arg, void *opaque, int key_index, void *meta) {
char* res = (char*) opaque;
if (res) *(char**)arg = strdup(res);
else *(char**)arg = strdup("");
}
int main(int argc, char* argv[]) {
struct client* c = NULL;
struct result_object object_set, object_get;
char* result = NULL;
int n;
c = client_init();
client_add_server(c, SERVER_NAME, strlen(SERVER_NAME), SERVER_PORT, strlen(SERVER_PORT), 1.0, 1);
object_set.alloc = NULL;
object_set.store = result_store;
object_set.free = NULL;
object_set.arg = NULL;
object_get.alloc = alloc_value;
object_get.store = result_store;
object_get.free = free_value;
object_get.arg = &result;
for (n = 0; n < 30000; n++) {
client_reset(c, &object_set, 1);
client_prepare_set(c, CMD_SET, 0, KEY, strlen(KEY), 0, 0, VALUE, strlen(VALUE));
client_execute(c);
client_reset(c, &object_get, 0);
client_prepare_get(c, CMD_GET, 0, KEY, strlen(KEY));
client_execute(c);
}
free(result);
return 0;
}
計測結果は
libmemcached: 6.953125
CMF: 5.984375
となりました。get/setのループだけなのにCMFは速いですね。
Perlに依存していないので、通常アプリケーションでも問題なく使えるかと思います。
memcachedと通信するC言語で作ったプログラムのパフォーマンスが悪いと思われたならば、一度試して見られてはどうでしょうか?