En C ++, ¿estoy pagando por lo que no estoy comiendo?

170

Consideremos los siguientes ejemplos de hello world en C y C ++:

main.c

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

main.cpp

#include <iostream>

int main()
{
    std::cout<<"Hello world"<<std::endl;
    return 0;
}

Cuando los compilo en Godbolt para ensamblar, el tamaño del código C es de solo 9 líneas ( gcc -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        xor     eax, eax
        add     rsp, 8
        ret

Pero el tamaño del código C ++ es de 22 líneas ( g++ -O3):

.LC0:
        .string "Hello world"
main:
        sub     rsp, 8
        mov     edx, 11
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
        xor     eax, eax
        add     rsp, 8
        ret
_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

... que es mucho más grande.

Es famoso que en C ++ pagas por lo que comes. Entonces, en este caso, ¿qué estoy pagando?

Saher
fuente
3
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Samuel Liew
26
Nunca escuché el término eatasociado con C ++. Creo que quieres decir: "¿Pagas solo por lo que usas "?
Giacomo Alzetta
77
@GiacomoAlzetta, ... es un coloquialismo, aplicando el concepto de un buffet de todo lo que puedas comer. Usar el término más preciso es ciertamente preferible con una audiencia global, pero como hablante nativo de inglés americano, el título tiene sentido para mí.
Charles Duffy
55
@ trolley813 Las pérdidas de memoria no tienen nada que ver con la cita y la pregunta de OP. El punto de "Paga solo por lo que usa" / "No paga por lo que no usa" es decir que no se afecta el rendimiento si no usa una característica / abstracción específica. Las pérdidas de memoria no tienen nada que ver con esto, y esto solo muestra que el término eates más ambiguo y debe evitarse.
Giacomo Alzetta

Respuestas:

60

Lo que está pagando es llamar a una biblioteca pesada (no tan pesada como imprimir en la consola). Inicializas un ostreamobjeto. Hay algo de almacenamiento oculto. Entonces, llamas std::endlque no es sinónimo de \n. La iostreambiblioteca lo ayuda a ajustar muchas configuraciones y poner la carga sobre el procesador en lugar del programador. Esto es lo que estás pagando.

Revisemos el código:

.LC0:
        .string "Hello world"
main:

Inicializando un objeto ostream + cout

    sub     rsp, 8
    mov     edx, 11
    mov     esi, OFFSET FLAT:.LC0
    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)

Llamar coutnuevamente para imprimir una nueva línea y vaciar

    mov     edi, OFFSET FLAT:_ZSt4cout
    call    std::basic_ostream<char, std::char_traits<char> >& std::endl<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&)
    xor     eax, eax
    add     rsp, 8
    ret

Inicialización de almacenamiento estático:

_GLOBAL__sub_I_main:
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

Además, es esencial distinguir entre el idioma y la biblioteca.

Por cierto, esto es solo una parte de la historia. No sabe lo que está escrito en las funciones que está llamando.

Arash
fuente
55
Como nota adicional, las pruebas exhaustivas mostrarán que anteponer un programa C ++ con "ios_base :: sync_with_stdio (false);" y "cin.tie (NULL)"; hará que cout sea más rápido que printf (Printf tiene formato de sobrecarga de cadena) El primero elimina la sobrecarga de asegurarse de que las cout; printf; coutescrituras estén en orden (ya que tienen sus propios buffers). El segundo se desincronizará couty cin, posiblemente, primero le pedirá cout; cininformación al usuario. El vaciado lo forzará a sincronizarse solo cuando realmente lo necesite.
Nicholas Pipitone
Hola Nicholas, muchas gracias por agregar estas notas útiles.
Arash
"es esencial distinguir entre el idioma y la biblioteca": Bueno, sí, pero la biblioteca estándar que viene con un idioma es la única disponible en todas partes, por lo que es la que se usa en todas partes (y sí, la biblioteca estándar C es parte de la especificación C ++, por lo que puede usarse cuando se desee). En cuanto a "No sabe lo que está escrito en las funciones que está llamando": puede enlazar estáticamente si realmente quiere saber, y de hecho el código de llamada que examina probablemente sea irrelevante.
Peter - Restablece a Monica el
211

Entonces, en este caso, ¿qué estoy pagando?

std::coutEs más poderoso y complicado que printf. Admite elementos como configuraciones regionales, indicadores de formato con estado y más.

Si no los necesita, use std::printfo std::puts, están disponibles en <cstdio>.


Es famoso que en C ++ pagas por lo que comes.

También quiero dejar en claro que C ++ ! = La biblioteca estándar de C ++. Se supone que la Biblioteca estándar es de uso general y "lo suficientemente rápida", pero a menudo será más lenta que una implementación especializada de lo que necesita.

Por otro lado, el lenguaje C ++ se esfuerza por hacer posible escribir código sin pagar costos ocultos adicionales innecesarios (por ejemplo, suscripción voluntaria virtual, no recolección de basura).

