Ottimizzare C++/Tecniche generali di ottimizzazione/Input/Output

Wikibooks, manuali e libri di testo liberi.
Ottimizzare C++
modifica
CopertinaOttimizzare C++/Copertina
  1. IntroduzioneOttimizzare C++/Introduzione
  2. Ciclo di vita dell’ottimizzazioneOttimizzare C++/Ciclo di vita dell’ottimizzazione
  3. Scrivere codice C++ efficienteOttimizzare C++/Scrivere codice C++ efficiente
    1. Costrutti che migliorano le prestazioniOttimizzare C++/Scrivere codice C++ efficiente/Costrutti che migliorano le prestazioni
    2. Costrutti che peggiorano le prestazioniOttimizzare C++/Scrivere codice C++ efficiente/Costrutti che peggiorano le prestazioni
    3. Costruzioni e distruzioniOttimizzare C++/Scrivere codice C++ efficiente/Costruzioni e distruzioni
    4. Allocazioni e deallocazioniOttimizzare C++/Scrivere codice C++ efficiente/Allocazioni e deallocazioni
    5. Accesso alla memoriaOttimizzare C++/Scrivere codice C++ efficiente/Accesso alla memoria
    6. Uso dei threadOttimizzare C++/Scrivere codice C++ efficiente/Uso dei thread
  4. Tecniche generali di ottimizzazioneOttimizzare C++/Tecniche generali di ottimizzazione
    1. Input/OutputOttimizzare C++/Tecniche generali di ottimizzazione/Input/Output
    2. CachingOttimizzare C++/Tecniche generali di ottimizzazione/Caching
    3. OrdinamentoOttimizzare C++/Tecniche generali di ottimizzazione/Ordinamento
    4. Altre tecnicheOttimizzare C++/Tecniche generali di ottimizzazione/Altre tecniche
  5. Ottimizzazione del codice C++Ottimizzare C++/Ottimizzazione del codice C++
    1. Allocazione e deallocazioneOttimizzare C++/Ottimizzazione del codice C++/Allocazione e deallocazione
    2. Supporto run-timeOttimizzare C++/Ottimizzazione del codice C++/Supporto run-time
    3. Numero di istruzioniOttimizzare C++/Ottimizzazione del codice C++/Numero di istruzioni
    4. Costruzioni e distruzioniOttimizzare C++/Ottimizzazione del codice C++/Costruzioni e distruzioni
    5. PipelineOttimizzare C++/Ottimizzazione del codice C++/Pipeline
    6. Accesso alla memoriaOttimizzare C++/Ottimizzazione del codice C++/Accesso alla memoria
    7. Operazioni velociOttimizzare C++/Ottimizzazione del codice C++/Operazioni veloci

Formato binario[modifica]

Invece di memorizzare i dati su file in formato testuale, memorizzali in formato binario.

In media, i numeri in formato binario occupano meno spazio dei numeri formattati, e quindi richiedono meno tempo per essere trasferiti dalla memoria al disco o viceversa, ma, soprattutto, se i dati vengono trasferiti nello stesso formato usato dal processore, non c'è bisogno di nessuna costosa conversione dal formato testuale al formato binario o viceversa.

Gli svantaggi del formato binario sono che i dati non sono facilmente leggibili e che tale formato può dipendere dall'architettura del processore.

File aperti[modifica]

Invece di aprire e chiudere un file di utilizzo frequente ogni volta che ci accedi, aprilo solamente la prima volta che ci accedi, e chiudilo quando hai finito di usarlo.

Chiudere e riaprire un file di disco richiede un tempo variabile, ma approssimativamente lo stesso che ci vuole per leggere dai 15 ai 20 KB di dati dalla cache del disco.

Perciò, se devi accedere spesso a un file, puoi evitare questa inefficienza aprendo il file solamente una volta prima di accedervi, mantenerlo aperto spostando il gestore del file a un ambito più esterno, e chiudendo il file quando hai finito.

Buffer di I/O[modifica]

Invece di fare molte operazioni di I/O su singoli oggetti piccoli o piccolissimi, fai operazioni di I/O su un buffer di 4 KB contenente molti oggetti.

Anche se le operazioni di I/O del supporto run-time sono bufferizzate, l'inefficienza di molte chiamate alle funzioni di I/O costa di più che copiare gli oggetti in un buffer.

I buffer grandi non hanno una buona località di riferimento dei dati.

Memory-mapped-file[modifica]

Eccetto che in una sezione critica di un sistema real-time, se devi accedere a gran parte di un file binario in modo non-sequenziale, invece di accedervi ripetutamente con operazioni di seek, oppure di caricarlo tutto in un buffer dell’applicazione, usa un memory-mapped-file, se il tuo sistema operativo fornisce tale strumento.

Quando si deve accedere a gran parte di un file binario in modo non-sequenziale, ci sono due tecniche alternative standard:

  • Aprire il file senza leggerne il contenuto; e ogni volta che si deve leggere un dato, saltare al punto di interesse usando una operazione di posizionamento nel file (seek), e leggere il dato usando un'operazione di lettura.
  • Allocare un buffer grande quanto tutto il file, aprire il file, leggere tutto il contenuto del file nel buffer, chiudere il file; e ogni volta che si deve leggere un dato, cercarlo nel buffer.

Rispetto alla prima tecnica, usando i memory-mapped-file ogni operazione di posizionamento viene sostituita da una semplice assegnazione a un puntatore, e ogni operazione di lettura da file viene sostituita da una semplice copia da memoria a memoria. Anche supponendo che i dati siano già nella disk cache, entrambe le operazioni effettuate con i memory-mapped-files sono notevolmente più veloci delle operazioni effettuate sui file, in quanto queste ultime comportano altrettante chiamate di libreria, le quali a loro volta effettuano chiamate di sistema.

Rispetto alla tecnica di precaricare in memoria l'intero file, usando i memory-mapped-file si hanno i seguenti vantaggi:

  • Usando le primitive di lettura di file, i dati vengono normalmente letti prima nella cache del disco e poi nella memoria del processo, mentre con i memory-mapped-file si accede direttamente al buffer caricato dal disco, risparmiando così sia un'operazione di copia che lo spazio di memoria per la cache del disco. Analoga situazione si ha per la scrittura su disco.
  • Leggendo tutto il file, il programma si blocca per un tempo significativo per leggere il file, mentre usando un memory-mapped-file tale tempo viene distribuito nel corso dell'elaborazione, man mano che si accede alle varie parti del file.
  • Se in alcune esecuzioni serve solo una piccola parte del file, il memory-mapped-file carica in memoria solo quelle parti.
  • Se più processi devono caricare in memoria lo stesso file, lo spazio di memoria viene allocato per ogni processo, mentre usando i memory-mapped-file il sistema operativo tiene in memoria una sola copia dei dati, condivisa da tutti i processi.
  • In condizioni di scarsità di memoria, il sistema operativo scrive nell'area di swap del disco anche la memoria del processo che non è stata modificata, mentre si limita a scartare le pagine non modificate del memory-mapped-file, senza scriverle sul disco.

Tuttavia, l’uso di memory mapped file non è appropriato in una porzione critica di un sistema real-time, in quanto l'accesso a tali dati ha una latenza fortemente variabile a seconda che il dato acceduto sia già stato caricato nella memoria di sistema o sia ancora solamente su disco.

A rigore, questa è una tecnica dipendente dalla piattaforma, in quanto la funzionalità dei memory-mapped-file non esiste in tutti i sistemi operativi. Tuttavia, dato che tale funzionalità esiste in tutti i principali sistemi operativi dotati di memoria virtuale, questa tecnica è di ampia applicabilità.

Ecco due classi che incapsulano le primitive di accesso a un file tramite memory-mapped-file, seguite da un piccolo programma che dimostra l'uso di tali classi. Sono utilizzabili sia nei sistemi operativi di tipo Posix (come Unix, Linux, e Mac OS X), sia in ambiente Windows. La classe MemoryFile permette sia di scrivere che di leggere un file, nonché di cambiarne la dimensione. La classe InputMemoryFile permette solo di leggere un file, ma è più semplice e sicura, e quindi è consigliabile nel caso in cui non si abbia bisogno di modificare il file.

File "memory_file.hpp":

#ifndef MEMORY_FILE_HPP
#define MEMORY_FILE_HPP
 
#include <stddef.h> // or <cstddef> and using std::size_t
 
/*
  Read-only memory-mapped file wrapper.
  It handles only files that can be wholly loaded
  into the address space of the process.
  The constructor opens the file, the destructor closes it.
  The "data" function returns a pointer to the beginning of the file,
  if the file has been successfully opened, otherwise it returns 0.
  The "size" function returns the length of the file in bytes,
  if the file has been successfully opened, otherwise it returns 0.
*/
class InputMemoryFile {
public:
    explicit InputMemoryFile(const char *pathname);
    ~InputMemoryFile();
    const char* data() const { return data_; }
    size_t size() const { return size_; }
private:
    const char* data_;
    size_t size_;
#if defined(_WIN32)
    typedef void* HANDLE;
    HANDLE file_handle_;
    HANDLE file_mapping_handle_;
#else
    int file_handle_;
#endif
};
 
/*
  Read/write memory-mapped file wrapper.
  It handles only files that can be wholly loaded
  into the address space of the process.
  The constructor opens the file, the destructor closes it.
  The "data" function returns a pointer to the beginning of the file,
  if the file has been successfully opened, otherwise it returns 0.
  The "size" function returns the initial length of the file in bytes,
  if the file has been successfully opened, otherwise it returns 0.
  Afterwards it returns the size the physical file will get if it is closed now.
  The "resize" function changes the number of bytes of the significant
  part of the file. The resulting size can be retrieved
  using the "size" function.
  The "reserve" grows the physical file to the specified number of bytes.
  The size of the resulting file can be retrieved using "capacity".
  Memory mapped files cannot be shrunk;
  a value smaller than the current capacity is ignored.
  The "capacity()" function return the size the physical file has at this time.
  The "flush" function ensure that the disk is updated
  with the data written in memory.
*/
class MemoryFile {
public:
    enum e_open_mode {
         if_exists_fail_else_create
        ,if_exists_keep_else_fail
        ,if_exists_keep_else_create
        ,if_exists_truncate_else_fail
        ,if_exists_truncate_else_create
    };
    MemoryFile(const char *pathname, e_open_mode open_mode);
    ~MemoryFile();
    char* data() { return data_; }
    void resize(size_t new_size);
    void reserve(size_t new_capacity);
    size_t size() const { return size_; }
    size_t capacity() const { return capacity_; }
    bool flush();
private:
    char* data_;
    size_t size_;
    size_t capacity_;
#if defined(_WIN32)
    typedef void * HANDLE;
    HANDLE file_handle_;
    HANDLE file_mapping_handle_;
#else
    int file_handle_;
#endif
};
#endif // MEMORY_FILE_HPP

File "memory_file.cpp":

#include "memory_file.hpp"
#if defined(_WIN32)
// our typedefs MemoryFile::HANDLE and InputMemoryFile::HANDLE play nice with typedef HANDLE
#include <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#endif
 
InputMemoryFile::InputMemoryFile(const char *pathname):
    data_(0),
    size_(0),
#if defined(_WIN32)
    file_handle_(INVALID_HANDLE_VALUE),
    file_mapping_handle_(INVALID_HANDLE_VALUE)
#else
    file_handle_(-1)
#endif
{
#if defined(_WIN32)
    file_handle_ = ::CreateFile(pathname, GENERIC_READ,
        FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
    if (file_handle_ == INVALID_HANDLE_VALUE) return;
    file_mapping_handle_ = ::CreateFileMapping(
        file_handle_, 0, PAGE_READONLY, 0, 0, 0);
    if (file_mapping_handle_ == INVALID_HANDLE_VALUE) return;
    data_ = static_cast<char*>(::MapViewOfFile(
        file_mapping_handle_, FILE_MAP_READ, 0, 0, 0));
    if (data_) size_ = ::GetFileSize(file_handle_, 0);
#else
    file_handle_ = ::open(pathname, O_RDONLY);
    if (file_handle_ == -1) return;
    struct stat sbuf;
    if (::fstat(file_handle_, &sbuf) == -1) return;
    data_ = static_cast<const char*>(::mmap(
        0, sbuf.st_size, PROT_READ, MAP_SHARED, file_handle_, 0));
    if (data_ == MAP_FAILED) data_ = 0;
    else size_ = sbuf.st_size;
#endif
}
 
InputMemoryFile::~InputMemoryFile() {
#if defined(_WIN32)
    ::UnmapViewOfFile(data_);
    ::CloseHandle(file_mapping_handle_);
    ::CloseHandle(file_handle_);
#else
    ::munmap(const_cast<char*>(data_), size_);
    ::close(file_handle_);
#endif
}
 
MemoryFile::MemoryFile(const char *pathname, e_open_mode open_mode):
    data_(0),
    size_(0),
#if defined(_WIN32)
    file_handle_(INVALID_HANDLE_VALUE),
    file_mapping_handle_(INVALID_HANDLE_VALUE)
#else
    file_handle_(-1)
#endif
{
#if defined(_WIN32)
    int windows_open_mode;
    switch (open_mode)
    {
    case if_exists_fail_else_create:
        windows_open_mode = CREATE_NEW;
        break;
    case if_exists_keep_else_fail:
        windows_open_mode = OPEN_EXISTING;
        break;
    case if_exists_keep_else_create:
        windows_open_mode = OPEN_ALWAYS;
        break;
    case if_exists_truncate_else_fail:
        windows_open_mode = TRUNCATE_EXISTING;
        break;
    case if_exists_truncate_else_create:
        windows_open_mode = CREATE_ALWAYS;
        break;
    default: return;
    }
    const size_t min_file_size = 4096;
    file_handle_ = ::CreateFile(pathname, GENERIC_READ | GENERIC_WRITE,
        0, 0, windows_open_mode, FILE_ATTRIBUTE_NORMAL, 0);
    if (file_handle_ == INVALID_HANDLE_VALUE) return;
    size_t initial_file_size = ::GetFileSize(file_handle_, 0);
    size_t adjusted_file_size = initial_file_size == 0 ? min_file_size : initial_file_size;
    file_mapping_handle_ = ::CreateFileMapping(
        file_handle_, 0, PAGE_READWRITE, 0, adjusted_file_size, 0);
    if (file_mapping_handle_ == INVALID_HANDLE_VALUE) return;
    data_ = static_cast<char*>(::MapViewOfFile(
        file_mapping_handle_, FILE_MAP_WRITE, 0, 0, 0));
    if (data_) {
        size_ = initial_file_size;
        capacity_ = adjusted_file_size;
    }
#else
    int posix_open_mode = O_RDWR;
    switch (open_mode)
    {
    case if_exists_fail_else_create:
        posix_open_mode |= O_EXCL | O_CREAT;
        break;
    case if_exists_keep_else_fail:
        break;
    case if_exists_keep_else_create:
        posix_open_mode |= O_CREAT;
        break;
    case if_exists_truncate_else_fail:
        posix_open_mode |= O_TRUNC;
        break;
    case if_exists_truncate_else_create:
        posix_open_mode |= O_TRUNC | O_CREAT;
        break;
    default: return;
    }
    const size_t min_file_size = 4096;
    file_handle_ = ::open(pathname, posix_open_mode, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if (file_handle_ == -1) return;
    struct stat sbuf;
    if (::fstat(file_handle_, &sbuf) == -1) return;
    size_t initial_file_size = sbuf.st_size;
    size_t adjusted_file_size = (initial_file_size == 0) ? min_file_size : initial_file_size;
    ::ftruncate(file_handle_, adjusted_file_size);
    data_ = static_cast<char*>(::mmap(
        0, adjusted_file_size, PROT_READ | PROT_WRITE, MAP_SHARED, file_handle_, 0));
    if (data_ == MAP_FAILED) data_ = 0;
    else {
        size_ = initial_file_size;
        capacity_ = adjusted_file_size;
    }
#endif
}
 
void MemoryFile::resize(size_t new_size) {
    if (new_size > capacity_) reserve(new_size);
    size_ = new_size;
}
 
void MemoryFile::reserve(size_t new_capacity) {
    if (new_capacity <= capacity_) return;
#if defined(_WIN32)
    ::UnmapViewOfFile(data_);
    ::CloseHandle(file_mapping_handle_);
    file_mapping_handle_ = ::CreateFileMapping(
        file_handle_, 0, PAGE_READWRITE, 0, new_capacity, 0);
    capacity_ = new_capacity;
    data_ = static_cast<char*>(::MapViewOfFile(
        file_mapping_handle_, FILE_MAP_WRITE, 0, 0, 0));
#else
    ::munmap(data_, size_);
    ::ftruncate(file_handle_, new_capacity);
    data_ = static_cast<char*>(::mmap(
        0, new_capacity, PROT_READ | PROT_WRITE, MAP_SHARED, file_handle_, 0));
    if (data_ == MAP_FAILED) data_ = 0;
    capacity_ = new_capacity;
#endif
}
 
MemoryFile::~MemoryFile() {
#if defined(_WIN32)
    ::UnmapViewOfFile(data_);
    ::CloseHandle(file_mapping_handle_);
    if (size_ != capacity_)
    {
        ::SetFilePointer(file_handle_, size_, 0, FILE_BEGIN);
        ::SetEndOfFile(file_handle_);
    }
    ::CloseHandle(file_handle_);
#else
    ::munmap(data_, size_);
    if (size_ != capacity_)
    {
        ::ftruncate(file_handle_, size_);
    }
    ::close(file_handle_);
#endif
}
 
bool MemoryFile::flush() {
#if defined(_WIN32)
    return ::FlushViewOfFile(data_, size_) != 0;
#else
    return ::msync(data_, size_, MS_SYNC) == 0;
#endif
}

File "memory_file_test.cpp":

#include "memory_file.hpp"
#include <iostream> // for std::cerr
 
// TODO review interface, reader cannot tell what CopyFile(backupfile, preciousfile, false) will do
bool CopyFile(const char* source, const char* dest, bool overwrite)
{
    InputMemoryFile source_mf(source);
    if (! source_mf.data()) return false;
    MemoryFile dest_mf(dest, overwrite ?
        MemoryFile::if_exists_truncate_else_create :
        MemoryFile::if_exists_fail_else_create);
    if (! dest_mf.data()) return false;
    dest_mf.resize(source_mf.size());
    if (source_mf.size() != dest_mf.size()) return false;
    std::copy(source_mf.data(), source_mf.data() + source_mf.size(),
        dest_mf.data());
    return true;
}
 
int main() {
    if (! CopyFile("memory_file_test.cpp", "copy.tmp", true)) { 
        std::cerr << "Copy failed" << std::endl;
        return 1;
    }
}