在 MEX 中超快地将二进制文件写入磁盘

Write binary file to disk super fast in MEX

我需要尽快将大量数据写入磁盘。在 MATLAB 中,我可以使用 fwrite:

function writeBinaryFileMatlab(data)
    fid = fopen('file_matlab.bin', 'w');
    fwrite(fid, data, class(data));
    fclose(fid);
end

现在我必须做同样的事情,但是来自 MATLAB 调用的 MEX 文件。因此,我设置了一个可以使用 fstreamfopen 写入文件的 MEX 函数(受 this SO post 结果的启发)。然而,这比从 MATLAB 调用 fwrite 慢得多,如下所示。为什么会这样,如何提高 MEX 函数的写入速度。

#include "mex.h"
#include <iostream>
#include <stdio.h>
#include <fstream>

using namespace std;

void writeBinFile(int16_t *data, size_t size)
{
    FILE *fID;
    fID = fopen("file_fopen.bin", "wb");
    fwrite(data, sizeof(int16_t), size, fID);
    fclose(fID);
}

void writeBinFileFast(int16_t *data, size_t size)
{
    ofstream file("file_ostream.bin", std::ios::out | std::ios::binary);
    file.write((char *)&data[0], size * sizeof(int16_t));
    file.close();
}

void mexFunction(int nlhs, mxArray *plhs[],
                 int nrhs, const mxArray *prhs[])
{
    const mxArray *mxPtr = prhs[0];
    size_t nelems = mxGetNumberOfElements(mxPtr);
    int16_t *ptr = (int16_t *)mxGetData(mxPtr);
#ifdef USE_OFSTREAM
    writeBinFileFast(ptr, nelems);
#else
    writeBinFile(ptr, nelems);
#endif
}

然后我使用以下脚本检查性能:

mex -R2018a -Iinclude CXXFLAGS="$CXXFLAGS -O3" -DUSE_OFSTREAM main.cpp -output writefast_ofstream
mex -R2018a -Iinclude CXXFLAGS="$CXXFLAGS -O3" main.cpp -output writefast_fwrite

for k = 1:10
    sizeBytes = 2^k * 1024 * 1024;
    fprintf('Generating data of size %i MB\n', sizeBytes / 2^20)
    M = sizeBytes / 2; % 2 bytes for an int16
    sizeMB(k) = sizeBytes / 2^20;
    data = int16(rand(M, 1) * 100);

    fprintf('TESTING: write matlab\n')
    t_matlab(k) = timeit(@() writeBinaryFileMatlab(data));

    fprintf('TESTING: write ofstream\n')
    t_ofstream(k) = timeit(@() writefast_ofstream(data), 0);

    fprintf('TESTING: write fwrite\n')
    t_fwrite(k) = timeit(@() writefast_fwrite(data), 0);
end

% and plot result
figure(14); clf;
plot((sizeMB), t_matlab)
hold on
plot((sizeMB), t_ofstream)
plot((sizeMB), t_fwrite)
legend('Matlab', 'ofstream', 'fwrite')
xticks(sizeMB)

这给了我下面的情节。为什么从 MATLAB 调用 fwrite 比从 MEX 调用快得多?我怎样才能在我的 MEX 函数中达到相同的速度?

我正在使用 Windows 10. 配备 Core i7、SSD 的笔记本电脑。


更新

我尝试了评论中的各种建议,但仍然没有达到 MATLAB 的 fwrite 性能。在此处查看带有源代码的回购协议:https://github.com/rick3rt/saveBinaryDataMex

这是 MSVC 2017 的结果,结合了 rahnema1 的建议:

更新 2

哇,我终于得到了比 MATLAB 更快的东西! 答案成功了:) 这里结合了所有建议方法的数字(完整的 src 可以在 Github 上找到)。

[不幸的是,这只是部分答案。]

这是一个 Windows 问题。我尝试在 macOS 上重现您的结果,并发现了一个不同的、有趣的行为。我修改了您的代码以区分 C fwrite 和 C++ std::fwrite,并添加了使用较低级别 Posix write.

编写的代码

这是 C++ 代码:

#include "mex.h"
#include <stdio.h>
#include <cstdio>
#include <fcntl.h>
#include <unistd.h>

void writeBinFile_c(int16_t *data, std::size_t size)
{
    ::FILE *fID = ::fopen("file_c.bin", "wb");
    ::fwrite(data, sizeof(int16_t), size, fID);
    ::fclose(fID);
}

void writeBinFile_std(int16_t *data, std::size_t size)
{
    std::FILE *fID = std::fopen("file_std.bin", "wb");
    std::fwrite(data, sizeof(int16_t), size, fID);
    std::fclose(fID);
}

void writeBinFile_unix(int16_t *data, std::size_t size)
{
    int fID = open("file_unix.bin", O_CREAT|O_WRONLY|O_TRUNC);
    ::write(fID, data, sizeof(int16_t) * size);
    ::close(fID);
}