Vittorio Romeo
fuente
44
+1 por decir que se supone que The Standard Library es de uso general y "lo suficientemente rápido", pero a menudo será más lenta que una implementación especializada de lo que necesita. Muchos parecen usar alegremente componentes STL sin tener en cuenta las implicaciones de rendimiento en comparación con la suya propia.
Craig Estey
77
@Craig OTOH muchas partes de la biblioteca estándar suelen ser más rápidas y más correctas de lo que normalmente se podría producir.
Peter - Restablece a Monica el
2
@ PeterA.Schneider OTOH, cuando la versión STL es 20x-30x más lenta, rodar la tuya es algo bueno. Vea mi respuesta aquí: codereview.stackexchange.com/questions/191747/… Allí, otros también sugirieron [al menos un parcial] rodar el suyo.
Craig Estey
1
@CraigEstey Un vector es (aparte de la asignación dinámica inicial que puede ser significativa, dependiendo de cuánto trabajo se haga eventualmente con una instancia dada) no menos eficiente que una matriz C; Está diseñado para no ser. Se debe tener cuidado de no copiarlo, reservar suficiente espacio inicialmente, etc., pero todo eso también se debe hacer con una matriz y de manera menos segura. Con respecto a su ejemplo vinculado: Sí, un vector de vectores (a menos que esté optimizado) incurrirá en una indirección adicional en comparación con una matriz 2D, pero supongo que la eficiencia 20x no está enraizada allí sino en el algoritmo.
Peter - Restablece a Monica el
174

No estás comparando C y C ++. Está comparando printfy std::cout, que son capaces de diferentes cosas (configuraciones regionales, formato de estado, etc.).

Intente usar el siguiente código para comparar. Godbolt genera el mismo ensamblaje para ambos archivos (probado con gcc 8.2, -O3).

C Principal:

#include <stdio.h>

int main()
{
    int arr[6] = {1, 2, 3, 4, 5, 6};
    for (int i = 0; i < 6; ++i)
    {
        printf("%d\n", arr[i]);
    }
    return 0;
}

main.cpp:

#include <array>
#include <cstdio>

int main()
{
    std::array<int, 6> arr {1, 2, 3, 4, 5, 6};
    for (auto x : arr)
    {
        std::printf("%d\n", x);
    }
}
pschill
fuente
Aplausos por mostrar código equivalente y explicar el motivo.
HackSlash
134

Sus listados están comparando manzanas y naranjas, pero no por la razón implícita en la mayoría de las otras respuestas.

Veamos qué hace realmente tu código:

C:

  • imprime una sola cadena, "Hello world\n"

C ++:

  • transmitir la cadena "Hello world"astd::cout
  • transmitir el std::endlmanipulador astd::cout

Aparentemente, su código C ++ está haciendo el doble de trabajo. Para una comparación justa, debemos combinar esto:

#include <iostream>

int main()
{
    std::cout<<"Hello world\n";
    return 0;
}

... y de repente su código de ensamblaje se mainparece mucho a los de C:

main:
        sub     rsp, 8
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        xor     eax, eax
        add     rsp, 8
        ret

De hecho, podemos comparar el código C y C ++ línea por línea, y hay muy pocas diferencias :

sub     rsp, 8                      sub     rsp, 8
mov     edi, OFFSET FLAT:.LC0   |   mov     esi, OFFSET FLAT:.LC0
                                >   mov     edi, OFFSET FLAT:_ZSt4cout
call    puts                    |   call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
xor     eax, eax                    xor     eax, eax
add     rsp, 8                      add     rsp, 8
ret                                 ret

La única diferencia real es que en C ++ llamamos operator <<con dos argumentos ( std::couty la cadena). Podríamos eliminar incluso esa ligera diferencia usando un equivalente equivalente C más cercano fprintf, que también tiene un primer argumento que especifica la secuencia.

Esto deja el código de ensamblaje para _GLOBAL__sub_I_main, que se genera para C ++ pero no para C. Esta es la única sobrecarga verdadera que es visible en esta lista de ensamblados (hay más, sobrecarga invisible para ambos idiomas, por supuesto). Este código realiza una configuración única de algunas funciones de biblioteca estándar de C ++ al inicio del programa C ++.

Pero, como se explica en otras respuestas, la diferencia relevante entre estos dos programas no se encontrará en el resultado de ensamblaje de la mainfunción, ya que todo el trabajo pesado ocurre detrás de escena.

Konrad Rudolph
fuente
21
Por cierto, también se debe configurar el tiempo de ejecución de C , y esto sucede en una función llamada, _startpero su código es parte de la biblioteca de tiempo de ejecución de C. En cualquier caso, esto sucede tanto para C como para C ++.
Konrad Rudolph
2
@Deduplicator: en realidad, de manera predeterminada, la biblioteca iostream no realiza ningún almacenamiento en búfer std::couty, en su lugar, pasa E / S a la implementación stdio (que utiliza sus propios mecanismos de almacenamiento en búfer). En particular, cuando se conecta a (lo que se sabe que es) un terminal interactivo, de forma predeterminada, nunca verá una salida con búfer completo al escribir en std::cout. Debe deshabilitar explícitamente la sincronización con stdio si desea que la biblioteca iostream use sus propios mecanismos de almacenamiento en búfer std::cout.
66
@KonradRudolph: En realidad, printfno es necesario vaciar las corrientes aquí. De hecho, en un caso de uso común (salida redirigida a un archivo), generalmente encontrará que la printfdeclaración no funciona . Solo cuando la salida tiene un buffer de línea o un buffer, el printfdisparador se activará.
2
@PeterCordes: Correcto, no puede bloquear con búferes de salida sin vaciar, pero puede encontrarse con la sorpresa de que el programa ha aceptado su entrada y marchó sin mostrar la salida esperada. Lo sé porque he tenido la oportunidad de depurar una "Ayuda, mi programa se cuelga durante la entrada, ¡pero no puedo entender por qué!" eso le había dado a otro desarrollador ajustes durante unos días.
2
@PeterCordes: El argumento que hago es "escribe lo que quieres decir": las nuevas líneas son apropiadas cuando quieres que la salida esté eventualmente disponible, y endl es apropiado cuando quieres que la salida esté disponible de inmediato.
53

Es famoso que en C ++ pagas por lo que comes. Entonces, en este caso, ¿qué estoy pagando?

Así de simple. Usted paga por std::cout. "Pagas solo por lo que comes" no significa "siempre obtienes los mejores precios". Claro, printfes más barato. Se puede argumentar que std::coutes más seguro y más versátil, por lo tanto, su mayor costo está justificado (cuesta más, pero proporciona más valor), pero eso no pasa nada. No usas printf, usas std::cout, así que pagas por usar std::cout. No pagas por usarlo printf.

Un buen ejemplo son las funciones virtuales. Las funciones virtuales tienen algunos costos de tiempo de ejecución y requisitos de espacio, pero solo si realmente los usa. Si no usa funciones virtuales, no paga nada.

Algunas observaciones

  1. Incluso si el código C ++ se evalúa como más instrucciones de ensamblaje, sigue siendo un puñado de instrucciones, y cualquier sobrecarga de rendimiento probablemente se vea reducida por las operaciones de E / S reales.

  2. En realidad, a veces es incluso mejor que "en C ++ pagas por lo que comes". Por ejemplo, el compilador puede deducir que la llamada a la función virtual no es necesaria en algunas circunstancias y transformarla en una llamada no virtual. Eso significa que puede obtener funciones virtuales de forma gratuita . ¿No es genial?

el.pescado
fuente
66
No obtienes funciones virtuales de forma gratuita. Todavía tiene que pagar el costo de escribirlos primero y luego depurar la transformación del compilador de su código cuando no coincide con su idea de lo que se suponía que debía hacer.
alephzero
2
@alephzero No estoy seguro de que sea particularmente relevante comparar los costos de desarrollo con los costos de rendimiento.
Una gran oportunidad para un juego de palabras desperdiciado ... Podría haber usado la palabra 'calorías' en lugar de 'precio'. A partir de eso, se podría decir que C ++ es más gordo que C. O al menos ... el código específico en cuestión (soy parcial contra C ++ a favor de C, así que no puedo ir más allá). Pobre de mí. @Bilkokuya Puede que no sea relevante en todos los casos, pero ciertamente es algo que uno no debe ignorar. Por lo tanto, es relevante en general.
Pryftan
46

La "lista de ensamblaje para printf" NO es para printf, sino para Putts (¿tipo de optimización del compilador?); printf es mucho más complejo que pone ... ¡no lo olvides!

Álvaro Gustavo López
fuente
13
Hasta ahora, esta es la mejor respuesta, ya que todos los demás se quedan atrapados en un arenque rojo sobre std::coutlas partes internas, que no son visibles en la lista de la asamblea.
Konrad Rudolph
12
La lista de ensamblaje es para una llamada a puts , que se ve idéntica a una llamada a printfsi solo pasa una cadena de formato único y cero argumentos adicionales. (excepto que también habrá un xor %eax,%eaxporque estamos pasando cero argumentos FP en registros a una función variada). Ninguno de estos es la implementación, simplemente pasando un puntero a una cadena a la función de biblioteca. Pero sí, la optimización printfde putsalgo gcc hace para formatos que sólo tienen "%s", o cuando no hay conversiones, y los extremos de cuerda con un salto de línea.
Peter Cordes
45

Veo algunas respuestas válidas aquí, pero voy a profundizar un poco más en los detalles.

Vaya al resumen a continuación para obtener la respuesta a su pregunta principal si no desea revisar todo este muro de texto.


Abstracción

