¿Cómo escribir un búfer grande en un archivo binario en C++, rápidamente?

Resuelto Dominic Hofer asked hace 12 años • 12 respuestas

Estoy intentando escribir grandes cantidades de datos en mi SSD (unidad de estado sólido). Y por cantidades enormes me refiero a 80 GB.

Busqué soluciones en la web, pero la mejor que encontré fue esta:

#include <fstream>
const unsigned long long size = 64ULL*1024ULL*1024ULL;
unsigned long long a[size];
int main()
{
    std::fstream myfile;
    myfile = std::fstream("file.binary", std::ios::out | std::ios::binary);
    //Here would be some error handling
    for(int i = 0; i < 32; ++i){
        //Some calculations to fill a[]
        myfile.write((char*)&a,size*sizeof(unsigned long long));
    }
    myfile.close();
}

Compilado con Visual Studio 2010 y optimizaciones completas y ejecutado en Windows7, este programa alcanza un máximo de alrededor de 20 MB/s. Lo que realmente me molesta es que Windows puede copiar archivos de otro SSD a este SSD a una velocidad de entre 150 MB/s y 200 MB/s. Entonces al menos 7 veces más rápido. Por eso creo que debería poder ir más rápido.

¿Alguna idea de cómo puedo acelerar mi escritura?

Dominic Hofer avatar Jul 19 '12 22:07 Dominic Hofer
Aceptado

Esto hizo el trabajo (en el año 2012):

#include <stdio.h>
const unsigned long long size = 8ULL*1024ULL*1024ULL;
unsigned long long a[size];

int main()
{
    FILE* pFile;
    pFile = fopen("file.binary", "wb");
    for (unsigned long long j = 0; j < 1024; ++j){
        //Some calculations to fill a[]
        fwrite(a, 1, size*sizeof(unsigned long long), pFile);
    }
    fclose(pFile);
    return 0;
}

Acabo de cronometrar 8 GB en 36 segundos, que son aproximadamente 220 MB/s y creo que eso maximiza mi SSD. También vale la pena señalar que el código en la pregunta usó un núcleo al 100%, mientras que este código solo usa del 2 al 5%.

Muchas gracias a todos.

Actualización : Han pasado 5 años, ahora estamos en 2017. Los compiladores, el hardware, las bibliotecas y mis requisitos han cambiado. Es por eso que hice algunos cambios en el código e hice algunas mediciones nuevas.

Primero el código:

#include <fstream>
#include <chrono>
#include <vector>
#include <cstdint>
#include <numeric>
#include <random>
#include <algorithm>
#include <iostream>
#include <cassert>

std::vector<uint64_t> GenerateData(std::size_t bytes)
{
    assert(bytes % sizeof(uint64_t) == 0);
    std::vector<uint64_t> data(bytes / sizeof(uint64_t));
    std::iota(data.begin(), data.end(), 0);
    std::shuffle(data.begin(), data.end(), std::mt19937{ std::random_device{}() });
    return data;
}

long long option_1(std::size_t bytes)
{
    std::vector<uint64_t> data = GenerateData(bytes);

    auto startTime = std::chrono::high_resolution_clock::now();
    auto myfile = std::fstream("file.binary", std::ios::out | std::ios::binary);
    myfile.write((char*)&data[0], bytes);
    myfile.close();
    auto endTime = std::chrono::high_resolution_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}

long long option_2(std::size_t bytes)
{
    std::vector<uint64_t> data = GenerateData(bytes);

    auto startTime = std::chrono::high_resolution_clock::now();
    FILE* file = fopen("file.binary", "wb");
    fwrite(&data[0], 1, bytes, file);
    fclose(file);
    auto endTime = std::chrono::high_resolution_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}

long long option_3(std::size_t bytes)
{
    std::vector<uint64_t> data = GenerateData(bytes);

    std::ios_base::sync_with_stdio(false);
    auto startTime = std::chrono::high_resolution_clock::now();
    auto myfile = std::fstream("file.binary", std::ios::out | std::ios::binary);
    myfile.write((char*)&data[0], bytes);
    myfile.close();
    auto endTime = std::chrono::high_resolution_clock::now();

    return std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
}

