Zig言語でLinuxのパイプの読み書きの効率を上げる実装例(その2)
さきほどの記事の続きです。
パイプへの書き込みにwrtite(2)でなくvmsplice(2)を使う
この記事によって私はvmsplice(2)
の存在をしりました。
パイプに対してwriteするときにはカーネルはカーネル内部にメモリを確保して、そこにコピーを行います。そのため、write(2)
はそのメモリコピーが終わった段階でリターンすることができます。この記事によると、カーネル内部にメモリを確保するために空きを探すことが性能のボトルネックになっているということでした。
vmsplice(2)
を使うと、ユーザーのバッファをそのままカーネルに渡すことになるので、ユーザー空間からカーネル空間のメモリコピーを省略することができるそうです。
なお、読み込みのときにはカーネル空間からユーザー空間へのメモリコピーは避けることができないため、read(2)
を大きく上回る代替手段はないそうです。
Zig言語での実装
vmsplice(2)
は今はまだZigの標準ライブラリには入っていませんでした。なので標準Cライブラリのものを呼び出します。
const std = @import("std");
const fs = std.fs;
const os = std.os;
const c = @cImport({
@cDefine("_GNU_SOURCE", "");
@cInclude("fcntl.h");
@cInclude("sys/uio.h");
@cInclude("errno.h");
});
fn getErrno() c_int {
return c.__errno_location().*;
}
pub fn vmspliceSingleBuffer(buf: []const u8, fd: os.fd_t) !void {
var iov: c.struct_iovec = .{
.iov_base = @ptrCast(?*anyopaque, @constCast(buf.ptr)),
.iov_len = buf.len,
};
while (true) {
const n = c.vmsplice(fd, &iov, 1, @bitCast(c_uint, c.SPLICE_F_GIFT));
if (n < 0) {
const errno = getErrno();
switch (errno) {
c.EINTR => continue,
c.EAGAIN => unreachable,
c.EPIPE => return error.BrokenPipe,
c.EBADF => return error.InvalidFileDescriptor,
c.EINVAL => return error.InvalidArgument,
c.ENOMEM => return error.SystemResources,
else => std.log.err("vmsplice: errno={d}", .{errno}),
}
} else if (@bitCast(usize, n) == iov.iov_len) {
return;
} else if (n != 0) {
//std.log.info("vmsplice: return value mismatch: n={d}, iov_len={d}", .{ n, iov.iov_len });
const un = @bitCast(usize, n);
iov.iov_len -= un;
iov.iov_base = @intToPtr(?*anyopaque, @ptrToInt(iov.iov_base) + un);
continue;
}
return error.Vmsplice;
}
unreachable;
}
書き込みのところ。
最初に一度だけ、fdがパイプであるかどうかを調べて、isPipe
というブーリアンの変数に入れておきます。
それがtrue
の場合にはwriter.writeAll
の代わりに上で定義したvmspliceSingleBuffer
を使います。
書き込みのたびに毎回このような判断をするのでなく、書き込みのハンドラ関数そのものを関数ポインタにして差し替えてしまうという最適化ができそうですが、とりあえず今はこれで。
const vms = @import("vmsplice.zig");
...
if (isPipe) {
vms.vmspliceSingleBuffer(buf, outFile.?.handle) catch |err| {
switch (err) {
error.BrokenPipe => {},
else => {
log.err("frameHandle: {s}", .{@errorName(err)});
},
}
running = false;
};
} else if (outFile) |f| {
f.writeAll(buf) catch |err| {
log.err("frameHandle: {s}", .{@errorName(err)});
running = false;
};
}
...
vmsplice(2)
に渡すバッファはmmap(2)
で得たページサイズにアラインされているメモリである必要がありますが、これはZigの場合では自分で``mmap(2)を使わなくてもアロケータに
std.heap.page_allocatorを選んで
alloc`すればその条件をクリアできます。
以下の2つのリポジトリにこの変更はマージしています。
効果の確認
とりあえず、この変更で外から見える動作としては等価なものになりました。
性能測定によって効果を確認するべきですが、とりあえず今はvmsplice(2)
のシステムコールが使われていることをstraceのログで確認しました。
$ grep vmsplice vmsplicelog/st_write.log
3256552 vmsplice(3, [{iov_base="\345\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177"..., iov_len=460800}], 1, SPLICE_F_GIFT) = 460800
3256552 vmsplice(3, [{iov_base="\344\200\346\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177"..., iov_len=460800}], 1, SPLICE_F_GIFT) = 460800
3256552 vmsplice(3, [{iov_base="\343\200\346\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177"..., iov_len=460800}], 1, SPLICE_F_GIFT) = 460800
3256552 vmsplice(3, [{iov_base="\343\200\345\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177"..., iov_len=460800}], 1, SPLICE_F_GIFT) = 460800
3256552 vmsplice(3, [{iov_base="\343\200\346\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177"..., iov_len=460800}], 1, SPLICE_F_GIFT) = 460800
3256552 vmsplice(3, [{iov_base="\342\200\345\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177\347\200\347\177"..., iov_len=460800}], 1, SPLICE_F_GIFT) = 460800
...
ToDo
Linuxカーネルのソースコードを読んで、vmsplice(2)
のしくみを理解する。
性能評価を行なって有意な差になるかどうかを確認する。
関連
Discussion