Gran diferencia (x9) en el tiempo de ejecución entre código casi idéntico en C y C ++

85

Estaba intentando resolver este ejercicio de www.spoj.com: FCTRL - Factorial

Realmente no tienes que leerlo, solo hazlo si tienes curiosidad :)

Primero lo implementé en C ++ (aquí está mi solución):

#include <iostream>
using namespace std;

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    std::ios_base::sync_with_stdio(false); // turn off synchronization with the C library’s stdio buffers (from https://stackoverflow.com/a/22225421/5218277)

    cin >> num_of_inputs;

    while (num_of_inputs--)
    {
        cin >> fact_num;

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        cout << num_of_trailing_zeros << "\n";
    }

    return 0;
}

Lo cargué como la solución para g ++ 5.1

El resultado fue: Tiempo 0,18 Mem 3,3 M Resultados de ejecución de C ++

Pero luego vi algunos comentarios que afirmaban que su tiempo de ejecución era inferior a 0,1. Como no podía pensar en un algoritmo más rápido, intenté implementar el mismo código en C :

#include <stdio.h>

int main() {
    unsigned int num_of_inputs;
    unsigned int fact_num;
    unsigned int num_of_trailing_zeros;

    scanf("%d", &num_of_inputs);

    while (num_of_inputs--)
    {
        scanf("%d", &fact_num);

        num_of_trailing_zeros = 0;

        for (unsigned int fives = 5; fives <= fact_num; fives *= 5)
            num_of_trailing_zeros += fact_num/fives;

        printf("%d", num_of_trailing_zeros);
        printf("%s","\n");
    }

    return 0;
}

Lo cargué como solución para gcc 5.1

Esta vez el resultado fue: Tiempo 0.02 Mem 2.1M Resultados de ejecución de C

Ahora el código es casi el mismo , agregué std::ios_base::sync_with_stdio(false);al código C ++ como se sugirió aquí para desactivar la sincronización con los búferes stdio de la biblioteca C. También he dividido la printf("%d\n", num_of_trailing_zeros);que printf("%d", num_of_trailing_zeros); printf("%s","\n");para compensar doble llamada de operator<<en cout << num_of_trailing_zeros << "\n";.

Pero todavía vi un mejor rendimiento x9 y un menor uso de memoria en el código C frente a C ++.

¿Porqué es eso?

EDITAR

Me fijo unsigned longa unsigned inten el código C. Debería haber sido así unsigned inty los resultados que se muestran arriba están relacionados con la unsigned intversión new ( ).

