style coding code c++ c performance coding-style

coding - c++ naming conventions



¿C++ estilo vs. rendimiento? (10)

  1. Aconsejaría no atoi , y atol como regla general, pero no solo por motivos de estilo. Hacen esencialmente imposible detectar errores de entrada. Mientras que una stringstream puede hacer el mismo trabajo, strtol (por ejemplo, es lo que normalmente recomiendo como reemplazo directo).
  2. No estoy seguro de quién está dando ese consejo. Use punteros inteligentes cuando son útiles, pero cuando no lo son, no hay nada de malo en usar un puntero en bruto.
  3. Realmente no tengo idea de quién cree que "no es una buena práctica" usar operadores bitwise en C ++. A menos que hubiera algunas condiciones específicas adjuntas a ese consejo, yo diría que simplemente estaba mal.
  4. Este depende en gran medida de dónde dibuje la línea entre una entrada excepcional, y (por ejemplo) una entrada que se espera, pero no se puede usar. En términos generales, si está aceptando comentarios directamente del usuario, no puede (y no debe) clasificar cualquier cosa como verdaderamente excepcional. El principal punto bueno de las excepciones (incluso en una situación como esta) es garantizar que los errores no se ignoren. OTOH, no creo que ese sea siempre el único criterio, así que no puedes decir que es la forma correcta de manejar cada situación.

En general, me parece que has recibido un consejo que es dogmático al punto de ignorar la realidad. Probablemente sea mejor ignorarlo o, al menos, verlo como una posición bastante extrema sobre cómo se podría escribir C ++, no necesariamente cómo debería escribirse siempre (o siempre, necesariamente).

Estilo de C ++ frente a rendimiento: ¿está utilizando cosas de estilo C, que son más rápidos que algunos equivalentes de C ++, esa mala práctica? Por ejemplo:

  • ¡No use atoi() , itoa() , atol() , etc.! Use std::stringstream <- probablemente a veces sea mejor, pero siempre? ¿Qué tiene de malo usar las funciones de C? Sí, estilo C, no C ++, pero ¿qué? Esto es C ++, estamos buscando rendimiento todo el tiempo ..

  • Nunca use punteros en bruto, use punteros inteligentes en su lugar. De acuerdo, son muy útiles, todo el mundo lo sabe, lo sé, lo uso todo el tiempo y sé cuán mejor son esos punteros en bruto, pero a veces es completamente seguro utilizar punteros en bruto ... ¿Por qué no? "¿No es el estilo C ++? <- ¿Es esto suficiente?

  • No use operaciones bitwise - ¿demasiado estilo C? ¿Qué? ¿Por qué no, cuando estás seguro de lo que estás haciendo? Por ejemplo, no haga un intercambio de variables a nivel de bits ( a ^= b; b ^= a; a ^= b; ) - use el intercambio estándar de 3 pasos. No uses el turno de la izquierda para multiplicar por dos. Etc, etc. (OK, eso no es estilo C ++ vs. estilo C, pero aún así "no es una buena práctica")

  • Y, finalmente, el más costoso: "No use enum-s para devolver códigos, es demasiado estilo C, use excepciones para errores diferentes"? ¿Por qué? De acuerdo, cuando hablamos de manejo de errores en niveles profundos, de acuerdo, pero ¿por qué siempre? ¿Qué hay de malo en esto, por ejemplo, cuando hablamos de una función, que devuelve diferentes códigos de error y cuando el manejo de errores se implementará solo en la función, que llama al primero ? Quiero decir, no es necesario pasar los códigos de error en un nivel superior. Las excepciones son bastante lentas y son excepciones para situaciones excepcionales, no para ... la belleza.

  • etc., etc., etc.

Bien, sé que un buen estilo de codificación es muy, muy importante <- el código debe ser fácil de leer y entender. Sé que no hay necesidad de micro optimizaciones, ya que los compiladores modernos son muy inteligentes y las optimizaciones del compilador son muy poderosas. Pero también sé lo caro que es el manejo de excepciones , cómo (algunos) smart_pointers están implementados, y que no hay necesidad de smart_ptr todo el tiempo ... Sé que, por ejemplo, atoi no es tan "seguro" como std::stringstream es, pero aún así ... ¿Qué pasa con el rendimiento?