void mexFunction(int nlhs, mxArray *plhs[],
                 int nrhs, const mxArray *prhs[])
{
    const mxArray *mxPtr = prhs[0];
    std::size_t nelems = mxGetNumberOfElements(mxPtr);
    int16_t *ptr = (int16_t *)mxGetData(mxPtr);
    double mode = -1;
    if (nrhs > 1) {
      mode = mxGetScalar(prhs[1]);
    }
    if (mode == 0) {
       writeBinFile_c(ptr, nelems);
    } else if (mode == 1) {
       writeBinFile_std(ptr, nelems);
    } else if (mode == 2) {
       writeBinFile_unix(ptr, nelems);
    } else {
       mexErrMsgTxt("Wrong mode!");
    }
}

这是 MATLAB 代码:

mex -R2018a -Iinclude CXXFLAGS="$CXXFLAGS -O3" writefast.cpp

N = 10;
sizeMB = zeros(1,N);
t_matlab = zeros(1,N);
t_fwrite_c = zeros(1,N);
t_fwrite_std = zeros(1,N);
t_unix = zeros(1,N);
for k = 1:10
    sizeBytes = 2^k * 1024 * 1024;
    fprintf('Generating data of size %i MB\n', sizeBytes / 2^20)
    M = sizeBytes / 2; % 2 bytes for an int16
    sizeMB(k) = sizeBytes / 2^20;
    data = int16(rand(M, 1) * 100);

    fprintf('TESTING: matlab\n')
    t_matlab(k) = timeit(@() writeBinaryFileMatlab(data));

    fprintf('TESTING: ::fwrite\n')
    t_fwrite_c(k) = timeit(@() writefast(data, 0), 0);

    fprintf('TESTING: std::fwrite\n')
    t_fwrite_std(k) = timeit(@() writefast(data, 1), 0);

    fprintf('TESTING: Unix write\n')
    t_unix(k) = timeit(@() writefast(data, 1), 0);
end

% and plot result
figure
plot((sizeMB), t_matlab)
hold on
plot((sizeMB), t_fwrite_c)
plot((sizeMB), t_fwrite_std)
plot((sizeMB), t_unix)
legend('Matlab', 'C std lib', 'C++ Std lib', 'Unix')
xticks(sizeMB)
set(gca,'xscale','log','yscale','log')

function writeBinaryFileMatlab(data)
    fid = fopen('file_matlab.bin', 'w');
    fwrite(fid, data, class(data));
    fclose(fid);
end

这些是两个 运行 的输出:

请注意时序如何在高达 64 MB 的情况下保持一致,然后出现分歧。在 128 MB 及以上时,时间足以让 timeit 到 运行 工具在内部循环中仅一次,因此您会看到单个 运行 秒的中值时间,而不是平均在多个 运行s 上,就像在 64 MB 及以下时一样。因此对于 128 MB 及以上,我们看到时间在两个不同时间之间翻转,这可能是缓存的影响。但是在不同的 运行 中,是不同的方法变慢或变快,所以我很清楚它们都是一样的。

因此,在 macOS 上,MATLAB 的 fwrite 和 C 库 fwrite 之间没有区别。你看到的一定是Windows问题。

而且我很确定这与缓存有关,因为:

  • This post on Undocumented MATLAB 讨论了 fwrite 的性能,以及默认情况下,MATLAB 如何在每次调用 fwrite 后刷新缓存。这在这里无关紧要,因为只有一次调用 fwrite。但是 post 表示 MATLAB 函数处理缓存的方式与 C 库的不同。

  • The C library fwrite 工作 就好像 它为每个要写入的字节调用 fputc 一样。它可能实际上并没有这样做,但这可能表明 Windows 上出了什么问题。请注意,在 Windows 上,对于 MSVC 和 MinGW 编译器,您使用相同的 C 库 msvcrt。一定是问题出在了,MATLAB 一定没有用它来写入文件。

如某些 posts 中所示,非常大的缓冲区往往会降低性能。所以缓冲区是一部分一部分写入文件的。对我来说 8 MiB 给出了最好的性能。

void writeBinFilePartByPart(int16_t *int_data, size_t size)
{        
  size_t part = 8 * 1024 * 1024;

  size = size * sizeof(int16_t);
  
  char *data = reinterpret_cast<char *> (int_data);

  HANDLE file = CreateFileA (
    "windows_test.bin", 
    GENERIC_WRITE, 
    0, 
    NULL,
    CREATE_ALWAYS, 
    FILE_FLAG_SEQUENTIAL_SCAN, 
    NULL);
  
  // Expand file size
  SetFilePointer (file, size, NULL, FILE_BEGIN);
  SetEndOfFile (file);
  SetFilePointer (file, 0, NULL, FILE_BEGIN);

  DWORD written;
  if (size < part)
    {
      WriteFile (file, data, size, &written, NULL);  
      CloseHandle (file);
      return;
    }

  size_t rem = size % part;
  for (size_t i = 0; i < size-rem; i += part)
    {
      WriteFile (file, data+i, part, &written, NULL);
    }

  if (rem)
    WriteFile (file, data+size-rem, rem, &written, NULL);
  
  CloseHandle (file);
}

将输出与@Cris Luengo 提到的 C++ Std lib 方法进行比较: