2009-07-29 25 views
5

Al solucionar algunos problemas de rendimiento en nuestras aplicaciones, descubrí que las funciones de C stdio.h (y, al menos para nuestro proveedor, las clases fstream de C++) son seguras para los hilos. Como resultado, cada vez que hago algo tan simple como fgetc, el RTL tiene que adquirir un bloqueo, leer un byte y liberar el bloqueo.E/S de archivo sin hilos en C/C++

Esto no es bueno para el rendimiento.

¿Cuál es la mejor forma de obtener E/S de archivos que no sean de rosca en C y C++, para que pueda administrar el bloqueo y obtener un mejor rendimiento?

  • MSVC ofrece _fputc_nolock, y GCC proporciona unlocked_stdio y flockfile, pero no puedo encontrar ningún funciones similares en mi compilador (CodeGear C++ Builder).
  • Podría usar la API de Windows sin procesar, pero eso no es portátil y supongo que sería más lento que un fgetc desbloqueado para la E/S de carácter a la vez.
  • Podría cambiar a algo como el Apache Portable Runtime, pero eso podría ser mucho trabajo.

¿Cómo se acercan los demás a esto?

Editar: Dado que algunas personas se preguntaban, había probado esto antes de publicarlo. fgetc no hace llamadas al sistema si puede satisfacer las lecturas de su búfer, pero aún así bloquea, por lo que el bloqueo termina tomando un enorme porcentaje de tiempo (cientos de bloqueos para adquirir y liberar un solo bloque de datos leídos del disco)) No hacer una E/S de personaje a personaje sería una solución, pero las clases fstream de C++ Builder desafortunadamente usan fgetc (así que si quiero usar las clases iostream, estoy atascado), y tengo mucho del código heredado que usa fgetc y amigos para leer los campos de los archivos de estilo de registro (lo que sería razonable si no fuera por problemas de bloqueo).

+0

Las funciones stdio.h de C no son seguras para hilos; ese es tu vendedor también. – MSalters

+0

No solo mi proveedor; POSIX, por ejemplo, lo requiere. –

Respuesta

3

Simplemente no haría IO un par a la vez si es sensible al rendimiento.

+0

todas las operaciones a las secuencias tienen como resultado IO de caracteres. Debe trabajar con los búferes de transmisión. He publicado a continuación cómo ... – ovanes

1

fgetc es casi seguro que no lee un byte cada vez que lo llama (donde "leer" me refiero a invocar una llamada al sistema para realizar E/S). Busque en otro lado su cuello de botella de rendimiento, ya que probablemente este no sea el problema, y ​​el uso de funciones inseguras ciertamente no es la solución. Cualquier control de bloqueo que realice probablemente será menos eficiente que el manejo realizado por las rutinas estándar.

+1

No lee un byte a la vez, pero puede bloquearlo cada vez. Por cierto, tomar un bloqueo es obligatorio en POSIX, hay getc_unlocked() (y una variante sin desbloqueo de las funciones IO de char by char, y funciones de bloqueo para que puedan ser protegidas). – AProgrammer

1

La manera más fácil sería leer todo el archivo en la memoria, y luego proporcionar su propia interfaz similar a fgetc a ese búfer.

1

¿Por qué no solo la memoria asigna el archivo? El mapeo de memoria es portátil (excepto en Windows Vista, que requiere que salte las esperanzas de usarlo ahora, las dumbasses). De todos modos, asigne un mapa a su archivo en la memoria y haga su propio bloqueo/no bloqueo en la ubicación de la memoria resultante.

El sistema operativo maneja todos los bloqueos necesarios para leer realmente desde el disco; NUNCA podrá eliminar esta sobrecarga. Pero su sobrecarga de procesamiento, por otro lado, no se verá afectada por un bloqueo extraño que no sea el que usted mismo hace.

1

