C/C++

unix domain socketよりもlocal宛のtcp socketの方が速いこともある

こんばんは!
毎日投稿するぞって思ってるんですけどなかなか難しいですね。3日ぶりの更新となってしまいました(-_-;)

さて、今回は前回(unix domainのソケット通信)前々回(ソケット通信)の続きとしてUNIXソケットとTCPソケットの性能比較を行っていきたいと思って書き始めたんですよ、で、いざ性能測定してみたら自分の信じてた常識が崩壊して途方に暮れながら記事を書いている今日この頃です。

流石に長ったらしい検証コードの後ろに結果書くのは見にくいかと思ったので先に結果から説明しますと、タイトルにも書いたんですけど、ループバックアドレス(127.0.0.1)宛のTCPソケット(IPv4)の方がUNIXドメインソケットの方が速かったんですよ。
今までプロセス間通信としてUNIXドメインソケットを愛用してきただけに衝撃的でした。

以下結果です。

データ[KiB]
(各100万回)
TCP[sec]UNIX[sec]
11.01.33
41.562.17
164.206.91

一応各10回ずつの測定で、docker上のCentOS7での結果です。テストコードはboost等のライブラリは使用していない生のCで書いてます。

UNIXドメインソケットの方が速いと今まで信じてきていたので本当に衝撃の結果でした(2回目)。


以下、テストに使用したソースコードです。

サーバ側ソース