Alex Lop.
fuente
31
Los flujos de C ++ son extremadamente lentos por diseño. Porque lento y constante gana la carrera. : P ( corre antes de que me
llame la atención
7
La lentitud no proviene de la seguridad o la adaptabilidad. Está sobrediseñado con todas las banderas de flujo.
Karoly Horvath
8
@AlexLop. Usar a std::ostringstreampara acumular la salida y enviarlo a std::cout todos a la vez al final reduce el tiempo a 0.02. El uso std::couten bucle es simplemente más lento en su entorno y no creo que haya una forma sencilla de mejorarlo.
Blastfurnace
6
¿A nadie más le preocupa el hecho de que estos tiempos se obtuvieron utilizando ideone?
ildjarn
6
@Olaf: Me temo que no estoy de acuerdo, este tipo de pregunta está muy relacionada con el tema de todas las etiquetas elegidas. C y C ++ están lo suficientemente cerca en general que tal diferencia en el rendimiento requiere una explicación. Me alegro de haberlo encontrado. Tal vez GNU libc ++ debería mejorarse como consecuencia.
chqrlie

Respuestas:

56

Ambos programas hacen exactamente lo mismo. Utilizan exactamente el mismo algoritmo y, dada su baja complejidad, su rendimiento está vinculado principalmente a la eficiencia del manejo de entrada y salida.

escanear la entrada con scanf("%d", &fact_num);un lado y cin >> fact_num;el otro no parece muy costoso de ninguna manera. De hecho, debería ser menos costoso en C ++, ya que el tipo de conversión se conoce en tiempo de compilación y el compilador de C ++ puede invocar directamente el analizador correcto. Lo mismo ocurre con la salida. Incluso se asegura de escribir una llamada separada para printf("%s","\n");, pero el compilador de C es lo suficientemente bueno como para compilar esto como una llamada a putchar('\n');.

Entonces, considerando la complejidad tanto de la E / S como del cálculo, la versión C ++ debería ser más rápida que la versión C.

La desactivación completa del almacenamiento en búfer stdoutralentiza la implementación de C a algo incluso más lento que la versión de C ++. Otra prueba de AlexLop con un fflush(stdout);después de la última printfarroja un rendimiento similar al de la versión C ++. No es tan lento como deshabilitar completamente el almacenamiento en búfer porque la salida se escribe en el sistema en pequeños fragmentos en lugar de un byte a la vez.

Esto parece apuntar a un comportamiento específico en su biblioteca C ++: sospecho que la implementación de su sistema de ciny coutdescarga la salida coutcuando se solicita la entrada cin. Algunas bibliotecas de C también hacen esto, pero generalmente solo cuando se lee / escribe desde y hacia la terminal. La evaluación comparativa realizada por el sitio www.spoj.com probablemente redirige la entrada y salida hacia y desde archivos.

AlexLop hizo otra prueba: leer todas las entradas a la vez en un vector y luego calcular y escribir toda la salida ayuda a comprender por qué la versión C ++ es mucho más lenta. Aumenta el rendimiento al de la versión C, esto prueba mi punto y elimina las sospechas sobre el código de formato C ++.

Otra prueba de Blastfurnace, que almacena todas las salidas en una std::ostringstreamy la descarga de una sola vez al final, mejora el rendimiento de C ++ al de la versión básica de C. QED.

El entrelazado de entrada ciny salida coutparece causar un manejo de E / S muy ineficiente, anulando el esquema de almacenamiento en búfer de flujo. reduciendo el rendimiento por un factor de 10.

PD: su algoritmo es incorrecto fact_num >= UINT_MAX / 5porque fives *= 5se desbordará y se ajustará antes de que se convierta > fact_num. Puede corregir esto haciendo fivesun unsigned longo un unsigned long longsi uno de estos tipos es mayor que unsigned int. También se usa %ucomo scanfformato. Tienes suerte de que los chicos de www.spoj.com no sean demasiado estrictos en sus puntos de referencia.

EDITAR: Como se explica más adelante por vitaux, este comportamiento es de hecho obligatorio por el estándar C ++. cinestá vinculado coutde forma predeterminada. Una operación de entrada cinpara la que el búfer de entrada necesita rellenarse hará coutque se vacíe la salida pendiente. En la implementación del OP, cinparece vaciar coutsistemáticamente, lo cual es un poco excesivo y visiblemente ineficiente.

Ilya Popov proporcionó una solución simple para esto: cinse puede desatar coutlanzando otro hechizo mágico además de std::ios_base::sync_with_stdio(false);:

cin.tie(nullptr);

También tenga en cuenta que tal descarga forzada también ocurre cuando se usa en std::endllugar de '\n'producir un final de línea encendido cout. Cambiar la línea de salida a una apariencia cout << num_of_trailing_zeros << endl;más idiomática e inocente de C ++ degradaría el rendimiento de la misma manera.

chqrlie
fuente
2
Probablemente tengas razón sobre la descarga de flujo. Recopilar la salida en a std::ostringstreamy enviarla toda una vez al final reduce el tiempo a la paridad con la versión C.
Blastfurnace
2
@ DavidC.Rankin: Aventuré una conjetura (cout se enrojece al leer cin), ideé una forma de probarlo, AlexLop lo implementó y brinda evidencia convincente, pero Blastfurnace ideó una forma diferente de demostrar mi punto y sus pruebas. dar pruebas igualmente convincentes. Lo tomo como prueba, pero por supuesto no es una prueba completamente formal, mirando el código fuente de C ++ podría.
chqrlie
2
Intenté usar ostringstreampara la salida y me dio Time 0.02 QED :). Respecto al fact_num >= UINT_MAX / 5, ¡BUEN punto!
Alex Lop.
1
¡Recopilar todas las entradas en ay vectorluego procesar los cálculos (sin ostringstream) da el mismo resultado! Tiempo 0.02. Combinar ambos vectory ostringstreamno lo mejora más. Mismo tiempo 0.02
Alex Lop.
2
Una solución más simple que funciona incluso si sizeof(int) == sizeof(long long)es la siguiente: agregue una prueba en el cuerpo del bucle después num_of_trailing_zeros += fact_num/fives;para verificar si fivesha alcanzado su máximo:if (fives > UINT_MAX / 5) break;
chqrlie
44

Otro truco para hacer iostreams más rápido cuando usas ambos ciny coutes llamar