Entonces, en este caso, ¿qué estoy pagando?

Estás pagando por la abstracción . Ser capaz de escribir código más simple y amigable para los humanos tiene un costo. En C ++, que es un lenguaje orientado a objetos, casi todo es un objeto. Cuando usas cualquier objeto, tres cosas principales siempre sucederán debajo del capó:

  1. Creación de objetos, básicamente asignación de memoria para el objeto en sí y sus datos.
  2. Inicialización de objetos (generalmente a través de algún init()método). Por lo general, la asignación de memoria ocurre debajo del capó como lo primero en este paso.
  3. Destrucción de objetos (no siempre).

No lo ve en el código, pero cada vez que usa un objeto, las tres cosas anteriores deben suceder de alguna manera. Si hicieras todo manualmente, el código obviamente sería mucho más largo.

Ahora, la abstracción se puede hacer de manera eficiente sin agregar gastos generales: tanto los compiladores como los programadores pueden utilizar la inlínea de métodos y otras técnicas para eliminar los gastos generales de la abstracción, pero este no es su caso.

¿Qué está pasando realmente en C ++?

Aquí está, desglosado:

  1. La std::ios_baseclase se inicializa, que es la clase base para todo lo relacionado con E / S.
  2. El std::coutobjeto se inicializa.
  3. Su cadena se carga y se pasa a std::__ostream_insert, que (como ya lo descubrió por el nombre) es un método de std::cout(básicamente el <<operador) que agrega una cadena a la secuencia.
  4. cout::endltambién se pasa a std::__ostream_insert.
  5. __std_dso_handlese pasa a __cxa_atexit, que es una función global que se encarga de "limpiar" antes de salir del programa. __std_dso_handleesta función llama a sí misma para desasignar y destruir los objetos globales restantes.

Entonces, ¿usar C == sin pagar nada?

En el código C, están ocurriendo muy pocos pasos:

  1. Su cadena se carga y se pasa a putstravés del ediregistro.
  2. puts se llama.

No hay objetos en ningún lado, por lo tanto, no es necesario inicializar / destruir nada.

Sin embargo, esto no quiere decir que usted no está "pagando" por nada en C . Todavía está pagando por la abstracción, y también por la inicialización de la biblioteca estándar de C y la resolución dinámica de la printffunción (o, en realidadputs , que está optimizada por el compilador, ya que no necesita ninguna cadena de formato) todavía sucede bajo el capó.

Si escribieras este programa en ensamblado puro, se vería así:

jmp start

msg db "Hello world\n"

start:
    mov rdi, 1
    mov rsi, offset msg
    mov rdx, 11
    mov rax, 1          ; write
    syscall
    xor rdi, rdi
    mov rax, 60         ; exit
    syscall

Lo que básicamente resulta en invocar la write llamada al sistema seguida por la exitllamada al sistema. Ahora, esto sería lo mínimo para lograr lo mismo.


Para resumir

C es mucho más básico , y solo hace lo mínimo necesario, dejando el control total al usuario, que puede optimizar y personalizar completamente todo lo que quiera. Le dice al procesador que cargue una cadena en un registro y luego llama a una función de biblioteca para usar esa cadena. C ++ por otro lado es mucho más complejo y abstracto . Esto tiene una enorme ventaja al escribir código complicado, y permite un código más fácil de escribir y más amigable para los humanos, pero obviamente tiene un costo. Siempre habrá un inconveniente en el rendimiento en C ++ si se compara con C en casos como este, ya que C ++ ofrece más de lo necesario para realizar tareas tan básicas y, por lo tanto, agrega más sobrecarga .

Respondiendo a tu pregunta principal :

¿Estoy pagando por lo que no estoy comiendo?

En este caso específico, . No está aprovechando nada de lo que C ++ tiene para ofrecer más que C, pero eso es solo porque no hay nada en ese simple código con el que C ++ pueda ayudarlo: es tan simple que realmente no necesita C ++.


¡Ah, y solo una cosa más!

Las ventajas de C ++ pueden no parecer obvias a primera vista, ya que escribió un programa muy simple y pequeño, pero mire un ejemplo un poco más complejo y vea la diferencia (ambos programas hacen exactamente lo mismo):

C :

#include <stdio.h>
#include <stdlib.h>

int cmp(const void *a, const void *b) {
    return *(int*)a - *(int*)b;
}

int main(void) {
    int i, n, *arr;

    printf("How many integers do you want to input? ");
    scanf("%d", &n);

    arr = malloc(sizeof(int) * n);

    for (i = 0; i < n; i++) {
        printf("Index %d: ", i);
        scanf("%d", &arr[i]);
    }

    qsort(arr, n, sizeof(int), cmp)

    puts("Here are your numbers, ordered:");

    for (i = 0; i < n; i++)
        printf("%d\n", arr[i]);

    free(arr);

    return 0;
}

C ++ :

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