#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 55555
#define UNIX_SOCKET_PATH "/tmp/test.unixsocket"
static int data_size_g; // データサイズ
static int send_num_g; //送信数
int server(bool inet);
// 引数はデータサイズと送信数
int main(int argc, char *argv[]) {
int ret_code = 0;
int i = 0;
char *endptr;
int test_num;
// コマンドライン引数の取得(getopt使うべきだったな...)
if (argc == 4) {
data_size_g = strtol(argv[1], &endptr, 10);
printf("data_size_g=%d\n", data_size_g);
if (data_size_g == 0 || argv[1] == '\0' || *endptr != '\0') {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size recv_num test_num\n");
return EXIT_FAILURE;
}
send_num_g = strtol(argv[2], &endptr, 10);
printf("send_num_g=%d\n", send_num_g);
if (send_num_g == 0 || argv[2] == '\0' || *endptr != '\0') {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size recv_num test_num\n");
return EXIT_FAILURE;
}
test_num = strtol(argv[3], &endptr, 10);
printf("test_num=%d\n", test_num);
if (test_num == 0 || argv[3] == '\0' || *endptr != '\0') {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size recv_num test_num\n");
return EXIT_FAILURE;
}
} else {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size recv_num test_num\n");
return EXIT_FAILURE;
}
// tcpソケットのサーバー処理
printf("start server\n");
for (i = 0; i < test_num; i++) {
ret_code = server(true);
if (ret_code != 0) {
printf("error\n");
return EXIT_FAILURE;
}
ret_code = server(false);
if (ret_code != 0) {
printf("error\n");
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}
int server(bool inet) {
int ret_code = 0;
char *buf;
int yes = 1;
int fd_accept = -1; // 接続受け付け用のFD
int fd_other = -1; // sendとかrecv用のFD
ssize_t size = 0;
int flags = 0; // MSG_WAITALLとかMSG_NOSIGNALをよく使うけど今回はサンプルなのでフラグは無し
// inetフラグがtrueの場合はINETドメインのソケットを作る
if (inet == true) {
// ソケットアドレス構造体
struct sockaddr_in sin, sin_client;
memset(&sin, 0, sizeof(sin));
memset(&sin_client, 0, sizeof(sin_client));
socklen_t socklen = sizeof(sin_client);
// インターネットドメインのTCPソケットを作成
fd_accept = socket(AF_INET, SOCK_STREAM, 0);
if (fd_accept == -1) {
printf("failed to socket(errno=%d, %s)\n", errno, strerror(errno));
return -1;
}
// REUSEADDRを設定しておかないと連続でbind()できない。sleepするのもメンドイし連続でテストしたいので。
ret_code = setsockopt(fd_accept, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
if (ret_code == -1) {
printf("failed to setsockopt(errno=%d, %s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
// ソケットアドレス構造体を設定
sin.sin_family = AF_INET; // インターネットドメイン(IPv4)
sin.sin_addr.s_addr = INADDR_ANY; // 全てのアドレスからの接続を受け入れる(=0.0.0.0)
sin.sin_port = htons(SERVER_PORT); // 接続を待ち受けるポート
// 上記設定をソケットに紐づける
ret_code = bind(fd_accept, (const struct sockaddr *)&sin, sizeof(sin));
if (ret_code == -1) {
printf("failed to bind(errno=%d, %s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
// ソケットに接続待ちを設定する。10はバックログ、同時に何個迄接続要求を受け付けるか。
ret_code = listen(fd_accept, 10);
if (ret_code == -1) {
printf("failed to listen(errno=%d, %s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
printf("accept wating...\n");
// 接続受付処理。接続が来るまで無限に待つ。recvのタイムアウト設定時はそれによる。シグナル来ても切れるけど。
fd_other = accept(fd_accept, (struct sockaddr *)&sin_client, &socklen);
if (fd_other == -1) {
printf("failed to accept(errno=%d, %s)\n", errno, strerror(errno));
return -1;
}
} else {
remove(UNIX_SOCKET_PATH); // socket作る前に前回のファイルを消しておく。終了処理でやってもいいけど。
// ソケットアドレス構造体←今回はここがUNIXドメイン用のやつ
struct sockaddr_un sun, sun_client;
memset(&sun, 0, sizeof(sun));
memset(&sun_client, 0, sizeof(sun_client));
socklen_t socklen = sizeof(sun_client);
// UNIXドメインのソケットを作成
fd_accept = socket(AF_LOCAL, SOCK_STREAM, 0);
if (fd_accept == -1) {
printf("failed to socket(errno:%d, error_str:%s)\n", errno, strerror(errno));
return -1;
}
// REUSEADDRを設定しておかないと連続でbind()できない。sleepするのもメンドイし連続でテストしたいので。
ret_code = setsockopt(fd_accept, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes));
if (ret_code == -1) {
printf("failed to setsockopt(errno=%d, %s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
// ソケットアドレス構造体を設定
sun.sun_family = AF_LOCAL; // UNIXドメイン
strcpy(sun.sun_path, UNIX_SOCKET_PATH); // UNIXドメインソケットのパスを指定
// 上記設定をソケットに紐づける
ret_code = bind(fd_accept, (const struct sockaddr *)&sun, sizeof(sun));
if (ret_code == -1) {
printf("failed to bind(errno:%d, error_str:%s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
// ソケットに接続待ちを設定する。10はバックログ、同時に何個迄接続要求を受け付けるか。
ret_code = listen(fd_accept, 10);
if (ret_code == -1) {
printf("failed to listen(errno:%d, error_str:%s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
printf("accept wating...\n");
// 接続受付処理。接続が来るまで無限に待つ。recvのタイムアウト設定時はそれによる。シグナル来ても切れるけど。
fd_other = accept(fd_accept, (struct sockaddr *)&sun_client, &socklen);
if (fd_other == -1) {
printf("failed to accept(errno:%d, error_str:%s)\n", errno, strerror(errno));
close(fd_accept);
return -1;
}
}
// データ本体の受信用にバッファーを確保
buf = malloc(data_size_g);
if (buf == NULL) {
printf("failed to malloc");
close(fd_other);
close(fd_accept);
return -1;
}
int i; // for文の中で書けないのはメンドイ...
// テスト用のループ。前回と異なりひたすら受信
for (i = 0; i < send_num_g; i++) {
// データ本体の受信
int recv_size = 0;
int remain = data_size_g;
while (1) {
size = recv(fd_other, (char *)buf + recv_size, remain, flags);
if (size == (data_size_g - recv_size)) {
break;
} else if (size == -1) {
printf("failed to recv data(errno:%d, error_str:%s)\n", errno, strerror(errno));
free(buf);
close(fd_other);
close(fd_accept);
return -1;
} else if (size < (data_size_g - recv_size)) {
// 全部recvできるまで再送を続ける
recv_size += size;
remain -= recv_size;
}
}
}
free(buf);
close(fd_other);
close(fd_accept);
return ret_code;
}

クライアント側ソース

#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/un.h>
#define SERVER_ADDR "127.0.0.1"
#define SERVER_PORT 55555
#define UNIX_SOCKET_PATH "/tmp/test.unixsocket"
static int data_size_g; // データサイズ
static int send_num_g; //送信数
int client(bool inet);
// 引数はデータサイズと送信数
int main(int argc, char *argv[]) {
int ret_code = 0;
int i = 0;
char *endptr;
int test_num;
// コマンドライン引数の取得
if (argc == 4) {
data_size_g = strtol(argv[1], &endptr, 10);
printf("data_size_g=%d\n", data_size_g);
if (data_size_g == 0 || argv[1] == '\0' || *endptr != '\0') {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size send_num test_num\n");
return EXIT_FAILURE;
}
send_num_g = strtol(argv[2], &endptr, 10);
printf("send_num_g=%d\n", send_num_g);
if (send_num_g == 0 || argv[2] == '\0' || *endptr != '\0') {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size send_num test_num\n");
return EXIT_FAILURE;
}
test_num = strtol(argv[3], &endptr, 10);
printf("test_num=%d\n", test_num);
if (test_num == 0 || argv[3] == '\0' || *endptr != '\0') {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size send_num test_num\n");
return EXIT_FAILURE;
}
} else {
printf("invalid argument\nusage:\"PROGRAM NAME\" data_size send_num test_num\n");
return EXIT_FAILURE;
}
printf("start test client\n");
printf("tcp \tunix \n");
for (i = 0; i < test_num; i++) {
// tcpソケットのクライアント処理
ret_code = client(true);
if (ret_code != 0) {
printf("error\n");
return EXIT_FAILURE;
}
// クライアント側が先に処理始まるとconnectでエラるので。
sleep(1);
printf("\t");
// UNIXドメインソケットのクライアント処理
ret_code = client(false);
if (ret_code != 0) {
printf("error\n");
return EXIT_FAILURE;
}
// クライアント側が先に処理始まるとconnectでエラるので。
sleep(1);
printf("\n");
}
return EXIT_SUCCESS;
}
int client(bool inet) {
clock_t start, end;
int ret_code = 0;
char *buf;
int fd = -1;
ssize_t size = 0;
int flags = 0; // MSG_WAITALLとかMSG_NOSIGNALをよく使うけど今回はサンプルなのでフラグは無し
// inetフラグがtrueの場合はINETドメインのソケットを作る
if (inet == true) {
// ソケットアドレス構造体
struct sockaddr_in sin, sin_client;
memset(&sin, 0, sizeof(sin));
memset(&sin_client, 0, sizeof(sin_client));
socklen_t socklen = sizeof(sin_client);
// インターネットドメインのTCPソケットを作成
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd == -1) {
printf("failed to socket(errno=%d, %s)\n", errno, strerror(errno));
return -1;
}
// ソケットアドレス構造体に接続先(サーバー)を設定
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(SERVER_ADDR);
sin.sin_port = htons(SERVER_PORT);
// 上記設定を用いてサーバーに接続
ret_code = connect(fd, (const struct sockaddr *)&sin, sizeof(sin));
if (ret_code == -1) {
printf("failed to connect(errno=%d, %s)\n", errno, strerror(errno));
close(fd);
return -1;
}
} else {
// ソケットアドレス構造体←今回はここがUNIXドメイン用のやつ
struct sockaddr_un sun, sun_client;
memset(&sun, 0, sizeof(sun));
memset(&sun_client, 0, sizeof(sun_client));
socklen_t socklen = sizeof(sun_client);
// UNIXドメインのソケットを作成
fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (fd == -1) {
printf("failed to socket(errno:%d, error_str:%s)\n", errno, strerror(errno));
return -1;
}
// ソケットアドレス構造体を設定
sun.sun_family = AF_LOCAL; // UNIXドメイン
strcpy(sun.sun_path, UNIX_SOCKET_PATH); // UNIXドメインソケットのパスを指定
// 上記設定を用いてサーバーに接続
ret_code = connect(fd, (const struct sockaddr *)&sun, sizeof(sun));
if (ret_code == -1) {
printf("failed to connect(errno:%d, error_str:%s)\n", errno, strerror(errno));
close(fd);
return -1;
}
}
// データ本体の送信用にバッファーを確保
buf = malloc(data_size_g);
if (buf == NULL) {
printf("failed to malloc");
close(fd);
return -1;
}
start = clock();
int i; // for文の中で書けないのはメンドイ...
// テスト用のループ。前回と異なりひたすら送信
for (i = 0; i < send_num_g; i++) {
// データ本体の送信
int send_size = 0;
int remain = data_size_g;
while (1) {
size = send(fd, (char *)buf + send_size, remain, flags);
if (size == (data_size_g - send_size)) {
break;
} else if (size == -1) {
printf("failed to send data(errno:%d, error_str:%s)\n", errno, strerror(errno));
free(buf);
close(fd);
return -1;
} else if (size < (data_size_g - send_size)) {
// 全部sendできるまで再送を続ける
send_size += size;
remain -= send_size;
}
}
}
end = clock();
free(buf);
close(fd);
printf("%.3f", (double)(end - start)/CLOCKS_PER_SEC);
return 0;
}

本当にTCPソケットの方が速いのか、docker上だからなのか、テストコードがおかしいのか…等を含め追加で調査したいと思います。
なにかご存知の方がいらしたら教えてください。。。

午前二時のテンションで検証なんてするもんじゃないな…そうだ踏切でも行ってこよう(錯乱)

コメント

  1. すみません、2018年の記事に 2024年の世界からアクセスします。

    プロセスって、起動から一定時間が経つとOSから「こいつ長時間かかるし後回し」ってされる可能性がありませんか。

    クライアント側ソース:
    プロセス起動 → TCP の試験 → UnixSock の試験

    だと、起動からの経過時間によってOSのプロセス スケジューリングアルゴリズムの影響を受けることはないでしょうか。

    プロセス起動 → TCP の試験
    プロセス起動 → UnixSock の試験

    という実行方法だと、結果が変わるかもしれませんね。

タイトルとURLをコピーしました