EDIT : No estoy hablando de algunas cosas realmente difíciles, que son solo específicas de C-style. Quiero decir, no se sorprenda de usar punteros de función o métodos virtuales y este tipo de cosas, que un programador de C ++ puede no saber, si nunca usó tales cosas (mientras que los programadores de C hacen esto todo el tiempo). Estoy hablando de algunas cosas más comunes y fáciles, como en los ejemplos.


  1. Las funciones con argumentos char* mutables son malas en C ++ porque es demasiado difícil manejar manualmente su memoria, ya que tenemos alternativas. No son genéricos, no podemos cambiar fácilmente de char a wchar_t como basic_string permite basic_string . También lexical_cast es un reemplazo más directo para atoi , itoa .
  2. Si realmente no necesita la inteligencia de un puntero inteligente, no lo use.
  3. Para intercambiar utilizar swap . Utilice operaciones bitwise solo para operaciones bitwise: verificación / configuración / inversión de indicadores, etc.
  4. Las excepciones son rápidas. Permiten eliminar las ramas de la condición de verificación de errores, por lo que si realmente "nunca suceden", aumentan el rendimiento.

¿Por qué el gasto de las excepciones es un argumento? Las excepciones son excepciones porque son raras. Su rendimiento no influye en el rendimiento general. Los pasos que debe seguir para que su código sea una excepción segura tampoco influyen en el rendimiento. Pero por otro lado las excepciones son convenientes y flexibles.


¿Qué es una mejor práctica? Las palabras de Wikipedia son mejores que las mías:

Una buena práctica es una técnica, método, proceso, actividad, incentivo o recompensa que la sabiduría convencional considera más eficaz para lograr un resultado particular que cualquier otra técnica, método, proceso, etc. cuando se aplica a una condición o circunstancia particular.

[...]

Una buena práctica dada solo es aplicable a una condición o circunstancia particular y puede tener que ser modificada o adaptada para circunstancias similares . Además, una "mejor" práctica puede evolucionar para ser mejor a medida que se descubren mejoras.

Creo que la verdad universal no existe en la programación: si crees que algo se ajusta mejor a tu situación que la llamada "mejor práctica", haz lo que creas que es correcto, pero sabe perfectamente por qué lo haces (es decir, : demuéstralo con números).


Agregando a la respuesta de @Jerry Coffin, que creo que es extremadamente útil, me gustaría presentar algunas observaciones subjetivas.

  • Lo que pasa es que los programadores tienden a ser elegantes. Es decir, a la mayoría de nosotros realmente nos gusta escribir código de fantasía solo por el simple hecho de hacerlo. Esto está perfectamente bien siempre y cuando esté haciendo el proyecto por su cuenta. Recuerde que un buen software es aquel cuyo código binario funciona como se espera y no aquel cuyo código fuente está limpio. Sin embargo, cuando se trata de proyectos más grandes que son desarrollados y mantenidos por mucha gente, es económicamente mejor escribir un código más simple para que nadie del equipo pierda el tiempo para entender lo que quería decir. Incluso al costo del tiempo de ejecución (costo naturalmente menor). Es por eso que muchas personas, incluido yo mismo, desalientan el uso del truco xor en lugar de la asignación (puede que te sorprenda, pero hay muchos programadores que no han oído hablar del truco xor). El truco de xor solo funciona para los enteros de todos modos, y la forma tradicional de intercambiar enteros es muy rápida de todos modos, por lo que usar el truco de xor es simplemente elegante.

  • usar itoa, atoi, etc. en lugar de flujos es más rápido. Sí lo es. Pero ¿cuánto más rápido? No mucho. A menos que la mayoría de su programa realice solo conversiones de texto a cadena y viceversa, no notará la diferencia. ¿Por qué la gente usa itoa, atoi, etc.? Bueno, algunos de ellos lo hacen, porque desconocen la alternativa de c ++. Otro grupo lo hace porque es solo un LOC. Para el primer grupo, qué vergüenza, para el segundo, ¿por qué no aumentar :: lexical_cast?

  • excepciones ... ah ... sí, pueden ser más lentos que los códigos de retorno, pero en la mayoría de los casos no lo son. Los códigos de retorno pueden contener información, que no es un error. Las excepciones se deben utilizar para informar errores graves, que no se pueden ignorar . Algunas personas se olvidan de esto y usan excepciones para simular algunos mecanismos de señal / ranura extraños (créanme, lo he visto, asco). Mi opinión personal es que no hay nada de malo en el uso de códigos de retorno, pero los errores graves deben informarse con excepciones, a menos que el generador de perfiles haya demostrado que abstenerse de ellos mejoraría considerablemente el rendimiento

  • punteros en bruto: mi opinión es la siguiente: nunca utilice punteros inteligentes cuando no se trata de propiedad. Siempre use punteros inteligentes cuando se trata de propiedad. Naturalmente con algunas excepciones.

  • Desplazamiento de bits en lugar de multiplicación por potencias de dos. Esto, creo, es un ejemplo clásico de optimización prematura. x << 3; Apuesto a que al menos el 25% de sus compañeros de trabajo necesitarán algo de tiempo antes de que comprendan / comprendan que esto significa x * 8 ; ¿Código ofuscado (al menos para el 25%) por qué razones exactas? Nuevamente, si el generador de perfiles le dice que este es el cuello de botella (lo cual dudo que sea el caso en casos extremadamente raros), entonces la luz verde, adelante y hágalo (dejando un comentario de que esto significa x * 8 )

