与 cstdio 和 std::fstream 相比,为什么 SDL_RWops 在写入文件时性能如此差?

Why SDL_RWops performs so poorly when writing to file compared to cstdio and std::fstream?

我目前正在将我的爱好项目从 std::fstream 迁移到 SDL_RWops(因为 SDL_RWops 是我在 Android 上加载资产的唯一简单选择) .

从文件读取完美,但写入文件却非常慢。

考虑以下测试用例:

C 标准 IO - 0.217193 秒

std::FILE *io = std::fopen("o.txt", "w");
for (int i = 0; i < 1024*1024*4; i++)
    std::putc('0', io);
std::fclose(io);

C++ 流 - 0.278278 秒

std::ofstream io("o.txt");
for (int i = 0; i < 1024*1024*4; i++)
    io << '0';
io.close();

SDL_RWops:- 17.9893 秒

SDL_RWops *io = SDL_RWFromFile("o.txt", "w");
for (int i = 0; i < 1024*1024*4; i++)
    io->write(io, "0", 1, 1);
io->close(io);

所有测试用例均使用带有 -O3 的 g++ 5.3.0 (mingw-w64) x86 编译。我用过 SDL 2.0.4.

我也试过 -O0 得到类似的结果(慢 0.02 到 0.25 秒)。

看完这些结果后,我有一个明显的问题:
为什么SDL_RWops写入性能这么差?
我该怎么做才能让它表现更好?


编辑: 这里是windows_file_write()的代码(来自SDL),io->write应该指向的地方。它应该 进行缓冲输出,但我不确定它是如何工作的。

static size_t SDLCALL
windows_file_write(SDL_RWops * context, const void *ptr, size_t size, size_t num)
{
    size_t total_bytes;
    DWORD byte_written;
    size_t nwritten;

    total_bytes = size * num;

    if (!context || context->hidden.windowsio.h == INVALID_HANDLE_VALUE || total_bytes <= 0 || !size)
        return 0;

    if (context->hidden.windowsio.buffer.left) {
        SetFilePointer(context->hidden.windowsio.h,
                       -(LONG)context->hidden.windowsio.buffer.left, NULL,
                       FILE_CURRENT);
        context->hidden.windowsio.buffer.left = 0;
    }

    /* if in append mode, we must go to the EOF before write */
    if (context->hidden.windowsio.append) {
        if (SetFilePointer(context->hidden.windowsio.h, 0L, NULL, FILE_END) ==
            INVALID_SET_FILE_POINTER) {
            SDL_Error(SDL_EFWRITE);
            return 0;
        }
    }

    if (!WriteFile
        (context->hidden.windowsio.h, ptr, (DWORD)total_bytes, &byte_written, NULL)) {
        SDL_Error(SDL_EFWRITE);
        return 0;
    }

    nwritten = byte_written / size;
    return nwritten;
}

简而言之:我设法改进了它。现在我得到 0.316382 秒,只比其他解决方案慢一点。
但这是我一生中做过的最肮脏的黑客攻击之一。如果有更好的解决方案,我将不胜感激。

它是如何完成的:我已经为 SDL_RWFromFile() 滚动了自定义替换:我已经 copy-pasted 从 SDL_rwops.c 实现并删除了所有预处理器分支,就好像只有 [=13] =] 被定义。该函数包含对 SDL_RWFromFP() 的调用,因此我也有 copy-pasted SDL_RWFromFP() 并对其应用了相同的修改。反过来,SDL_RWFromFP() 依赖于 stdio_size()stdio_read()stdio_write()stdio_seek()stdio_close()(这些是 SDL_rwops.c 的一部分也是),因此我也 copy-pasted 他们。反过来,这些(再次!)依赖于 struct SDL_RWops 内部 "hidden" 联合的某些字段,这些字段在 windows 上使用预处理器禁用。我没有更改 header,而是更改了 copy-pasted 代码以使用 "hidden" 联合的 不同 成员, 存在于 windows。 (这是安全的,因为除了我自己的代码和 copy-pasted 代码之外,没有任何其他内容触及该结构。)进行了一些其他调整以使代码作为 C++ 而不是 C.

