2013-01-23 8 views
12

Porównując czytanie zbiorów za pomocą trzech następujących technik:Dlaczego C <stdio.h> FILE * fread() jest szybsze niż Win32 ReadFile()?

  1. C <stdio.h>FILE*
  2. Win32 CreateFile()/ReadFile()
  3. mapowania pamięci Win32

I zauważyć, że # 1 jest większa niż 2, i # 3 jest najszybszy.

np. Posortowane od najszybszych do najwolniejszych, do przetwarzania plik testowy 900MB, mam te wyniki:

mapowania pamięci Win32: 821.308 ms

C plików (FILE *): 1779.83 ms

Win32 file (CreateFile): 3649.67 ms

Dlaczego technika C <stdio.h> szybciej niż Win32 ReadFile() dostępu? Spodziewam się, że surowe interfejsy API Win32 będą miały mniejszy kąt nadmiarowy niż CRT. Czego tu mi brakuje?

Test kompilacyjny Kod źródłowy C++.


EDIT

Powtórzyłem test z 4KB odczytać bufory i korzystania trzy różne pliki (o tej samej treści), aby uniknąć buforowanie efektów, które mogłyby zakłócić pomiary wydajności, a teraz wyniki są zgodne z oczekiwaniami.
Na przykład, w przypadku pliku zawierającego ok 400 Mb Wyniki są następujące:

  1. mapowania pamięci Win32: 305.908 MS

  2. Win32 pliku (CreateFile) 451.402 MS

  3. C plik (FILE *): 460.579 ms


//////////////////////////////////////////////////////////////////////////////// 
// 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; 
    } 
} 

//////////////////////////////////////////////////////////////////////////////// 
+8

Aby zmniejszyć obciążenie podsystemu, implementacja 'PLIK' wykonuje własne buforowanie. Nie ma w tym nic magicznego. –

+7

W skrócie: jeśli wykonasz testy równoległe w pliku SAME, drugi wynik testu zostanie zniekształcony przez buforowanie danych pliku/metadanych. System Windows buforuje dane pliku, podobnie jak sprzęt kontrolera dysku. Nie mogłem powiedzieć od szybkiego przeczytania kodu, czy powyższe stwierdzenie ma zastosowanie, ale najczęściej jest przyczyną takich rzeczy. –

+1

Co się stanie, jeśli zwiększysz rozmiar bufora w 'count_char_using_win32_file()'? – NPE

Odpowiedz

11

Wystarczy prowadził kilka testów na moim komputerze, który pokazuje, że zwiększenie rozmiaru bufora rzeczywiście zwiększa wydajność:

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 

pewne kroki w Visual Studio 2012 debugger pokazuje, że rozmiar bufora metody FILE * wynosi 4096 bajtów przynajmniej na mojej maszynie. (I jak inni już powiedzieli, nazywa się to również ReadFile, chyba że czytasz z konsoli.)

Interesujące jest również to, że duże bufory nieznacznie spowalniają wydajność. Usunięcie operatora new poza testem również nie rozwiązuje problemu.

Najpierw test odwzorowany w pamięci był dla mnie dość powolny, ponieważ uruchomiłem go w trybie debugowania. Zaktualizowałem wszystkie wyniki kompilacją w trybie wydań. Mapowanie pamięci stało się pierwszym.

+2

Wolne odwzorowanie pamięci polega na uruchomieniu go w trybie debugowania z programu Visual Studio. Twórz w trybie Release. – paddy

+0

@paddy: Oczywiście zakładam, że uruchamiamy testy zbudowane w trybie Release ... –

+0

Tak, zdałem sobie z tego sprawę. Właśnie zauważyłem, że jeśli zbuduję twój program w trybie debugowania, widzę wolne mapowanie pamięci w tej samej kolejności, co opisane w tej odpowiedzi. – paddy

4

Czy jesteś pewien, że testujesz prawidłowo?
W jaki sposób rozliczasz pozycję dysku, czas wyszukiwania, buforowanie plików itp.?

stdio i win32 ostatecznie wywołują takie same połączenia z jądrem systemu Windows, aby otworzyć plik.

mmap robi rzeczy nieco inaczej, ponieważ może zarezerwować faktycznie czytanie danych, dopóki nie jest używany - jeśli mają wielkość i wydajność spraw stałym Plik mmap jest dobrym rozwiązaniem

9

Najszybszy dostęp do dysku I już kiedykolwiek osiągnięto użycie ReadFile. Ale specjalnie otworzyłem plik z flagami, aby spełnić wymagania dotyczące dostępu do dysku i buforowania. Jeśli użyjesz tego dosłownie, porównanie jest trochę ułomne.

Powinieneś przeczytać więcej o tej funkcji, a także CreateFile. Przekonasz się, że możesz czytać dane w (wielokrotnościach) bloków o rozmiarach sektorowych do pamięci wyrównanej do sektora. Wtedy wykonasz dodatkową robotę fread.

Jak powiedzieli inni, fread wykonuje własne buforowanie. Twoja implementacja buforowania z ReadFile nadal wymaga pracy.

Sprawdź MSDN. ALI informacje tam są. Konkretnie tutaj:

+1

Potrzebowałem zwykłego ** dostępu sekwencyjnego ** i otworzyłem plik z flagą "FILE_FLAG_SEQUENTIAL_SCAN". Czy możesz dokładniej określić swoją sugestię? Której flagi powinienem użyć, aby uzyskać optymalny dostęp do sekwencyjnego odczytu? –

3

Przy użyciu pamięci mapowane pliku, nie ma potrzeby, aby skopiować zawartość pliku do aplikacji - to jest odwzorowany w jako część pamięci wirtualnej bezpośrednio z systemu operacyjnego, więc kiedy uzyskujesz dostęp do zawartości pliku, należy po prostu odczytać ją bezpośrednio na stronie, która trafia do zmapowanej pamięci.

Jeśli wykonasz pracę poprawnie, gdy korzystasz z interfejsu Win32 API, powinno być szybsze C stdio, ponieważ w jego trakcie jest mniej narzutów. Jednak jest całkiem możliwe, że nie uzyskujesz idealnej równowagi między obciążeniem systemowym a "zbyt dużym buforem, więc czytanie trwa dłużej niż to konieczne". Proponuję spróbować z 4K lub 8K (może nawet 32K) jako bufor w twojej funkcjonalności Win32 API - rozmiar bufora będący wielokrotnością 4K jest idealny, ponieważ strona pamięci to (zwykle) 4KB. Mniej połączeń z interfejsem API powoduje mniejsze koszty, ale nie chcesz posuwać się zbyt daleko.

[Zrobiłem takie testy na Linuksie, i znalazłem podobne wyniki - i z mojego doświadczenia tam: używaj różnych plików dla każdego testu, w przeciwnym razie buforowanie twojego systemu plików pomoże testom, które zostaną uruchomione później!].

Powiązane problemy