int main()
{
    const std::size_t kB = 1024;
    const std::size_t MB = 1024 * kB;
    const std::size_t GB = 1024 * MB;

    for (std::size_t size = 1 * MB; size <= 4 * GB; size *= 2) std::cout << "option1, " << size / MB << "MB: " << option_1(size) << "ms" << std::endl;
    for (std::size_t size = 1 * MB; size <= 4 * GB; size *= 2) std::cout << "option2, " << size / MB << "MB: " << option_2(size) << "ms" << std::endl;
    for (std::size_t size = 1 * MB; size <= 4 * GB; size *= 2) std::cout << "option3, " << size / MB << "MB: " << option_3(size) << "ms" << std::endl;

    return 0;
}

Este código se compila con Visual Studio 2017 y g++ 7.2.0 (un nuevo requisito). Ejecuté el código con dos configuraciones:

  • Computadora portátil, Core i7, SSD, Ubuntu 16.04, g++ Versión 7.2.0 con -std=c++11 -march=native -O3
  • Escritorio, Core i7, SSD, Windows 10, Visual Studio 2017 versión 15.3.1 con /Ox /Ob2 /Oi /Ot /GT /GL /Gy

Lo que dio las siguientes medidas (después de descartar los valores de 1 MB, porque eran valores atípicos obvios): Ambas veces, la opción 1 y la opción 3 maximizan mi SSD. No esperaba ver esto, porque la opción 2 solía ser el código más rápido en mi vieja máquina en ese entonces.ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

TL;DR : Mis medidas indican que se debe usar std::fstreammás FILE.

Dominic Hofer avatar Jul 19 '2012 16:07 Dominic Hofer

Pruebe lo siguiente, en orden:

  • Tamaño de búfer más pequeño. Escribir ~2 MiB a la vez podría ser un buen comienzo. En mi última computadora portátil, ~512 KiB fue el punto ideal, pero aún no lo he probado en mi SSD.

    Nota: He notado que los buffers muy grandes tienden a disminuir el rendimiento. He notado pérdidas de velocidad al usar buffers de 16 MiB en lugar de buffers de 512 KiB antes.

  • Use _open(o _topensi desea que Windows sea correcto) para abrir el archivo, luego use _write. Esto probablemente evitará una gran cantidad de almacenamiento en búfer, pero no es seguro que lo haga.

  • Usando funciones específicas de Windows como CreateFiley WriteFile. Eso evitará cualquier almacenamiento en búfer en la biblioteca estándar.

user541686 avatar Jul 19 '2012 15:07 user541686

No veo ninguna diferencia entre std::stream/FILE/device. Entre buffering y no buffering.

Tenga en cuenta también:

  • Las unidades SSD "tienden" a disminuir la velocidad (tasas de transferencia más bajas) a medida que se llenan.
  • Las unidades SSD "tienden" a ralentizarse (tasas de transferencia más bajas) a medida que envejecen (debido a que los bits no funcionan).

Veo que el código se ejecuta en 63 segundos.
Por lo tanto, una velocidad de transferencia de: 260 M/s (mi SSD parece un poco más rápido que el tuyo).

64 * 1024 * 1024 * 8 /*sizeof(unsigned long long) */ * 32 /*Chunks*/

= 16G
= 16G/63 = 260M/s

No obtengo ningún aumento al pasar a ARCHIVO* desde std::fstream.

#include <stdio.h>

using namespace std;

int main()
{
    
    FILE* stream = fopen("binary", "w");

    for(int loop=0;loop < 32;++loop)
    {
         fwrite(a, sizeof(unsigned long long), size, stream);
    }
    fclose(stream);

}

Por lo tanto, la secuencia de C++ funciona tan rápido como lo permite la biblioteca subyacente.

Pero creo que es injusto comparar el sistema operativo con una aplicación integrada sobre el sistema operativo. La aplicación no puede hacer suposiciones (no sabe que las unidades son SSD) y, por lo tanto, utiliza los mecanismos de archivos del sistema operativo para la transferencia.

Si bien el sistema operativo no necesita hacer suposiciones. Puede indicar los tipos de unidades involucradas y utilizar la técnica óptima para transferir los datos. En este caso una transferencia directa de memoria a memoria. Intente escribir un programa que copie 80G de una ubicación en la memoria a otra y vea qué tan rápido es.

