Consideremos los siguientes ejemplos de hello world en C y C ++:
#include <stdio.h>
int main()
{
printf("Hello world\n");
return 0;
}
#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?
eat
asociado con C ++. Creo que quieres decir: "¿Pagas solo por lo que usas "?eat
es más ambiguo y debe evitarse.Respuestas:
Lo que está pagando es llamar a una biblioteca pesada (no tan pesada como imprimir en la consola). Inicializas un
ostream
objeto. Hay algo de almacenamiento oculto. Entonces, llamasstd::endl
que no es sinónimo de\n
. Laiostream
biblioteca 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:
Inicializando un objeto ostream + cout
Llamar
cout
nuevamente para imprimir una nueva línea y vaciarInicialización de almacenamiento estático:
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.
fuente
cout; printf; cout
escrituras estén en orden (ya que tienen sus propios buffers). El segundo se desincronizarácout
ycin
, posiblemente, primero le pedirácout; cin
información al usuario. El vaciado lo forzará a sincronizarse solo cuando realmente lo necesite.std::cout
Es más poderoso y complicado queprintf
. Admite elementos como configuraciones regionales, indicadores de formato con estado y más.Si no los necesita, use
std::printf
ostd::puts
, están disponibles en<cstdio>
.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).fuente
No estás comparando C y C ++. Está comparando
printf
ystd::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:
main.cpp:
fuente
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:
"Hello world\n"
C ++:
"Hello world"
astd::cout
std::endl
manipulador astd::cout
Aparentemente, su código C ++ está haciendo el doble de trabajo. Para una comparación justa, debemos combinar esto:
... y de repente su código de ensamblaje se
main
parece mucho a los de C:De hecho, podemos comparar el código C y C ++ línea por línea, y hay muy pocas diferencias :
La única diferencia real es que en C ++ llamamos
operator <<
con dos argumentos (std::cout
y la cadena). Podríamos eliminar incluso esa ligera diferencia usando un equivalente equivalente C más cercanofprintf
, 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
main
función, ya que todo el trabajo pesado ocurre detrás de escena.fuente
_start
pero 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 ++.std::cout
y, 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 enstd::cout
. Debe deshabilitar explícitamente la sincronización con stdio si desea que la biblioteca iostream use sus propios mecanismos de almacenamiento en búferstd::cout
.printf
no es necesario vaciar las corrientes aquí. De hecho, en un caso de uso común (salida redirigida a un archivo), generalmente encontrará que laprintf
declaración no funciona . Solo cuando la salida tiene un buffer de línea o un buffer, elprintf
disparador se activará.Así de simple. Usted paga por
std::cout
. "Pagas solo por lo que comes" no significa "siempre obtienes los mejores precios". Claro,printf
es más barato. Se puede argumentar questd::cout
es 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 usasprintf
, usasstd::cout
, así que pagas por usarstd::cout
. No pagas por usarloprintf
.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
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.
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?
fuente
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!
fuente
std::cout
las partes internas, que no son visibles en la lista de la asamblea.puts
, que se ve idéntica a una llamada aprintf
si solo pasa una cadena de formato único y cero argumentos adicionales. (excepto que también habrá unxor %eax,%eax
porque 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ónprintf
deputs
algo 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.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
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ó:
init()
método). Por lo general, la asignación de memoria ocurre debajo del capó como lo primero en este paso.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:
std::ios_base
clase se inicializa, que es la clase base para todo lo relacionado con E / S.std::cout
objeto se inicializa.std::__ostream_insert
, que (como ya lo descubrió por el nombre) es un método destd::cout
(básicamente el<<
operador) que agrega una cadena a la secuencia.cout::endl
también se pasa astd::__ostream_insert
.__std_dso_handle
se pasa a__cxa_atexit
, que es una función global que se encarga de "limpiar" antes de salir del programa.__std_dso_handle
esta 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:
puts
través deledi
registro.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
printf
funció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í:
Lo que básicamente resulta en invocar la
write
llamada al sistema seguida por laexit
llamada 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 :
En este caso específico, sí . 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 :
C ++ :
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
malloc
yfree
có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.fuente
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
printf
peroputs
, 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::ostream
funciona 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
fuente
std::cout
arroja excepciones también?std::cout
es unstd::basic_ostream
y 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ó.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.Sobrecarga del operador
<iostream>
es el elemento secundario de cómo no utilizar la sobrecarga del operador:Actuación
std::cout
es varias veces más lentoprintf()
. 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 def
. Si lo usa<iostream>
en una máquina multinúcleo, es probable que vea algo más.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.fuente
std::cout
Es varias veces más lentoprintf()
": 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 parprintf
. 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).printf
y secout
reduce. Por cierto, hay toneladas de puntos de referencia en este mismo sitio.Además de lo que han dicho todas las otras respuestas,
también existe el hecho de que no
std::endl
es lo mismo que'\n'
.Este es un error común desafortunadamente común.
std::endl
no significa "nueva línea",significa "imprimir nueva línea y luego vaciar la secuencia ". Flushing no es barato!
Ignorando completamente las diferencias entre
printf
ystd::cout
por un momento, para ser funcionalmente equivalente a su ejemplo de C, su ejemplo de C ++ debería verse así:Y aquí hay un ejemplo de cómo deberían ser sus ejemplos si incluye el enjuague.
C
C ++
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.
fuente
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.std::endl
para generar nuevas líneas.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.printf
descarga automáticamente al encontrar un personaje de nueva línea?Si bien las respuestas técnicas existentes son correctas, creo que la pregunta en última instancia se deriva de esta idea errónea:
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.
fuente
<cstdio>
y no incluir<iostream>
, al igual que con cómo puede compilar-fno-exceptions -fno-rtti -fno-unwind-tables -fno-asynchronous-unwind-tables
.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.
fuente
std::basic_*stream
hacia 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.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:
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.
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.
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.
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.
fuente