cin.tie(nullptr);

De forma predeterminada, cuando ingresa algo de cin, se vacía cout. Puede dañar significativamente el rendimiento si realiza entradas y salidas intercaladas. Esto se hace para los usos de la interfaz de línea de comandos, donde muestra un mensaje y luego espera los datos:

std::string name;
cout << "Enter your name:";
cin >> name;

En este caso, desea asegurarse de que el mensaje se muestre realmente antes de comenzar a esperar la entrada. Con la línea de arriba rompes ese lazo ciny te vuelves coutindependiente.

Desde C ++ 11, una forma más de lograr un mejor rendimiento con iostreams es usar std::getlinejunto con std::stoi, así:

std::string line;
for (int i = 0; i < n && std::getline(std::cin, line); ++i)
{
    int x = std::stoi(line);
}

De esta manera puede acercarse al estilo C en rendimiento, o incluso superarlo scanf. El uso de getchary especialmente getchar_unlockedjunto con el análisis escrito a mano todavía proporciona un mejor rendimiento.

PD. He escrito una publicación comparando varias formas de ingresar números en C ++, útil para jueces en línea, pero solo está en ruso, lo siento. Sin embargo, las muestras de código y la mesa final deberían ser comprensibles.

Ilya Popov
fuente
1
Gracias por la explicación y +1 por la solución, pero su alternativa propuesta con std::readliney std::stoino es funcionalmente equivalente al código de OP. Ambos cin >> x;y scanf("%f", &x);aceptan el espacio en blanco de la hormiga como separador, puede haber varios números en la misma línea.
chqrlie
27

El problema es que, citando cppreference :

cualquier entrada desde std :: cin, salida a std :: cerr o la terminación del programa fuerza una llamada a std :: cout.flush ()

Esto es fácil de probar: si reemplaza

cin >> fact_num;

con

scanf("%d", &fact_num);

y lo mismo para, cin >> num_of_inputspero mantenga cout, obtendrá prácticamente el mismo rendimiento en su versión C ++ (o, mejor dicho, versión IOStream) que en C uno:

ingrese la descripción de la imagen aquí

Lo mismo sucede si mantiene cinpero reemplaza

cout << num_of_trailing_zeros << "\n";

con

printf("%d", num_of_trailing_zeros);
printf("%s","\n");

Una solución simple es desatar couty cincomo lo menciona Ilya Popov:

cin.tie(nullptr);

Las implementaciones de bibliotecas estándar pueden omitir la llamada a flush en ciertos casos, pero no siempre. Aquí hay una cita de C ++ 14 27.7.2.1.3 (gracias a chqrlie):

Clase basic_istream :: sentry: Primero, si is.tie () no es un puntero nulo, la función llama a is.tie () -> flush () para sincronizar la secuencia de salida con cualquier flujo C externo asociado. Excepto que esta llamada se puede suprimir si el área de colocación de is.tie () está vacía. Además, se permite una implementación para diferir la llamada a flush hasta que se produzca una llamada a is.rdbuf () -> underflow (). Si no se produce dicha llamada antes de que se destruya el objeto centinela, la llamada a flush puede eliminarse por completo.

vitaut
fuente
Gracias por la explicación. Aún citando C ++ 14 27.7.2.1.3: Class basic_istream :: sentry : Primero, si is.tie()no es un puntero nulo, la función llama is.tie()->flush()para sincronizar la secuencia de salida con cualquier flujo C externo asociado. Excepto que esta llamada se puede suprimir si el área de venta de is.tie()está vacía. Además, se permite una implementación para aplazar la llamada a flush hasta que se is.rdbuf()->underflow()produzca una llamada de . Si no se produce dicha llamada antes de que se destruya el objeto centinela, la llamada a flush puede eliminarse por completo.
chqrlie
Como es habitual con C ++, las cosas son más complejas de lo que parecen. La biblioteca C ++ del OP no es tan eficiente como lo permite el Estándar.
chqrlie
Gracias por el enlace cppreference. Sin embargo, no me gusta la "respuesta incorrecta" en la pantalla de impresión ☺
Alex Lop.
@AlexLop. Vaya, se solucionó el problema de "respuesta incorrecta" =). Olvidé actualizar el otro cin (aunque esto no afecta el tiempo).
vitaut
@chqrlie Correcto, pero incluso en el caso de subdesbordamiento, es probable que el rendimiento sea peor en comparación con la solución stdio. Gracias por la ref. Estándar.
vitaut