int main(void) {
    int n;

    cout << "How many integers do you want to input? ";
    cin >> n;

    vector<int> vec(n);

    for (int i = 0; i < vec.size(); i++) {
        cout << "Index " << i << ": ";
        cin >> vec[i];
    }

    sort(vec.begin(), vec.end());

    cout << "Here are your numbers:" << endl;

    for (int item : vec)
        cout << item << endl;

    return 0;
}

Espero que puedan ver claramente lo que quiero decir aquí. Observe también cómo en C tiene que administrar la memoria en un nivel inferior usando mallocy freecómo debe tener más cuidado con la indexación y los tamaños, y cómo debe ser muy específico al tomar entradas e imprimir.

Marco Bonelli
fuente
27

Hay algunas ideas falsas para comenzar. Primero, el programa C ++ no da como resultado 22 instrucciones, es más como 22,000 de ellas (saqué ese número de mi sombrero, pero está aproximadamente en el estadio). Además, el código C no resultado 9 instrucciones. Esos son solo los que ves.

Lo que hace el código C es, después de hacer muchas cosas que no ves, llama a una función del CRT (que generalmente está presente, pero no necesariamente, como lib compartida), luego no verifica el valor de retorno o el identificador errores y rescata. Dependiendo de la configuración de optimización del compilador y ni siquiera lo llamaría printfpero puts, o algo aún más primitivo.
También podría haber escrito más o menos el mismo programa (a excepción de algunas funciones invisibles de inicio) en C ++, si solo llamara a esa misma función de la misma manera. O, si desea ser súper correcto, esa misma función con el prefijo std::.

El código C ++ correspondiente no es en realidad lo mismo. Si bien todo <iostream>esto es bien conocido por ser un cerdo gordo y feo que agrega una sobrecarga inmensa para programas pequeños (en un programa "real" realmente no se nota tanto), una interpretación un tanto más justa es que hace un horrible muchas cosas que no ves y que simplemente funcionan . Incluyendo, entre otros, el formato mágico de casi cualquier cosa fortuita, incluidos diferentes formatos de números y configuraciones regionales y demás, y almacenamiento en búfer y manejo adecuado de errores. ¿Manejo de errores? Bueno, sí, adivina qué, generar una cadena puede fallar, y a diferencia del programa C, el programa C ++ no ignoraría esto en silencio. Considerando questd::ostreamfunciona bajo el capó, y sin que nadie se dé cuenta, en realidad es bastante ligero. No es que lo esté usando porque odio la sintaxis de transmisión con pasión. Pero aún así, es bastante impresionante si consideras lo que hace.

Pero claro, C ++ en general no es tan eficiente como C puede ser. No puede ser tan eficiente ya que no es lo mismo y no está haciendo lo mismo. Por lo menos, C ++ genera excepciones (y código para generar, manejar o fallar en ellas) y ofrece algunas garantías que C no da. Entonces, claro, un programa C ++ necesariamente necesita ser un poco más grande. En general, sin embargo, esto no importa de ninguna manera. Por el contrario, para programas reales , rara vez he encontrado que C ++ funciona mejor porque, por una razón u otra, parece prestar para optimizaciones más favorables. No me preguntes por qué en particular, no lo sabría.

Si, en lugar de disparar y olvidar la esperanza de lo mejor, le interesa escribir el código C que es correcto (es decir, realmente verifica si hay errores y el programa se comporta correctamente en presencia de errores), entonces la diferencia es marginal, si existe

Damon
fuente
16
Muy buena respuesta, excepto que esta afirmación: "Pero claro, C ++ en general no es tan eficiente como C puede ser" es simplemente errónea. C ++ puede ser tan eficiente como C, y un código de nivel suficientemente alto puede ser más eficiente que un código C equivalente. Sí, C ++ tiene cierta sobrecarga debido a que tiene que manejar excepciones, pero en los compiladores modernos la sobrecarga es insignificante en comparación con las ganancias de rendimiento de mejores abstracciones sin costo.
Konrad Rudolph
Si entendí correctamente, ¿ std::coutarroja excepciones también?
Saher
66
@Saher: Sí, no, tal vez. std::coutes un std::basic_ostreamy que uno puede lanzar, y puede volver a lanzar excepciones que ocurran de otro modo si está configurado para hacerlo o puede tragar excepciones. La cosa es que las cosas pueden fallar, y tanto C ++ como la libra estándar de C ++ se construyen (principalmente) para que las fallas no pasen desapercibidas fácilmente. Esto es una molestia y una bendición (pero, más bendición que molestia). C por otro lado solo te muestra el dedo medio. No verificas un código de retorno, nunca sabes lo que pasó.
Damon
1
@KonradRudolph: Cierto, esto es lo que estaba tratando de señalar con "No he encontrado raramente que C ++ funcione mejor porque, por una razón u otra, parece prestar para optimizaciones más favorables. No me pregunte por qué en particular" . No es inmediatamente obvio por qué, pero no rara vez solo se optimiza mejor. Por cualquier razón. Se podría pensar que es todo lo mismo para el optimizador, pero no lo es.
Damon
22

