C言語: UNIX最速ファイルコピー

Created: Kazuki Ohta, 2006/06/14
Last Update: Kazuki Ohta, 2006/06/14



「write(2)の正しい使い方」と同じく、OS演習でやった事の延長線の記事を書いてみる。お題は「UNIX上で大規模ファイルを最速でコピーする方法」だ。一般的に、UNIXでファイルをcopyする際には以下のような方法が有る。 read, write, mmap辺りは良いとして、posix_fadviseというシステムコールが有るのは初めて知った。Linux 2.5.60から導入された(実質的には2.6)システムコールで、ファイルデータのアクセスパターンを予めOSに教えておいて効率的なI/Oを実現するためのシステムコールらしい。上に列挙した方法を用いて様々なサイズのファイルをコピーしてみて、「最速コピー」の為の方法を探ってみる事にする。

実装(0)

以下に挙げる方法のソースコードを全て固めたものを置いておく。

実装(1): read -> write based

-DUSE_FADVISEフラグでposix_fadviseを使用するか否かを決める。
#define BUF_SIZE 8192

int main(int argc, char **argv)
{
    char buf[BUF_SIZE];
    struct stat st_buf;

    if (argc < 3) {
        fprintf(stderr, "usage: %s src out\n", argv[0]);
        exit(EXIT_SUCCESS);
    }

    char *src  = argv[1];
    char *dest = argv[2];
    assert(strcmp(src, dest) != 0);

    int srcfd = open(src, O_RDONLY, 0644);
    assert(srcfd >= 0);

#if USE_FADVISE
    posix_fadvise(srcfd, 0, 0, POSIX_FADV_SEQUENTIAL);
    posix_fadvise(srcfd, 0, 0, POSIX_FADV_NOREUSE);
#endif

    /* get permission */
    assert(fstat(srcfd, &st_buf) >= 0);

    int destfd = open(dest, O_WRONLY | O_CREAT, st_buf.st_mode);
    assert(destfd >= 0);

    int n = 0;
    while ((n = read(srcfd, buf, sizeof(buf))) > 0) {
        char *p = &buf[0];
        const char * const endp = buf + n;
        while (p < endp) {
            int num_bytes = write(destfd, p, endp - p);
            p += num_bytes;
        }
    }
    assert(n == 0);

    fsync(destfd);

    close(destfd);
    close(srcfd);

    exit(EXIT_SUCCESS);
}

実装(2): mmap -> mmap based

-DUSE_MADVISEでmadviseを使うかどうかを決める。
int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s from_file to_file", argv[0]);
        exit(0);
    }

    /* from */
    int from = open(argv[1], O_RDONLY, 0644);
    assert(from >= 0);

    struct stat st_buf;
    assert(fstat(from, &st_buf) >= 0);
    size_t size = st_buf.st_size;

    void *from_mmap = mmap(NULL, size, PROT_READ, MAP_PRIVATE, from, 0);
    assert(from_mmap >= 0);

#if USE_MADVISE
    assert(madvise(from_mmap, size, MADV_SEQUENTIAL) >= 0);
#endif

    /* to */
    int to = open(argv[2], O_CREAT|O_RDWR, st_buf.st_mode);
    assert(to >= 0);

    int i = 0;
    assert(lseek(to, size - sizeof(int), 0L) >= 0);
    assert(write(to, (&i), sizeof(int)) == sizeof(int));

    void *to_mmap = mmap(NULL, size, PROT_WRITE, MAP_SHARED, to, 0);
    assert(to_mmap >= 0);

#if USE_MADVISE
    assert(madvise(to_mmap, size, MADV_SEQUENTIAL) >= 0);
#endif

    /* copy */
    memcpy(to_mmap, from_mmap, size);

    assert(msync(to_mmap, size, MS_SYNC) >= 0);
    assert(ftruncate(to, size) >= 0);

    return 1;
}

実装(3): mmap -> write based

-DUSE_MADVISEでmadviseを使うかどうかを決める。
int main(int argc, char *argv[])
{
    if (argc != 3) {
        fprintf(stderr, "usage: %s from_file to_file", argv[0]);
        exit(0);
    }

    /* from */
    int from = open(argv[1], O_RDONLY, 0644);
    assert(from >= 0);

    struct stat st_buf;
    assert(fstat(from, &st_buf) >= 0);
    size_t size = st_buf.st_size;

    void *from_mmap = mmap(NULL, size, PROT_READ, MAP_PRIVATE, from, 0);
    assert(from_mmap >= 0);

#if USE_MADVISE
    assert(madvise(from_mmap, size, MADV_SEQUENTIAL) >= 0);
#endif

    /* to */
    int to = open(argv[2], O_CREAT | O_WRONLY, 0666);
    assert(to >= 0);

    /* copy */
    char *p = from_mmap;
    const char * const endp = from_mmap + size;
    while (p < endp) {
        int num_bytes = write(to, p, endp - p);
        p += num_bytes;
    }
    assert(p == endp);

    fsync(to);

    close(to);
    close(from);

    return 1;
}

検証

以上のソースを用いて様々なファイルをコピーし、時間を計測してみた。各方式につき3回コピーを行い、一番早いタイムを計測した。ただし、環境によって変わってくるので、検証対象環境について一度プログラムを動かしてみるのが望ましい。

環境(1)

rw rw_fadv mm_sync mm_sync_madv mw mw_madv
1MB 0.065 0.060 0.030 0.068 0.052 0.032
10MB 0.654 0.669 0.641 0.638 0.601 0.658
100MB 6.397 6.738 6.813 6.860 6.733 6.541
500MB 1:11.72 1:06.76 1:32.30 1:35.57 55.944 1:00.63
1GB 2:17.60 2:09.61 3:30.32 3:47.09 2:47.38 2:27.34


[ return ]