En resumen. Un buen profesional reconoce los "buenos estilos", entiende por qué y cuándo son buenos y, con toda razón, hace excepciones porque sabe lo que está haciendo. Los profesionales promedio / malos se clasifican en 2 tipos: el primer tipo no reconoce el buen estilo, ni siquiera entiende qué y por qué lo es. despedirlos El otro tipo trata el estilo como un dogma, que no siempre es bueno.


Creo que estás respondiendo grandes partes de tu pregunta por tu cuenta. Personalmente prefiero el código fácil de leer (incluso si entiendes el estilo C, tal vez el siguiente para leer tu código tenga más problemas) y el código seguro (que sugiere cadenas de caracteres, excepciones, punteros inteligentes ...)

Si realmente tiene algo en lo que tiene sentido considerar operaciones bitwise, está bien. Pero a menudo veo a los programadores de C usar un char en lugar de un par de bools. No me gusta esto.

La velocidad es importante, pero la mayoría del tiempo generalmente se requiere en algunos puntos de acceso en un programa. Entonces, a menos que mida que alguna técnica es un problema (o sabe bastante seguro de que se convertirá en uno), preferiría usar lo que usted llama estilo C ++.


En general, lo que te falta es que la forma C a menudo no es más rápida. Simplemente se parece más a un hack, y la gente a menudo piensa que los hacks son más rápidos.

Nunca use punteros en bruto, use punteros inteligentes en su lugar. De acuerdo, son muy útiles, todo el mundo lo sabe, lo sé, lo uso todo el tiempo y sé cuán mejor son esos punteros en bruto, pero a veces es completamente seguro utilizar punteros en bruto ... ¿Por qué no?

Vamos a convertir la pregunta en su cabeza. A veces es seguro usar punteros en bruto. ¿Es eso solo una razón para usarlos? ¿Hay algo acerca de los punteros en bruto que sea realmente superior a los punteros inteligentes? Depende. Algunos tipos de punteros inteligentes son más lentos que los punteros en bruto. Otros no lo son. ¿Cuál es la razón de rendimiento para usar un puntero en bruto sobre std::unique_ptr o boost::scoped_ptr ? Ninguno de los dos tiene gastos generales, solo proporcionan una semántica más segura.

Esto no quiere decir que nunca se deben usar punteros en bruto. Solo que no deberías hacerlo solo porque crees que necesitas rendimiento, o simplemente porque "parece seguro". Hágalo cuando necesite representar algo que los punteros inteligentes no pueden. Como regla general, use punteros para señalar cosas, y punteros inteligentes para tomar posesión de las cosas. Pero es una regla de oro, no una regla universal. Utilice lo que se ajuste a la tarea en cuestión. Pero no asuma ciegamente que los punteros en bruto serán más rápidos. Y cuando use punteros inteligentes, asegúrese de estar familiarizado con todos ellos. Demasiada gente simplemente usa shared_ptr para todo, y eso es simplemente horrible, tanto en términos de rendimiento como en la vaga semántica de propiedad compartida que terminas aplicando a todo.

No use operaciones bitwise - ¿demasiado estilo C? ¿Qué? ¿Por qué no, cuando estás seguro de lo que estás haciendo? Por ejemplo, no haga un intercambio de variables a nivel de bits (a ^ = b; b ^ = a; a ^ = b;) - use el intercambio estándar de 3 pasos. No uses el turno de la izquierda para multiplicar por dos. Etc, etc. (OK, eso no es estilo C ++ vs. estilo C, pero aún así "no es una buena práctica")

Ese es el correcto. Y la razón es "es más rápido". El intercambio de bits es problemático de muchas maneras:

  • es más lento en una CPU moderna
  • Es más sutil y más fácil equivocarse.
  • Funciona con un conjunto muy limitado de tipos.

Y al multiplicar por dos, multiplica por dos . El compilador conoce este truco y lo aplicará si es más rápido . Y una vez más, el cambio tiene muchos de los mismos problemas. En este caso, puede ser más rápido (por lo que el compilador lo hará por usted), pero aún así es más fácil equivocarse y funciona con un conjunto limitado de tipos. En particular, podría compilarse bien con tipos que creas que es seguro hacer este truco con ... Y luego explotar en la práctica. En particular, el desplazamiento de bits en valores negativos es un campo minado. Deja que el compilador navegue por ti.