Estás pagando por un error. En los años 80, cuando los compiladores no eran lo suficientemente buenos como para verificar cadenas de formato, la sobrecarga del operador se consideraba una buena manera de imponer cierta apariencia de seguridad de tipo durante io. Sin embargo, cada una de sus características de banner se implementa mal o está en bancarrota conceptual desde el principio:

<iomanip>

La parte más repugnante del flujo de C ++ io api es la existencia de esta biblioteca de encabezado de formato. Además de ser caprichoso, feo y propenso a errores, combina el formato con la transmisión.

Suponga que desea imprimir una línea con 8 dígitos con relleno hexadecimal sin signo int seguido de un espacio seguido de un doble con 3 decimales. Con <cstdio>, puedes leer una cadena de formato conciso. Con <ostream>, debe guardar el estado anterior, establecer la alineación a la derecha, establecer el carácter de relleno, establecer el ancho de relleno, establecer la base en hexadecimal, generar el número entero, restaurar el estado guardado (de lo contrario, el formato entero contaminará el formato flotante), generará el espacio , establezca la notación como fija, establezca la precisión, genere el doble y la nueva línea, luego restaure el formato anterior.

// <cstdio>
std::printf( "%08x %.3lf\n", ival, fval );

// <ostream> & <iomanip>
std::ios old_fmt {nullptr};
old_fmt.copyfmt (std::cout);
std::cout << std::right << std::setfill('0') << std::setw(8) << std::hex << ival;
std::cout.copyfmt (old_fmt);
std::cout << " " << std::fixed << std::setprecision(3) << fval << "\n";
std::cout.copyfmt (old_fmt);

Sobrecarga del operador

<iostream> es el elemento secundario de cómo no utilizar la sobrecarga del operador:

std::cout << 2 << 3 && 0 << 5;

Actuación

std::cout es varias veces más lento printf() . La featuritis desenfrenada y el despacho virtual hacen mella.

Hilo de seguridad

Ambos <cstdio>y <iostream>son seguros para subprocesos ya que cada llamada de función es atómica. Pero, printf()se hace mucho más por llamada. Si ejecuta el siguiente programa con la <cstdio>opción, verá solo una fila de f. Si lo usa <iostream>en una máquina multinúcleo, es probable que vea algo más.

// g++ -Wall -Wextra -Wpedantic -pthread -std=c++17 cout.test.cpp

#define USE_STREAM 1
#define REPS 50
#define THREADS 10

#include <thread>
#include <vector>

#if USE_STREAM
    #include <iostream>
#else
    #include <cstdio>
#endif

void task()
{
    for ( int i = 0; i < REPS; ++i )
#if USE_STREAM
        std::cout << std::hex << 15 << std::dec;
#else
        std::printf ( "%x", 15);
#endif

}

int main()
{
    auto threads = std::vector<std::thread> {};
    for ( int i = 0; i < THREADS; ++i )
        threads.emplace_back(task);

    for ( auto & t : threads )
        t.join();

#if USE_STREAM
        std::cout << "\n<iostream>\n";
#else
        std::printf ( "\n<cstdio>\n" );
#endif
}

La respuesta a este ejemplo es que la mayoría de las personas ejercen disciplina para nunca escribir en un solo descriptor de archivo desde múltiples hilos de todos modos. Bueno, en ese caso, tendrás que observar que <iostream>, de manera útil, agarrará un candado en todos <<y cada uno >>. Mientras que en <cstdio>, no se bloqueará con tanta frecuencia, e incluso tiene la opción de no bloquear.

<iostream> gasta más cerraduras para lograr un resultado menos consistente.

KevinZ
fuente
2
La mayoría de las implementaciones de printf tienen una característica extremadamente útil para la localización: parámetros numerados. Si necesita producir alguna salida en dos idiomas diferentes (como inglés y francés) y el orden de las palabras es diferente, puede usar el mismo printf con una cadena de formato diferente, y puede imprimir los parámetros en un orden diferente.
gnasher729
2
Ese formato con estado de las transmisiones debe haber dado tantos errores difíciles de encontrar que no sé qué decir. Gran respuesta. Votaría más de una vez si pudiera.
mathreadler
66
" std::coutEs varias veces más lento printf()": esta afirmación se repite en toda la red, pero no ha sido así en mucho tiempo. Las implementaciones modernas de IOstream funcionan a la par printf. Este último también realiza un despacho virtual internamente para lidiar con flujos almacenados temporalmente y E / S localizadas (realizadas por el sistema operativo pero no obstante).
Konrad Rudolph
3
@KevinZ Y eso es genial, pero está evaluando una sola llamada específica, que muestra las fortalezas específicas de fmt (muchos formatos diferentes en una sola cadena). En un uso más típico, la diferencia entre printfy se coutreduce. Por cierto, hay toneladas de puntos de referencia en este mismo sitio.
Konrad Rudolph el
3
@KonradRudolph Eso tampoco es cierto. Los microbenchmarks a menudo subestiman el costo de la hinchazón y la indirección porque no agotan ciertos recursos limitados (ya sea registros, icache, memoria, predictores de ramificación) donde lo hará un programa real. Cuando alude al "uso más típico", básicamente dice que tiene bastante más hinchazón en otros lugares, lo cual está bien, pero fuera de tema. En mi opinión, si no tiene requisitos de rendimiento, no necesita programar en C ++.
KevinZ
18