el enfoque multiplataforma es bastante simple. Evite funciones u operadores donde el estándar especifique que deben usar centinela. centinela es una clase interna en clases iostream que garantiza la coherencia de la secuencia para cada carácter de salida y en el entorno de subprocesos múltiples bloquea el mutex relacionado con la secuencia para cada carácter que se está emitiendo.Esto evita las condiciones de carrera, a bajo nivel, pero todavía hace que la salida ilegible, ya que las cadenas de dos hilos podrían ser de salida al mismo tiempo como el siguiente ejemplo establece:

hilo 1 se debe escribir: abc
hilo 2 debe escribir: def

El resultado puede verse como: adebcf en lugar de abcdef o defabc. Esto se debe a que centinela se implementa para bloquear y desbloquear por carácter.

El estándar lo define para todas las funciones y operadores que se ocupan de istream o ostream. La única forma de evitar esto es usar almacenamientos intermedios de flujo y su propio bloqueo (por cadena, por ejemplo).

He escrito una aplicación, que produce algunos datos en un archivo y mide la velocidad. Si agrega aquí una función que permite usar directamente el fstream sin usar el buffer y flush, verá la diferencia de velocidad. Utiliza boost, pero espero que no sea un problema para ti. Intenta eliminar todos los streambuffers y ver la diferencia con y sin ellos. En mi caso, el inconveniente de rendimiento fue el factor 2-3 o más.

El following article de N. Myers explicará cómo funcionan las configuraciones regionales y centinela en C++ IOStreams. Y con seguridad debe buscar en el documento estándar ISO C++, cuyas funciones usan centinela.

buena suerte,
Ovanes

#include <vector> 
#include <fstream> 
#include <iterator> 
#include <algorithm> 
#include <iostream> 
#include <cassert> 
#include <cstdlib> 

#include <boost/progress.hpp> 
#include <boost/shared_ptr.hpp> 

double do_copy_via_streambuf() 
{ 
    const size_t len = 1024*2048; 
    const size_t factor = 5; 
    ::std::vector<char> data(len, 1); 

    std::vector<char> buffer(len*factor, 0); 

    ::std::ofstream 
    ofs("test.dat", ::std::ios_base::binary|::std::ios_base::out); 
    noskipws(ofs); 

    std::streambuf* rdbuf = ofs.rdbuf()->pubsetbuf(&buffer[0], buffer.size()); 

    ::std::ostreambuf_iterator<char> oi(rdbuf); 

    boost::progress_timer pt; 

    for(size_t i=1; i<=250; ++i) 
    { 
    ::std::copy(data.begin(), data.end(), oi); 
    if(0==i%factor) 
     rdbuf->pubsync(); 
    } 

    ofs.flush(); 
    double rate = 500/pt.elapsed(); 
    std::cout << rate << std::endl; 
    return rate; 
} 

void count_avarage(const char* op_name, double (*fct)()) 
{ 
    double av_rate=0; 
    const size_t repeat = 1; 
    std::cout << "doing " << op_name << std::endl; 
    for(size_t i=0; i<repeat; ++i) 
     av_rate+=fct(); 

    std::cout << "average rate for " << op_name << ": " << av_rate/repeat 
      << "\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n" 
      << std::endl; 
} 


int main() 
{ 
    count_avarage("copy via streambuf iterator", do_copy_via_streambuf); 
    return 0; 
} 
1

Una cosa a considerar es la construcción de un tiempo de ejecución personalizado. La mayoría de los compiladores proporcionan la fuente a la biblioteca de tiempo de ejecución (me sorprendería que no estuviera en el paquete C++ Builder).

Esto podría terminar siendo un montón de trabajo, pero tal vez han localizado el soporte de subprocesos para hacer algo como esto fácil. Por ejemplo, con el compilador del sistema integrado que estoy usando, está diseñado para esto: tienen anzuelos documentados para agregar las rutinas de bloqueo. Sin embargo, es posible que esto pueda ser un dolor de cabeza de mantenimiento, incluso si resulta relativamente fácil inicialmente.

Otra ruta similar sería hablar con alguien como Dinkumware sobre el uso de un tercero que proporciona las capacidades que necesita.

Cuestiones relacionadas