Editar

Cambié mi código para usar las llamadas de nivel inferior:
es decir, sin almacenamiento en búfer.

#include <fcntl.h>
#include <unistd.h>


const unsigned long long size = 64ULL*1024ULL*1024ULL;
unsigned long long a[size];
int main()
{
    int data = open("test", O_WRONLY | O_CREAT, 0777);
    for(int loop = 0; loop < 32; ++loop)
    {   
        write(data, a, size * sizeof(unsigned long long));
    }   
    close(data);
}

Esto no hizo ninguna diferencia.

NOTA : Mi unidad es una unidad SSD. Si tiene una unidad normal, es posible que vea una diferencia entre las dos técnicas anteriores. Pero como esperaba, la falta de almacenamiento en búfer y el almacenamiento en búfer (cuando se escriben fragmentos grandes mayores que el tamaño del búfer) no hacen ninguna diferencia.

Edición 2:

¿Has probado el método más rápido para copiar archivos en C++?

int main()
{
    std::ifstream  input("input");
    std::ofstream  output("ouptut");

    output << input.rdbuf();
}
Martin York avatar Jul 19 '2012 16:07 Martin York

La mejor solución es implementar una escritura asíncrona con doble almacenamiento en búfer.

Mira la línea del tiempo:

------------------------------------------------>
FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|FF|WWWWWWWW|

La 'F' representa el tiempo para llenar el búfer y la 'W' representa el tiempo para escribir el búfer en el disco. Entonces el problema es perder el tiempo entre escribir buffers en un archivo. Sin embargo, al implementar la escritura en un hilo separado, puedes comenzar a llenar el siguiente búfer de inmediato de esta manera:

------------------------------------------------> (main thread, fills buffers)
FF|ff______|FF______|ff______|________|
------------------------------------------------> (writer thread)
  |WWWWWWWW|wwwwwwww|WWWWWWWW|wwwwwwww|

F - llenando el primer búfer
f - llenando el segundo búfer
W - escribiendo el primer búfer en el archivo
w - escribiendo el segundo búfer en el archivo
_ - espere mientras se completa la operación

Este enfoque con intercambios de búfer es muy útil cuando llenar un búfer requiere un cálculo más complejo (por lo tanto, más tiempo). Siempre implemento una clase CSequentialStreamWriter que oculta la escritura asincrónica en su interior, por lo que para el usuario final la interfaz solo tiene funciones de escritura.

Y el tamaño del búfer debe ser múltiplo del tamaño del clúster de discos. De lo contrario, terminará con un rendimiento deficiente al escribir un único búfer en 2 grupos de discos adyacentes.

Escribiendo el último búfer.
Cuando llama a la función Escribir por última vez, debe asegurarse de que el búfer actual que se está llenando también se escriba en el disco. Por lo tanto, CSequentialStreamWriter debería tener un método separado, digamos Finalize (vaciado final del búfer), que debería escribir en el disco la última porción de datos.

Manejo de errores.
Mientras el código comienza a llenar el segundo búfer y el primero se escribe en un hilo separado, pero la escritura falla por algún motivo, el hilo principal debe estar al tanto de ese error.

------------------------------------------------> (main thread, fills buffers)
FF|fX|
------------------------------------------------> (writer thread)
__|X|

Supongamos que la interfaz de CSequentialStreamWriter tiene una función de escritura que devuelve bool o arroja una excepción, por lo que tiene un error en un hilo separado, debe recordar ese estado, por lo que la próxima vez que llame a Write o Finilize en el hilo principal, el método regresará. Falso o generará una excepción. Y realmente no importa en qué momento dejó de llenar un búfer, incluso si escribió algunos datos después del error; lo más probable es que el archivo esté dañado y sea inútil.

HandMadeOX avatar Aug 28 '2014 00:08 HandMadeOX

Sugeriría probar el mapeo de archivos . Lo usé mmapen el pasado, en un entorno UNIX, y quedé impresionado por el alto rendimiento que pude lograr.

Ralph avatar Jul 19 '2012 21:07 Ralph