13

次の3つの手法を使用してファイルの読み取りを比較します。

  1. C<stdio.h> FILE*
  2. Win32 CreateFile()/ReadFile()
  3. Win32メモリマッピング

#1は#2よりも速く、#3が最も速いことに気づきました。

たとえば、900MBのテストファイルを処理するために、最も速いものから最も遅いものへと並べ替えると、次の結果が得られました。

Win32メモリマッピング:821.308ミリ秒

Cファイル(FILE *):1779.83ミリ秒

Win32ファイル(CreateFile):3649.67ミリ秒

C<stdio.h>テクニックがWin32ReadFile()アクセスよりも速いのはなぜですか?生のWin32APIはCRTよりもオーバーヘッドが少ないと思います。ここで何が欠けていますか?

コンパイル可能なテストC++ソースコードは次のとおりです。


編集

パフォーマンス測定を歪める可能性のあるキャッシュ効果を回避するために、4KBの読み取りバッファーと3つの異なるファイル(同じコンテンツ)を使用してテストを繰り返しましたが、結果は期待どおりになりました。 たとえば、約400 MBのファイルの場合、結果は次のようになります。

  1. Win32メモリマッピング:305.908ミリ秒

  2. Win32ファイル(CreateFile):451.402ミリ秒

  3. Cファイル(FILE *):460.579ミリ秒


////////////////////////////////////////////////////////////////////////////////
// Test file reading using C FILE*, Win32 CreateFile and Win32 memory mapping.
////////////////////////////////////////////////////////////////////////////////


#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <exception>
#include <iostream>
#include <stdexcept>
#include <vector>
#include <Windows.h>


//------------------------------------------------------------------------
//                      Performance (speed) measurement
//------------------------------------------------------------------------

long long counter()
{
    LARGE_INTEGER li;
    QueryPerformanceCounter(&li);
    return li.QuadPart;
}

long long frequency()
{
    LARGE_INTEGER li;
    QueryPerformanceFrequency(&li);
    return li.QuadPart;
}

void print_time(const long long start, const long long finish,
    const char * const s)
{
    std::cout << s << ": " << (finish - start) * 1000.0 / frequency() << " ms\n";
}


//------------------------------------------------------------------------
//                      RAII handle wrappers
//------------------------------------------------------------------------

struct c_file_traits
{
    typedef FILE* type;

    static FILE* invalid_value()
    {
        return nullptr;
    }

    static void close(FILE* f)
    {
        fclose(f);
    }
};

struct win32_file_traits
{
    typedef HANDLE type;

    static HANDLE invalid_value()
    {
        return INVALID_HANDLE_VALUE;
    }

    static void close(HANDLE h)
    {
        CloseHandle(h);
    }
};

struct win32_handle_traits
{
    typedef HANDLE type;

    static HANDLE invalid_value()
    {
        return nullptr;
    }

    static void close(HANDLE h)
    {
        CloseHandle(h);
    }
};

template <typename Traits>
class handle
{
public:
    typedef typename Traits::type type;

    handle()
        : _h(Traits::invalid_value())
    {
    }

    explicit handle(type h)
        : _h(h)
    {
    }

    ~handle()
    {
        close();
    }

    bool valid() const
    {
        return (_h != Traits::invalid_value());
    }

    type get() const
    {
        return _h;
    }

    void close()
    {
        if (valid())
            Traits::close(_h);

        _h = Traits::invalid_value();
    }

    void reset(type h)
    {
        if (h != _h)
        {
            close();
            _h = h;
        }
    }


private: // Ban copy
    handle(const handle&);
    handle& operator=(const handle&);

private:
    type _h;    // wrapped raw handle
};

typedef handle<c_file_traits> c_file_handle;
typedef handle<win32_file_traits> win32_file_handle;
typedef handle<win32_handle_traits> win32_handle;


//------------------------------------------------------------------------
//              File reading tests using various techniques
//------------------------------------------------------------------------

unsigned long long count_char_using_c_file(const std::string& filename, const char ch)
{
    unsigned long long char_count = 0;

#pragma warning(push)
#pragma warning(disable: 4996) // fopen use is OK
    c_file_handle file(fopen(filename.c_str(), "rb"));
#pragma warning(pop)

    if (!file.valid())
        throw std::runtime_error("Can't open file.");

    std::vector<char> read_buffer(4*1024); // 4 KB
    bool has_more_data = true;
    while (has_more_data)
    {
        size_t read_count = fread(read_buffer.data(), 1, read_buffer.size(), file.get());
        for (size_t i = 0; i < read_count; i++)
        {
            if (read_buffer[i] == ch)
                char_count++;
        }

        if (read_count < read_buffer.size())
            has_more_data = false;
    }

    return char_count;
}