Por cierto, esto no tiene nada que ver con el "estilo C". El mismo consejo se aplica exactamente en C. En C, un intercambio regular es aún más rápido que el hackeo a nivel de bits, y el compilador seguirá haciendo cambios de bits en lugar de multiplicarse si es válido y si es más rápido.

Pero como programador, debe usar operaciones bitwise para una sola cosa: para hacer la manipulación bit a bit de los enteros. Ya tienes un operador de multiplicación, así que úsalo cuando quieras multiplicar. Y también tienes una función std::swap . Úsalo si quieres intercambiar dos valores. Uno de los trucos más importantes a la hora de optimizar es, tal vez sorprendentemente, escribir un código legible y significativo. Eso le permite a su compilador entender el código y optimizarlo. std::swap puede especializarse para realizar el intercambio más eficiente para el tipo particular en el que se usa. Y el compilador conoce varias formas de implementar la multiplicación, y elegirá la más rápida dependiendo de las circunstancias ... Si se lo dices. Si le dices que cambie un poco, en cambio, simplemente lo estás engañando. Dígale que se multiplique, y le dará la multiplicación más rápida que tiene.

Y, finalmente, el más costoso: "No use enum-s para devolver códigos, es demasiado estilo C, use excepciones para errores diferentes"?

Depende de a quién le preguntes. La mayoría de los programadores de C ++ que conozco encuentran espacio para ambos. Pero tenga en cuenta que una cosa desafortunada acerca de los códigos de retorno es que son fácilmente ignorados. Si eso es inaceptable, tal vez debería preferir una excepción en este caso. Otro punto es que RAII funciona mejor junto con las excepciones, y un programador de C ++ definitivamente debería usar RAII siempre que sea posible. Desafortunadamente, debido a que los constructores no pueden devolver códigos de error, las excepciones son a menudo la única forma de indicar errores.

Pero aún así ... ¿Qué pasa con el rendimiento?

Que hay de eso Cualquier programador decente de C estaría feliz de decirle que no optimice prematuramente.

Su CPU puede ejecutar quizás 8 mil millones de instrucciones por segundo. Si hace dos llamadas a un std::stringstream en ese segundo, ¿eso va a hacer una mella mensurable en el presupuesto?

No se puede predecir el rendimiento. No puede crear una directriz de codificación que dará como resultado un código rápido. Incluso si nunca lanza una sola excepción, y nunca usa el stringstream , su código aún no será rápido. Si intentas optimizar mientras escribes el código, gastarás el 90% del esfuerzo optimizando el 90% del código que casi nunca se ejecuta. Para obtener una mejora medible, debe centrarse en el 10% del código que constituye el 95% del tiempo de ejecución. Tratar de hacer que todo sea rápido solo se traduce en una gran cantidad de tiempo perdido, con poco que mostrar, y una base de código mucho más fea.


Esta no es realmente una "respuesta", pero si trabaja en un proyecto en el que el rendimiento es importante (por ejemplo, incrustado / juegos), las personas generalmente hacen la forma de C más rápida en lugar de la forma de C ++ más lenta de la manera que describió.

La excepción puede ser operaciones bitwise, donde no se gana tanto como se podría pensar. Por ejemplo, "No use el desplazamiento a la izquierda para multiplicar por dos". Un compilador medio decente generará el mismo código para << 2 y * 2.


La multiplicación por cambio de bits no mejora el rendimiento en C, el compilador lo hará por usted. Solo asegúrese de multiplicar o dividir por 2 ^ n valores para el rendimiento.

El intercambio de campos de bits también es algo que probablemente confundirá a su compilador.

No tengo mucha experiencia con el manejo de cadenas en C ++, pero por lo que sé, es difícil creer que sea más flexible que scanf y printf.

Además, estas declaraciones de "nunca debes", generalmente las considero recomendaciones.


Todas sus preguntas son a priori. Lo que quiero decir es que les está preguntando en abstracto, no en el contexto de ningún programa específico cuyo desempeño sea su preocupación. Eso es como tratar de nadar sin estar en el agua.

Si realiza la sintonía en un programa concreto específico, encontrará problemas de rendimiento y es probable que no tengan casi nada que ver con estas preguntas abstractas. Lo más probable es que todas sean cosas que no podrías haber pensado a priori.

Para un ejemplo específico de esto, mira aquí .

Si pudiera generalizar a partir de la experiencia, una fuente importante de problemas de rendimiento es la generalidad galopante . Es decir, si bien la abstracción de la estructura de datos generalmente se considera algo bueno, cualquier cosa buena puede ser sobreutilizada de forma masiva, y luego se convierte en una cosa incapacitante. Esto no es raro. En mi experiencia es típico.