Además de lo que han dicho todas las otras respuestas,
también existe el hecho de que nostd::endl es lo mismo que '\n'.

Este es un error común desafortunadamente común. std::endlno significa "nueva línea",
significa "imprimir nueva línea y luego vaciar la secuencia ". Flushing no es barato!

Ignorando completamente las diferencias entre printfy std::coutpor un momento, para ser funcionalmente equivalente a su ejemplo de C, su ejemplo de C ++ debería verse así:

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    return 0;
}

Y aquí hay un ejemplo de cómo deberían ser sus ejemplos si incluye el enjuague.

C

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    fflush(stdout);
    return 0;
}

C ++

#include <iostream>

int main()
{
    std::cout << "Hello world\n";
    std::cout << std::flush;
    return 0;
}

Al comparar el código, siempre debe tener cuidado de comparar y comparar las implicaciones de lo que está haciendo su código. A veces, incluso los ejemplos más simples son más complicados de lo que algunas personas creen.

Pharap
fuente
En realidad, usar std::endl es el equivalente funcional a escribir una nueva línea en una secuencia stdio con búfer de línea. stdout, en particular, se requiere que tenga un buffer de línea o un buffer cuando esté conectado a un dispositivo interactivo. Linux, creo, insiste en la opción de buffer de línea.
De hecho, la biblioteca iostream no tiene un modo de almacenamiento en línea ... la forma de lograr el efecto de almacenamiento en línea es precisamente usar std::endlpara generar nuevas líneas.
@Hurkyl Insistir? Entonces, ¿de qué sirve setvbuf(3)? ¿O quiere decir que el valor predeterminado es el almacenamiento intermedio de línea? FYI: Normalmente, todos los archivos están almacenados en bloque. Si una secuencia se refiere a un terminal (como normalmente lo hace stdout), se almacena en línea. El flujo de error estándar stderr siempre está sin búfer por defecto.
Pryftan
¿No se printfdescarga automáticamente al encontrar un personaje de nueva línea?
bool3max
1
@ bool3max Eso solo me diría lo que hace mi entorno, podría ser diferente en otros entornos. Incluso si se comporta igual en todas las implementaciones más populares, eso no significa que haya un caso límite en alguna parte. Es por eso que el estándar es tan importante: el estándar dicta si algo tiene que ser igual para todas las implementaciones o si está permitido variar entre implementaciones.
Pharap
16

Si bien las respuestas técnicas existentes son correctas, creo que la pregunta en última instancia se deriva de esta idea errónea:

Es famoso que en C ++ pagas por lo que comes.

Esto es solo una charla de marketing de la comunidad C ++. (Para ser justos, hay charlas de marketing en cada comunidad de idiomas). No significa nada concreto de lo que pueda confiar seriamente.

Se supone que "Pagas por lo que usas" significa que una función de C ++ solo tiene gastos generales si estás usando esa función. Pero la definición de "una característica" no es infinitamente granular. A menudo terminará activando características que tienen múltiples aspectos, y aunque solo necesita un subconjunto de esos aspectos, a menudo no es práctico o posible que la implementación incorpore la característica parcialmente.

En general, muchos (aunque posiblemente no todos) los idiomas se esfuerzan por ser eficientes, con diversos grados de éxito. C ++ está en algún lugar de la escala, pero no hay nada especial o mágico en su diseño que le permita tener un éxito perfecto en este objetivo.