unsigned long long count_char_using_win32_file(const std::string& filename, const char ch)
{
    unsigned long long char_count = 0;

    win32_file_handle file(::CreateFileA(
        filename.c_str(),
        GENERIC_READ,
        FILE_SHARE_READ,
        nullptr,
        OPEN_EXISTING,
        FILE_FLAG_SEQUENTIAL_SCAN,
        nullptr
        )
        );
    if (!file.valid())
        throw std::runtime_error("Can't open file.");

    std::vector<char> read_buffer(4*1024); // 4 KB
    bool has_more_data = true;
    while (has_more_data)
    {
        DWORD read_count = 0;
        if (!ReadFile(file.get(), read_buffer.data(), read_buffer.size(), &read_count, nullptr))
            throw std::runtime_error("File read error using ReadFile().");

        for (size_t i = 0; i < read_count; i++)
        {
            if (read_buffer[i] == ch)
                char_count++;
        }

        if (read_count < sizeof(read_buffer))
            has_more_data = false;
    }

    return char_count;
}


// Memory-map a file.
class file_map
{
public:
    explicit file_map(const std::string& filename)
        : _view(nullptr), _length(0)
    {
        _file.reset(::CreateFileA(
            filename.c_str(),
            GENERIC_READ,
            FILE_SHARE_READ,
            nullptr,
            OPEN_EXISTING,
            FILE_ATTRIBUTE_NORMAL,
            nullptr));
        if (!_file.valid())
            return;

        LARGE_INTEGER file_size;
        if (!GetFileSizeEx(_file.get(), &file_size))
            return;

        if (file_size.QuadPart == 0)
            return;

        _mapping.reset(::CreateFileMapping(
            _file.get(), nullptr,
            PAGE_READONLY,
            0,
            0,
            nullptr)
            );
        if (!_mapping.valid())
            return;

        _view = reinterpret_cast<char*>
            (::MapViewOfFile(_mapping.get(), FILE_MAP_READ, 0, 0, 0));
        if (!_view)
            return;

        _length = file_size.QuadPart;
    }

    ~file_map()
    {
        if (_view)
            UnmapViewOfFile(_view);
    }

    bool valid() const
    {
        return (_view != nullptr);
    }

    const char * begin() const
    {
        return _view;
    }

    const char * end() const
    {
        return begin() + length();
    }

    unsigned long long length() const
    {
        return _length;
    }

private:    // ban copy
    file_map(const file_map&);
    file_map& operator=(const file_map&);

private:
    win32_file_handle   _file;
    win32_handle        _mapping;
    char*               _view;
    unsigned long long  _length;    // in bytes
};


unsigned long long count_char_using_memory_mapping(const std::string& filename, const char ch)
{
    unsigned long long char_count = 0;

    file_map view(filename);
    if (!view.valid())
        throw std::runtime_error("Can't create memory-mapping of file.");

    for (auto it = view.begin(); it != view.end(); ++it)
    {
        if (*it == ch)
        {
            char_count++;
        }
    }

    return char_count;
}


template <typename TestFunc>
void run_test(const char * message, TestFunc test, const std::string& filename, const char ch)
{
    const long long start = counter();
    const unsigned long long char_count = test(filename, ch);
    const long long finish = counter();
    print_time(start, finish, message);
    std::cout << "Count of \'" << ch << "\' : " << char_count << "\n\n";
}


int main(int argc, char* argv[])
{
    static const int kExitOk = 0;
    static const int kExitError = 1;

    if (argc != 3)
    {
        std::cerr << argv[0] << " <char> <filename>.\n";
        std::cerr << "Counts occurrences of ASCII character <char>\n";
        std::cerr << "in the <filename> file.\n\n";
        return kExitError;
    }

    const char ch = *(argv[1]);
    const std::string filename = argv[2];

    try
    {
        // Execute tests on THREE different files with the same content,
        // to avoid caching effects.
        // (file names have incremental number suffix).
        run_test("C <stdio.h> file (FILE*)", count_char_using_c_file, filename + "1", ch);
        run_test("Win32 file (CreateFile)", count_char_using_win32_file, filename + "2", ch);
        run_test("Win32 memory mapping", count_char_using_memory_mapping, filename + "3", ch);

        return kExitOk;
    }
    catch (const std::exception& e)
    {
        std::cerr << "\n*** ERROR: " << e.what() << '\n';
        return kExitError;
    }
}

////////////////////////////////////////////////////////////////////////////////
4

4 に答える 4

11

私のマシンでいくつかのテストを実行したところ、バッファサイズを増やすと実際にパフォーマンスが向上することがわかりました。

