sirven que punteros para operadores los lenguaje ejemplo direccion dev declaracion cadenas aritmetica apuntadores c++ gcc null compiler-optimization delete-operator

que - punteros c++



¿Por qué GCC no optimiza la eliminación de punteros nulos en C++? (6)

Creo que el compilador no tiene conocimiento sobre "eliminar", especialmente que "eliminar nulo" es un NOOP.

Puede escribirlo explícitamente, por lo que el compilador no necesita implicar conocimiento sobre la eliminación.

ADVERTENCIA: No lo recomiendo como implementación general. El siguiente ejemplo debería mostrar cómo podrías "convencer" a un compilador limitado de eliminar el código de todos modos en ese programa muy especial y limitado.

int main() { int* ptr = nullptr; if (ptr != nullptr) { delete ptr; } }

Donde recuerdo bien, hay una manera de reemplazar "eliminar" con una función propia. Y en el caso de que una optimización por el compilador saliera mal.

@RichardHodges: ¿Por qué debería ser una des-optimización cuando uno da al compilador la pista para eliminar una llamada?

eliminar null es en general un NOOP (sin operación). Sin embargo, dado que es posible reemplazar o sobrescribir la eliminación, no hay garantías para todos los casos.

Depende del compilador saber y decidir si usar el conocimiento que eliminar null siempre podría eliminarse. hay buenos argumentos para ambos choises

Sin embargo, el compilador siempre puede eliminar el código muerto, esto "if (false) {...}" or "if (nullptr! = Nullptr) {...}"

Entonces, un compilador eliminará el código muerto y luego, al usar una verificación explícita, se verá como