Theodoros Chatzigiannakis
fuente
1
Solo puedo pensar en dos cosas en las que paga por algo que no usa: excepciones y RTTI. Y no creo que sea una charla de marketing; C ++ es básicamente una C más poderosa, que también es "no pague por lo que usa".
Rakete1111
2
@ Rakete1111 Hace tiempo que se estableció que si no se lanzan excepciones, no cuestan. Si su programa se está ejecutando constantemente, debe ser rediseñado. Si la condición de falla está fuera de su control, debe verificar la condición con una verificación de sanidad de retorno de bool, antes de llamar al método que se basa en que la condición es falsa.
schulmaster
1
@schulmaster: las excepciones pueden imponer restricciones de diseño cuando el código escrito en C ++ necesita interactuar con el código escrito en otros lenguajes, ya que las transferencias de control no locales solo pueden funcionar sin problemas entre los módulos si los módulos saben cómo coordinarse entre sí.
supercat
1
(aunque podría decirse que no todos) los idiomas se esfuerzan por ser eficientes . Definitivamente no todos: los lenguajes de programación esotéricos se esfuerzan por ser novedosos / interesantes, no eficientes. esolangs.org . Algunos de ellos, como BrainFuck, son famosos por su ineficiencia. O, por ejemplo, el lenguaje de programación de Shakespeare, tamaño mínimo de 227 bytes (codegolf) para imprimir todos los enteros . Fuera de los lenguajes destinados al uso de producción, la mayoría apunta a la eficiencia, pero algunos (como bash) apuntan principalmente por conveniencia y se sabe que son lentos.
Peter Cordes
2
Bueno, es marketing pero es casi completamente cierto. Puede seguir <cstdio>y no incluir <iostream>, al igual que con cómo puede compilar -fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables.
KevinZ
11

Las funciones de entrada / salida en C ++ están escritas con elegancia y están diseñadas para que sean fáciles de usar. En muchos aspectos, son un escaparate de las características orientadas a objetos en C ++.

Pero, a cambio, renuncia a un poco de rendimiento, pero eso es insignificante en comparación con el tiempo que le toma a su sistema operativo manejar las funciones en un nivel inferior.

Siempre puede recurrir a las funciones de estilo C, ya que son parte del estándar C ++, o tal vez renunciar a la portabilidad por completo y utilizar llamadas directas a su sistema operativo.

Betsabé
fuente
23
"Las funciones de Entrada / Salida en C ++ son monstruos horribles que luchan por ocultar su naturaleza Cthuliana detrás de una delgada capa de utilidad. En muchos aspectos, son un escaparate de cómo no diseñar un código C ++ moderno". Probablemente sería más preciso.
user673679
3
@ user673679: Muy cierto. El gran problema con las transmisiones de E / S de C ++ es lo que hay debajo: realmente hay mucha complejidad, y cualquiera que haya tratado con ellas (me refiero a esto std::basic_*streamhacia abajo) conoce los dolores de cabeza entrantes. Fueron diseñados para ser ampliamente generales y extendidos a través de la herencia; pero nadie finalmente hizo eso, debido a su complejidad (hay literalmente libros escritos en iostreams), tanto que nacieron nuevas bibliotecas solo para eso (por ejemplo, boost, ICU, etc.). Dudo que alguna vez dejemos de pagar por este error.
edmz
1

Como ha visto en otras respuestas, paga cuando vincula en bibliotecas generales y llama a constructores complejos. No hay una pregunta particular aquí, más una queja. Señalaré algunos aspectos del mundo real:

  1. Barne tenía un principio de diseño central para nunca permitir que la eficiencia sea una razón para permanecer en C en lugar de C ++. Dicho esto, uno debe tener cuidado para obtener estas eficiencias, y hay eficiencias ocasionales que siempre funcionaron pero que no estaban 'técnicamente' dentro de la especificación C. Por ejemplo, el diseño de los campos de bits no se especificó realmente.

  2. Intenta mirar a través de ostream. ¡Dios mío, está hinchado! No me sorprendería encontrar un simulador de vuelo allí. Incluso el printf () de stdlib usualmente corre alrededor de 50K. Estos no son programadores perezosos: la mitad del tamaño de printf tenía que ver con argumentos de precisión indirectos que la mayoría de la gente nunca usa. Casi todas las bibliotecas de procesadores realmente restringidos crean su propio código de salida en lugar de printf.

  3. El aumento de tamaño suele proporcionar una experiencia más contenida y flexible. Como analogía, una máquina expendedora venderá una taza de sustancia similar al café por unas pocas monedas y toda la transacción dura menos de un minuto. Llegar a un buen restaurante implica una mesa, sentarse, ordenar, esperar, obtener una buena taza, recibir una factura, pagar en su elección de formularios, agregar una propina y desearle un buen día al salir. Es una experiencia diferente, y más conveniente si te encuentras con amigos para una comida compleja.

  4. La gente todavía escribe ANSI C, aunque rara vez K&R C. Mi experiencia es que siempre lo compilamos con un compilador de C ++ usando algunos ajustes de configuración para limitar lo que se arrastra. Hay buenos argumentos para otros idiomas: Go elimina la sobrecarga polimórfica y el preprocesador loco. ; Ha habido algunos buenos argumentos para un diseño de memoria y empaquetado de campo más inteligente. En mi humilde opinión, creo que cualquier diseño de lenguaje debe comenzar con una lista de objetivos, al igual que el Zen de Python .

Ha sido una discusión divertida. Usted pregunta por qué no puede tener bibliotecas mágicamente pequeñas, simples, elegantes, completas y flexibles.

No hay respuesta. No habrá una respuesta. Esa es la respuesta.

Charles Merriam
fuente