C <stdio.h> file (FILE*): 1431.93 ms
Bufsize: 0
Count of 'x' : 3161882

Win32 file (CreateFile): 2289.45 ms
Bufsize: 1024
Count of 'x' : 3161882

Win32 file (CreateFile): 1714.5 ms
Bufsize: 2048
Count of 'x' : 3161882

Win32 file (CreateFile): 1479.16 ms
Bufsize: 4096
Count of 'x' : 3161882

Win32 file (CreateFile): 1328.25 ms
Bufsize: 8192
Count of 'x' : 3161882

Win32 file (CreateFile): 1256.1 ms
Bufsize: 16384
Count of 'x' : 3161882

Win32 file (CreateFile): 1223.54 ms
Bufsize: 32768
Count of 'x' : 3161882

Win32 file (CreateFile): 1224.84 ms
Bufsize: 65536
Count of 'x' : 3161882

Win32 file (CreateFile): 1212.4 ms
Bufsize: 131072
Count of 'x' : 3161882

Win32 file (CreateFile): 1238.09 ms
Bufsize: 262144
Count of 'x' : 3161882

Win32 file (CreateFile): 1209.2 ms
Bufsize: 524288
Count of 'x' : 3161882

Win32 file (CreateFile): 1223.67 ms
Bufsize: 1048576
Count of 'x' : 3161882

Win32 file (CreateFile): 1349.98 ms
Bufsize: 2097152
Count of 'x' : 3161882

Win32 memory mapping: 796.281 ms
Bufsize: 0
Count of 'x' : 3161882

Visual Studio 2012デバッガーのいくつかの手順で、少なくとも私のマシンでは、FILE*メソッドのバッファーサイズが4096バイトであることがわかります。(そして、他の人がすでに言っているようにReadFile、コンソールから読んでいない限り、それも呼び出します。)

大きなバッファがパフォーマンスをわずかに遅くすることも興味深いです。オペレーターをテストの外に移動してnewも、問題は解決しません。

最初に、メモリマップドテストはデバッグモードで実行したため、かなり遅くなりました。リリースモードのコンパイルですべての結果を更新しました。メモリマッピングが最初になりました。

于 2013-01-23T21:32:20.280 に答える
9

私がこれまでに達成した最速のディスクアクセスは、を使用することでしたReadFile。ただし、ディスクアクセスとキャッシュの要件を満たすために、フラグを使用してファイルを具体的に開きました。そのまま使用する場合、比較は少し不十分です。

関数だけでなく、についても読む必要がありますCreateFile。セクターサイズのブロック(の倍数)のデータをセクター整列メモリに読み取ることができます。次に、パフォーマンスを上回りますfread

他の人が言っているようにfread、独自のバッファリングを行っています。ReadFileまだ作業が必要なバッファリングの実装。

MSDNをチェックしてください。すべての情報はそこにあります。具体的には、ここで:

于 2013-01-23T20:29:48.323 に答える
4

適切にテストしていますか?
ディスクの位置、シーク時間、ファイルのキャッシュなどをどのように考慮していますか?

stdioとwin32は、最終的にWindowsカーネルに対して同じ呼び出しを行ってファイルを開きます。

mmapは、使用されるまで実際にデータを読み取ることを予約できるため、動作が少し異なります。ファイルサイズが固定されていてパフォーマンスが重要な場合は、mmapが適しています。

于 2013-01-23T20:28:08.120 に答える
3

メモリマップトファイルを使用する場合、ファイルのコンテンツをアプリケーションにコピーする必要はありません。OSから直接仮想メモリの一部としてマップされるため、ファイルコンテンツにアクセスする場合は、次のことを行う必要があります。マップされたメモリに入るページに直接読み込まれます。

Win32 APIを使用するときに正しく作業を行うと、呼び出しのオーバーヘッドが少なくなるため、Cstdioの方が高速になります。ただし、システムコールのオーバーヘッドと「バッファが大きすぎるため、読み取りに必要以上に時間がかかる」という理想的なバランスが取れていない可能性があります。Win32 API機能のバッファとして4Kまたは8K(場合によっては32K)を試してみることをお勧めします。メモリページは(通常)4KBであるため、4Kの倍数のバッファサイズが理想的です。APIの呼び出しが少ないほどオーバーヘッドは少なくなりますが、行き過ぎたくはありません。

[先日、Linuxでこのようなテストを行ったところ、同様の結果が見つかりました。そこでの経験から、テストごとに異なるファイルを使用してください。そうしないと、ファイルシステムのキャッシュが後で実行されるテストに役立ちます!]。

于 2013-01-23T20:34:37.237 に答える