int main() { int* ptr = nullptr; // dead code if (ptr != nullptr) { // delete ptr; // } }

Por favor, dime, ¿dónde hay una desoptimización?

Yo llamo a mi propuesta un estilo defensivo de codificación, pero no una des-optimización

Si alguien puede argumentar, que ahora el non-nullptr causará dos veces la comprobación en nullptr, tengo que responder

  1. Lo siento, esta no era la pregunta original
  2. si el compilador sabe acerca de la eliminación, especialmente que eliminar null es un noop, entonces el compilador podría eliminar el externo si cualquiera de los dos. Sin embargo, no esperaría que los compiladores sean tan específicos

@ Peter Cordes: Estoy de acuerdo con una regla de optimización si no es general. Sin embargo, la optimización general NO era la cuestión del abridor. La pregunta era por qué algunos compiladores no eliman la eliminación en un programa muy breve y carente de sentido. Mostré una forma de hacer que el compilador elimine de todos modos.

Si ocurre una situación como en ese programa corto, probablemente algo diferente está mal. En general, trataría de evitar new / delete (malloc / free) ya que las llamadas son bastante caras. Si es posible, prefiero usar la pila (automático).

Cuando echo un vistazo al caso real documentado mientras tanto, diría que la clase X está mal diseñada, lo que provoca un rendimiento pobre y demasiada memoria. ( https://godbolt.org/g/7zGUvo )

En lugar de

class X { int* i_; public: ...

en el diseño sería

class X { int i; bool valid; public: ...

o más temprano, le pediría la sensación de ordenar elementos vacíos / inválidos. Al final, me gustaría deshacerme de "válido" también.

Considere un programa simple:

int main() { int* ptr = nullptr; delete ptr; }

Con GCC (7.2), hay una instrucción de call relacionada con la operator delete en el programa resultante. Con los compiladores Clang e Intel, no existen tales instrucciones, la eliminación del puntero nulo está completamente optimizada ( -O2 en todos los casos). Puede probar aquí: https://godbolt.org/g/JmdoJi .

Me pregunto si esa optimización puede activarse de alguna manera con GCC. (Mi motivación más amplia proviene de un problema de swap personalizado vs std::swap para tipos móviles, donde la eliminación de punteros nulos puede representar una penalización de rendimiento en el segundo caso; consulte https://stackoverflow.com/a/45689282/580083 para detalles)

ACTUALIZAR

Para aclarar mi motivación para la pregunta: si uso simplemente delete ptr; sin if (ptr) guard en un operador de asignación de movimiento y un destructor de alguna clase, std::swap con objetos de esa clase produce 3 instrucciones de call con GCC. Esto podría ser una penalización de rendimiento considerable, por ejemplo, al ordenar una matriz de tales objetos.

Además, puedo escribir if (ptr) delete ptr; en todas partes, pero me pregunto si esto no puede ser una penalización de rendimiento también, ya que la expresión de delete necesita comprobar ptr . Pero, aquí, supongo, los compiladores generarán solo un cheque.

Además, me gusta mucho la posibilidad de llamar delete sin el guardia y fue una sorpresa para mí, que podría arrojar resultados diferentes (rendimiento).

ACTUALIZAR

Acabo de hacer un punto de referencia simple, es decir, ordenar objetos, que invocan delete en su operador de asignación de movimiento y destructor. La fuente está aquí: https://godbolt.org/g/7zGUvo

Tiempos de ejecución de std::sort medidos con GCC 7.1 y -O2 bandera en Xeon E2680v3:

Hay un error en el código vinculado, compara punteros, no valores apuntados. Los resultados corregidos son los siguientes:

  1. sin if guardia: 17.6 [s] 40.8 [s] ,
  2. con if guardia: 10.6 [s] 31.5 [s] ,
  3. con if guard y swap personalizado: 10.4 [s] 31.3 [s].

Estos resultados fueron absolutamente consistentes en muchas ejecuciones con una desviación mínima. La diferencia de rendimiento entre los primeros dos casos es significativa y no diría que este es un caso similar al "extremadamente curioso caso".


De acuerdo con C ++ 14 [expr.delete] / 7:

Si el valor del operando de la expresión de eliminación no es un valor de puntero nulo, entonces:

  • [... omitido ...]

De lo contrario, no se especifica si se llamará a la función de desasignación.

Por lo tanto, ambos compiladores cumplen con el estándar, ya que no se especifica si se operator delete para eliminar un puntero nulo.

Tenga en cuenta que el compilador en línea godbolt solo compila el archivo fuente sin vincularlo. Entonces el compilador en esa etapa debe permitir la posibilidad de que la operator delete sea ​​reemplazada por otro archivo fuente.

Como ya se especuló en otra respuesta, gcc puede estar buscando un comportamiento consistente en el caso de una operator delete reemplazo; esta implementación significaría que alguien puede sobrecargar esa función con fines de depuración y romper todas las invocaciones de la expresión de delete , incluso cuando se elimine un puntero nulo.

ACTUALIZADO: Se ha eliminado la especulación de que esto podría no ser un problema práctico, ya que OP proporcionó puntos de referencia que muestran que de hecho lo es.


El estándar realmente establece cuándo se deben llamar las funciones de asignación y desasignación y dónde no. Esta cláusula (@ n4296)

La biblioteca proporciona definiciones predeterminadas para las funciones globales de asignación y desasignación. Algunas funciones globales de asignación y desasignación son reemplazables (18.6.1). Un programa C ++ debe proporcionar como máximo una definición de una función reemplazable de atribución o desasignación. Cualquier definición de función reemplaza a la versión predeterminada proporcionada en la biblioteca (17.6.4.6). Las siguientes funciones de asignación y desasignación (18.6) se declaran implícitamente en alcance global en cada unidad de traducción de un programa.

probablemente sea la razón principal por la cual esas llamadas a funciones no se omiten arbitrariamente. Si lo fueran, el reemplazo de su implementación de la biblioteca causaría una función incoherente del programa compilado.

En la primera alternativa (eliminar objeto), el valor del operando de eliminar puede ser un valor de puntero nulo, un puntero a un objeto que no sea de matriz creado por una nueva expresión previa o un puntero a un subobjeto (1.8) que represente una clase base de tal objeto (Cláusula 10). Si no, el comportamiento no está definido.

Si el argumento dado a una función de desasignación en la biblioteca estándar es un puntero que no es el valor del puntero nulo (4.10), la función de desasignación desasignará el almacenamiento al que hace referencia el puntero, invalidando todos los punteros en referencia a cualquier parte del almacenamiento desasignado . La indicación a través de un valor de puntero no válido y la transferencia de un valor de puntero no válido a una función de desasignación tienen un comportamiento indefinido. Cualquier otro uso de un valor de puntero no válido tiene un comportamiento definido por la implementación.

...

Si el valor del operando de la expresión de eliminación no es un valor de puntero nulo, entonces

  • Si la llamada de asignación para la nueva expresión para el objeto que se va a eliminar no se omitió y la asignación no se extendió (5.3.4), la expresión de eliminación llamará a una función de desasignación (3.7.4.2). El valor devuelto por la llamada de asignación de la nueva expresión se pasará como el primer argumento de la función de desasignación.

  • De lo contrario, si la asignación se extendió o se proporcionó ampliando la asignación de otra newexpression, y la expresión de eliminación para cada otro valor de puntero producido por una nueva expresión que tenía almacenamiento proporcionado por la nueva expresión extendida se ha evaluado, la eliminación -expresión llamará una función de desasignación. El valor devuelto por la llamada de asignación de la nueva expresión extendida se pasará como el primer argumento de la función de desasignación.

    • De lo contrario, la expresión de eliminación no llamará a una función de desasignación

De lo contrario, no se especifica si se llamará a la función de desasignación.

El estándar indica lo que se debe hacer si el puntero NO es nulo. Implicando que borrar en ese caso es noop, pero para qué fin, no se especifica.


En primer lugar, voy a estar de acuerdo con algunos contestadores previos en que no es un error, y GCC puede hacer lo que quiera aquí. Dicho esto, me preguntaba si esto significa que algún código RAII común y simple puede ser más lento en GCC que Clang porque no se realiza una optimización directa.

Así que escribí un pequeño caso de prueba para RAII:

struct A { explicit A() : ptr(nullptr) {} A(A &&from) : ptr(from.ptr) { from.ptr = nullptr; } A &operator =(A &&from) { if ( &from != this ) { delete ptr; ptr = from.ptr; from.ptr = nullptr; } return *this; } int *ptr; }; A a1; A getA2(); void setA1() { a1 = getA2(); }

Como puede ver here , GCC omite la segunda llamada para delete en setA1 (para el temporal getA2 que se creó en la llamada a getA2 ). La primera llamada es necesaria para la corrección del programa porque a1 o a1.ptr pueden haber sido previamente asignados a.

Obviamente, preferiría más "rima y razón", ¿por qué se realiza la optimización a veces, pero no siempre if ( ptr != nullptr ) Pero no estoy dispuesto a rociar redundante if ( ptr != nullptr ) comprueba todo mi código RAII por el momento.



Siempre es seguro (para ser correcto) permitir que el programa llame al operator delete con un nullptr.

Para el rendimiento, es muy raro que tener el asm generado por el compilador realmente haga una prueba adicional y la rama condicional para omitir una llamada al operator delete sea ​​una ganancia. (No obstante, puede ayudar a optimizar la eliminación nullptr tiempo de compilación de gcc sin agregar una comprobación de tiempo de ejecución; consulte más abajo).

En primer lugar, un tamaño de código mayor fuera de un punto crítico real aumenta la presión en el caché L1I, y el caché uop descodificado aún más pequeño en las CPU x86 que tienen uno (Intel SnB-family, AMD Ryzen).

En segundo lugar, las ramas condicional adicionales agotan las entradas en los cachés de predicción de bifurcación (BTB = Branch Target Buffer, etc.). Dependiendo de la CPU, incluso una sucursal que nunca se toma puede empeorar las predicciones para otras ramas si alias en el BTB. (En otros, tal rama nunca recibe una entrada en el BTB, para guardar las entradas de las sucursales donde la predicción estática por defecto del fall-through es precisa.) Consulte https://xania.org/201602/bpu-part-one .

Si nullptr es raro en una ruta de código dada, entonces, en promedio, la verificación y la sucursal para evitar la call terminan con su programa gastando más tiempo en el cheque que el que se guarda.

Si el perfil muestra que tiene un punto caliente que incluye una delete , y la instrumentación / registro muestra que a menudo llama a delete con un nullptr, entonces vale la pena intentarlo
if (ptr) delete ptr; en lugar de simplemente delete ptr;

La predicción de la sucursal puede tener mejor suerte en ese único sitio de llamadas que para la sucursal dentro de la operator delete , especialmente si hay alguna correlación con otras sucursales cercanas. (Las BPU aparentemente modernas no solo miran cada una de las sucursales de forma aislada). Esto se suma a guardar la call incondicional en la función de la biblioteca (más otro jmp del resguardo PLT, de la sobrecarga dinámica de enlace en Unix / Linux).

Si está buscando null por cualquier otra razón, entonces podría tener sentido colocar la delete dentro de la rama no nula de su código.

Puede evitar delete llamadas en casos en que gcc pueda probar (después de la alineación) que un puntero es nulo, pero sin hacer una comprobación de tiempo de ejecución si no es así :

static inline bool is_compiletime_null(const void *ptr) { #ifdef __GNUC__ // __builtin_constant_p(ptr) is false even for nullptr, // but the checking the result of booleanizing works. return __builtin_constant_p(!ptr) && !ptr; #else return false; #endif }

Siempre devolverá false con clang porque evalúa __builtin_constant_p antes de __builtin_constant_p . Pero dado que clang ya omite las llamadas de delete cuando puede probar que un puntero es nulo, no lo necesita.

En realidad, esto podría ayudar en casos std::move , y puede usarlo de manera segura en cualquier lugar con (en teoría) ningún inconveniente de rendimiento. Siempre compilo a if(true) o if(false) , por lo que es muy diferente de if(ptr) , lo que probablemente resulte en una rama de tiempo de ejecución porque el compilador probablemente no puede probar que el puntero no es nulo en la mayoría de los casos ya sea. (Sin embargo, una desreferencia, porque un deref nulo sería UB, y los compiladores modernos optimizados en base a la suposición de que el código no contiene ningún UB).

Podría hacer esto como una macro para evitar la formación de estructuras no optimizadas (y así "funcionaría" sin tener que alinear primero). Puede usar una expresión de enunciado C de GNU para evitar la doble evaluación de la macroarg ( vea ejemplos para GNU C min() y max() ). Para el respaldo de compiladores sin extensiones GNU, puede escribir ((ptr), false) o algo para evaluar el arg una vez para los efectos secundarios mientras produce un resultado false .

Demostración: asm de gcc6.3 -O3 en el explorador compilador Godbolt

void foo(int *ptr) { if (!is_compiletime_null(ptr)) delete ptr; } # compiles to a tailcall of operator delete jmp operator delete(void*) void bar() { foo(nullptr); } # optimizes out the delete rep ret

Se compila correctamente con MSVC (también en el enlace del compilador), pero con la prueba que siempre devuelve falso, bar() es:

# MSVC doesn''t support GNU C extensions, and doesn''t skip nullptr deletes itself mov edx, 4 xor ecx, ecx jmp ??3@YAXPEAX_K@Z ; operator delete

Es interesante notar que la operator delete del operator delete toma el tamaño del objeto como una función arg ( mov edx, 4 ), pero el código gcc / Linux / libstdc ++ simplemente pasa el puntero.

Relacionado: Encontré esta publicación de blog , usando C11 (no C ++ 11) _Generic para intentar hacer algo __builtin_constant_p como __builtin_constant_p null-pointer checks dentro de los inicializadores estáticos.