这是我得到的:

#if OnWindows

#define hidden_stdio_fp        ((FILE * &)context->hidden.windowsio.h)
#define hidden_stdio_autoclose ((SDL_bool &)context->hidden.windowsio.append)

// ** Begin copied code **

static auto stdio_size = [](SDL_RWops * context) -> int64_t
{
    int64_t pos, size;

    pos = SDL_RWseek(context, 0, RW_SEEK_CUR);
    if (pos < 0) {
        return -1;
    }
    size = SDL_RWseek(context, 0, RW_SEEK_END);

    SDL_RWseek(context, pos, RW_SEEK_SET);
    return size;
};

static auto stdio_seek = [](SDL_RWops * context, int64_t offset, int whence) -> int64_t
{
    #ifdef HAVE_FSEEKO64
    if (std::fseeko64(hidden_stdio_fp, (off64_t)offset, whence) == 0) {
        return std::ftello64(hidden_stdio_fp);
    }
    #elif defined(HAVE_FSEEKO)
    if (std::fseeko(hidden_stdio_fp, (off_t)offset, whence) == 0) {
        return std::ftello(hidden_stdio_fp);
    }
    #elif defined(HAVE__FSEEKI64)
    if (std::_fseeki64(hidden_stdio_fp, offset, whence) == 0) {
        return std::_ftelli64(hidden_stdio_fp);
    }
    #else
    if (std::fseek(hidden_stdio_fp, offset, whence) == 0) {
        return std::ftell(hidden_stdio_fp);
    }
    #endif
    return SDL_Error(SDL_EFSEEK);
};

static auto stdio_read = [](SDL_RWops * context, void *ptr, std::size_t size, std::size_t maxnum) -> std::size_t
{
    std::size_t nread;

    nread = std::fread(ptr, size, maxnum, hidden_stdio_fp);
    if (nread == 0 && std::ferror(hidden_stdio_fp)) {
        SDL_Error(SDL_EFREAD);
    }
    return nread;
};

static auto stdio_write = [](SDL_RWops * context, const void *ptr, std::size_t size, std::size_t num) -> std::size_t
{
    std::size_t nwrote;

    nwrote = std::fwrite(ptr, size, num, hidden_stdio_fp);
    if (nwrote == 0 && std::ferror(hidden_stdio_fp)) {
        SDL_Error(SDL_EFWRITE);
    }
    return nwrote;
};

static auto stdio_close = [](SDL_RWops * context) -> int
{
    int status = 0;
    if (context) {
        if (hidden_stdio_autoclose) {
            /* WARNING:  Check the return value here! */
            if (std::fclose(hidden_stdio_fp) != 0) {
                status = SDL_Error(SDL_EFWRITE);
            }
        }
        SDL_FreeRW(context);
    }
    return status;
};

static auto RWFromFP = [](FILE * fp, SDL_bool autoclose) -> SDL_RWops *
{
    SDL_RWops *context = 0;

    context = SDL_AllocRW();
    if (context != 0) {
        context->size = stdio_size;
        context->seek = stdio_seek;
        context->read = stdio_read;
        context->write = stdio_write;
        context->close = stdio_close;
        hidden_stdio_fp = fp;
        hidden_stdio_autoclose = autoclose;
        context->type = SDL_RWOPS_STDFILE;
    }
    return context;
};

static auto SDL_RWFromFile = [](const char *file, const char *mode) -> SDL_RWops *
{
    SDL_RWops *context = 0;
    if (!file || !*file || !mode || !*mode) {
        SDL_SetError("SDL_RWFromFile(): No file or no mode specified");
        return 0;
    }

    FILE *fp = std::fopen(file, mode);

    if (fp == 0) {
        SDL_SetError("Couldn't open %s", file);
    } else {
        context = RWFromFP(fp, (SDL_bool)1);
    }

    return context;
};

// ** End copied code **

#undef hidden_stdio_fp
#undef hidden_stdio_autoclose

#endif